From cf0cc3460aa7c5dcbd4cdf5f14183459410ca688 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:02:28 +0800 Subject: [PATCH 01/32] feat(premium-analytics): copy data package from next-woocommerce-analytics --- .../premium-analytics/packages/data/README.md | 429 ++++++++++++++++++ .../packages/data/package.json | 23 + .../packages/data/src/api/constants.ts | 4 + .../packages/data/src/api/index.ts | 48 ++ .../src/api/report-bookings-fetch/index.ts | 5 + .../report-bookings-fetch.ts | 59 +++ .../api/report-conversion-rate-fetch/index.ts | 4 + .../report-conversion-rate-fetch.ts | 59 +++ .../api/report-coupons-by-date-fetch/index.ts | 4 + .../report-coupons-by-date-fetch.ts | 68 +++ .../src/api/report-coupons-fetch/index.ts | 4 + .../report-coupons-fetch.ts | 55 +++ .../report-customers-by-date-fetch/index.ts | 1 + .../report-customers-by-date-fetch.ts | 78 ++++ .../src/api/report-customers-fetch/index.ts | 4 + .../report-customers-fetch.ts | 57 +++ .../data/src/api/report-export-fetch/index.ts | 5 + .../report-export-fetch.ts | 61 +++ .../index.ts | 1 + ...port-order-attribution-by-product-fetch.ts | 77 ++++ .../index.ts | 6 + .../report-order-attribution-summary-fetch.ts | 90 ++++ .../data/src/api/report-orders-fetch/index.ts | 5 + .../report-orders-fetch.ts | 69 +++ .../src/api/report-products-fetch/index.ts | 7 + .../report-products-fetch.ts | 76 ++++ .../report-sessions-by-device-fetch/index.ts | 4 + .../report-sessions-by-device-fetch.ts | 64 +++ .../index.ts | 4 + .../report-visitors-by-location-fetch.ts | 61 +++ .../src/api/report-visitors-fetch/index.ts | 4 + .../report-visitors-fetch.ts | 43 ++ .../__tests__/get-default-preset.test.ts | 107 +++++ .../packages/data/src/defaults/index.ts | 1 + .../packages/data/src/defaults/reports.ts | 116 +++++ .../packages/data/src/hooks/index.ts | 12 + .../data/src/hooks/use-product-images.ts | 94 ++++ .../data/src/hooks/use-report-bookings.ts | 18 + .../src/hooks/use-report-conversion-rate.ts | 25 + .../src/hooks/use-report-coupons-by-date.ts | 17 + .../data/src/hooks/use-report-coupons.ts | 17 + .../src/hooks/use-report-customers-by-date.ts | 26 ++ .../data/src/hooks/use-report-customers.ts | 18 + .../src/hooks/use-report-order-attribution.ts | 66 +++ .../data/src/hooks/use-report-orders.ts | 26 ++ .../data/src/hooks/use-report-products.ts | 17 + .../hooks/use-report-sessions-by-device.ts | 45 ++ .../hooks/use-report-visitors-by-location.ts | 39 ++ .../data/src/hooks/use-report-visitors.ts | 26 ++ .../packages/data/src/hooks/use-report.ts | 160 +++++++ .../packages/data/src/index.ts | 53 +++ .../packages/data/src/prefetch/index.ts | 1 + .../data/src/prefetch/prefetch-report.ts | 122 +++++ .../data/src/processing/bookings/index.ts | 119 +++++ .../src/processing/conversion-rate/index.ts | 155 +++++++ .../src/processing/coupons-by-date/index.ts | 100 ++++ .../data/src/processing/coupons/index.ts | 84 ++++ .../src/processing/customers-by-date/index.ts | 179 ++++++++ .../data/src/processing/customers/index.ts | 88 ++++ .../packages/data/src/processing/index.ts | 12 + .../src/processing/order-attribution/index.ts | 2 + ...e-order-attribution-by-product-response.ts | 90 ++++ ...tize-order-attribution-summary-response.ts | 115 +++++ .../orders-by-product-type/index.ts | 82 ++++ .../data/src/processing/orders/index.ts | 81 ++++ .../data/src/processing/products/index.ts | 83 ++++ .../processing/sessions-by-device/index.ts | 83 ++++ .../packages/data/src/processing/utils.ts | 11 + .../processing/visitors-by-location/index.ts | 77 ++++ .../data/src/processing/visitors/index.ts | 83 ++++ .../src/providers/global-error-context.tsx | 114 +++++ .../src/providers/global-error-manager.ts | 35 ++ .../packages/data/src/providers/index.ts | 11 + .../src/providers/query-client-provider.tsx | 151 ++++++ .../packages/data/src/queries/index.ts | 12 + .../data/src/queries/report-bookings-query.ts | 49 ++ .../queries/report-conversion-rate-query.ts | 48 ++ .../queries/report-coupons-by-date-query.ts | 51 +++ .../data/src/queries/report-coupons-query.ts | 51 +++ .../queries/report-customers-by-date-query.ts | 50 ++ .../src/queries/report-customers-query.ts | 48 ++ .../report-order-attribution-summary-query.ts | 144 ++++++ .../data/src/queries/report-orders-query.ts | 45 ++ .../data/src/queries/report-products-query.ts | 53 +++ .../report-sessions-by-device-query.ts | 47 ++ .../report-visitors-by-location-query.ts | 46 ++ .../data/src/queries/report-visitors-query.ts | 48 ++ .../packages/data/src/types.ts | 120 +++++ .../data/src/types/filter-condition.ts | 23 + .../packages/data/src/types/product-image.ts | 7 + .../packages/data/src/types/product-type.ts | 4 + .../compute-date-range-from-preset.test.ts | 160 +++++++ .../__tests__/has-comparison-enabled.test.ts | 82 ++++ .../__tests__/normalize-report-params.test.ts | 294 ++++++++++++ .../packages/data/src/utils/date.ts | 122 +++++ .../data/src/utils/ensure-core-settings.ts | 25 + .../packages/data/src/utils/index.ts | 15 + .../packages/data/src/utils/interval.ts | 102 +++++ .../packages/data/src/utils/parsing.ts | 15 + .../data/src/utils/preset-date-range.ts | 35 ++ .../data/src/utils/product-filters.ts | 25 + .../packages/data/src/utils/search.ts | 155 +++++++ .../packages/data/src/utils/types.ts | 29 ++ .../packages/data/tsconfig.json | 9 + 104 files changed, 6154 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/data/README.md create mode 100644 projects/packages/premium-analytics/packages/data/package.json create mode 100644 projects/packages/premium-analytics/packages/data/src/api/constants.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/defaults/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/defaults/reports.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/prefetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/products/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/utils.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types/product-image.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types/product-type.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/date.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/interval.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/parsing.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/search.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/types.ts create mode 100644 projects/packages/premium-analytics/packages/data/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/data/README.md b/projects/packages/premium-analytics/packages/data/README.md new file mode 100644 index 000000000000..3c6f603476e2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/README.md @@ -0,0 +1,429 @@ +# @next-woo-analytics/data + +Data management package for WooCommerce Analytics with React Query +integration. + +## Installation + +This package is an internal dependency of the WooCommerce Analytics NextAdmin integration. It's automatically available when working within the NextAdmin framework. + +```tsx +import { + AnalyticsQueryClientProvider, + useReport, + prefetchReport, + // ... other exports +} from '@next-woo-analytics/data'; +``` + +## Features + +- **React Query Integration**: Built on `@tanstack/react-query` for + caching and state management +- **Prefetching Support**: Route-based data prefetching for improved + performance +- **Data Processing**: Automatic sanitization of API responses + (strings → numbers) +- **Comparison Support**: Built-in primary + comparison data fetching +- **TypeScript Support**: Fully typed data structures and API responses +- **Smart Caching**: Automatic cache invalidation and background + refetching + +## Usage + +### Setup + +```tsx +import { AnalyticsQueryClientProvider } from '@next-woo-analytics/data'; + +function App() { + return ( + + {/* Your app components */} + + ); +} +``` + +### Fetching Data + +```tsx +import { + useReportOrders, + useReportOrdersByProductType, + useReportOrderAttribution, + useReportCoupons +} from '@next-woo-analytics/data'; + +function OrdersReport() { + // Orders endpoint separates primary and comparison periods + const { primary, comparison, hasComparison } = useReportOrders( { + from: '2025-07-01T00:00:00', + to: '2025-07-29T23:59:59', + interval: 'day', + } ); +} + +function OrdersByProductTypeReport() { + // Orders by product type with filtering support + const { primary, comparison, hasComparison } = useReportOrdersByProductType( { + from: '2025-07-01T00:00:00', + to: '2025-07-29T23:59:59', + interval: 'day', + filters: [ + { + key: 'product_type', + value: ['simple'], + compare: 'IN' + } + ] + } ); +} + +function OrderAttributionReport() { + // Order attribution requires a view parameter + const { primary, comparison, hasComparison } = useReportOrderAttribution( { + from: '2025-07-01T00:00:00', + to: '2025-07-29T23:59:59', + interval: 'day', + view: 'channel', // 'channel' | 'source' | 'campaign' | 'device' | 'channel-source' + } ); +} + +function CouponsReport() { + // Coupons works like orders with separate comparison requests + const { primary, comparison, hasComparison } = useReportCoupons( { + from: '2025-07-01T00:00:00', + to: '2025-07-29T23:59:59', + interval: 'day', + } ); +} +``` + +### Prefetching + +```tsx +import { prefetchReport, ensureCoreSettingsReady } from '@next-woo-analytics/data'; + +export const route = { + beforeLoad: async () => { + // Ensure site settings are loaded first + await ensureCoreSettingsReady(); + + // Now safely prefetch reports + await prefetchReport( 'orders' ); + }, +}; +``` + +## API Reference + +### Individual Hooks (Recommended) + +#### `useReportOrders( params )` + +Fetches orders report data with automatic processing and comparison support. + +**Parameters:** +- `params`: `ReportParams` with `from`, `to`, `interval`, and optional comparison params + +**Returns:** `{ primary, comparison, hasComparison }` + +#### `useReportOrdersByProductType( params )` + +Fetches orders report data filtered by product type or other product characteristics with automatic processing and comparison support. + +**Parameters:** +- `params`: `ReportParams` with `from`, `to`, `interval`, optional `filters` array, and optional comparison params + +**Filters Structure:** +```typescript +filters: Array<{ + key: string; // e.g., 'product_type', 'virtual' + value: string | string[]; // e.g., ['simple'], '1' + compare: '=' | 'IN' | 'NOT IN' | '!=' | '>' | '<' | '>=' | '<='; +}> +``` + +**Common Filter Examples:** +- Product types: `{ key: 'product_type', value: ['simple', 'variable'], compare: 'IN' }` +- Virtual products: `{ key: 'virtual', value: '1', compare: '=' }` +- Non-virtual products: `{ key: 'virtual', value: '0', compare: '=' }` + +**Returns:** `{ primary, comparison, hasComparison }` + +#### `useReportOrderAttribution( params )` + +Fetches order attribution data with built-in comparison handling. + +**Parameters:** +- `params`: `ReportParams` with `from`, `to`, `interval`, `view`, and optional comparison params + +**Returns:** `{ primary, comparison, hasComparison }` + +#### `useReportCoupons( params )` + +Fetches coupons report data with automatic processing and comparison support. + +**Parameters:** +- `params`: `ReportParams` with `from`, `to`, `interval`, and optional comparison params + +**Returns:** `{ primary, comparison, hasComparison }` + +### Legacy Hook (Deprecated) + +#### `useReport( reportType, params )` + +**⚠️ Deprecated:** Use individual hooks instead for better type safety and performance. + +**Parameters:** +- `reportType`: `'orders'` | `'orders-by-product-type'` | `'order-attribution'` | `'coupons'` +- `params`: `ReportParams` + +**Returns:** Same as individual hooks above + +### `prefetchReport( reportType, params )` + +Prefetches data for improved performance. Same parameters as `useReport`. + +**Usage:** Call in route `beforeLoad` functions for instant data loading + +**Returns:** Promise that resolves when data is prefetched + +**Caching:** Uses React Query cache, so subsequent `useReport` calls are +instant + +**Example:** +```tsx +// Prefetch orders data +await prefetchReport( 'orders', { from, to, interval } ); + +// Prefetch orders by product type data +await prefetchReport( 'orders-by-product-type', { + from, + to, + interval, + filters: [{ key: 'product_type', value: ['simple'], compare: 'IN' }] +} ); + +// Prefetch order attribution data +await prefetchReport( 'order-attribution', { from, to, interval, view: 'channel' } ); + +// Prefetch coupons data +await prefetchReport( 'coupons', { from, to, interval } ); +``` + +### `normalizeReportParams( params? )` + +Normalizes and validates report parameters, providing defaults when needed. + +**Parameters:** +- `params`: Optional partial parameters object + +**Returns:** `{ primary, comparison? }` with normalized parameters + +**Defaults:** Last 30 days, daily interval when not specified + +**Validation:** Ensures required fields are present for API calls + +### `getDefaultIntervalForPeriod( period, from, to )` + +Returns the optimal default interval for a given time period. + +**Parameters:** +- `period`: `string` - Period identifier (e.g., 'today', 'last-7-days', 'last-30-days') +- `from`: `string` - Start date +- `to`: `string` - End date + +**Returns:** `IntervalType` - Optimal interval ('hour', 'day', 'week', 'month', 'quarter', 'year') + +**Example:** +```tsx +import { getDefaultIntervalForPeriod } from '@next-woo-analytics/data'; + +const interval = getDefaultIntervalForPeriod( 'last-7-days', from, to ); // Returns 'day' +``` + +### `ORDER_ATTRIBUTION_VIEWS` + +Constant array of available order attribution views. + +**Values:** `['channel', 'source', 'campaign', 'device', 'channel-source']` + +**Example:** +```tsx +import { ORDER_ATTRIBUTION_VIEWS } from '@next-woo-analytics/data'; + +// Use in components for view selection +const views = ORDER_ATTRIBUTION_VIEWS; // ['channel', 'source', ...] +``` + +## Architecture + +``` +src/ +├── api/ # API functions and query keys +│ ├── index.ts # API exports +│ ├── constants.ts # Shared API endpoint constants +│ ├── report-orders-fetch/ # Orders API client +│ │ ├── index.ts # Orders API exports +│ │ └── report-orders-fetch.ts # Orders API implementation +│ ├── report-orders-by-product-type-fetch/ # Orders by product type API client +│ │ ├── index.ts # Orders by product type API exports +│ │ └── report-orders-by-product-type-fetch.ts # Orders by product type API implementation +│ ├── report-order-attribution-summary-fetch/ # Attribution API client +│ │ ├── index.ts # Attribution API exports +│ │ └── report-order-attribution-summary-fetch.ts # Attribution API implementation +│ └── report-coupons-fetch/ # Coupons API client +│ ├── index.ts # Coupons API exports +│ └── report-coupons-fetch.ts # Coupons API implementation +├── queries/ # React Query configurations +│ ├── index.ts # Query exports +│ ├── report-orders-query.ts # Orders query definition +│ ├── report-orders-by-product-type-query.ts # Orders by product type query definition +│ ├── report-order-attribution-summary-query.ts # Attribution query definition +│ └── report-coupons-query.ts # Coupons query definition +├── hooks/ # React hooks +│ ├── index.ts # Hook exports +│ └── use-report.ts # Main useReport hook with comparison +├── prefetch/ # Prefetching functions +│ ├── index.ts # Prefetch exports +│ └── prefetch-report-orders.ts # Multi-report prefetch logic (orders + attribution + coupons) +├── processing/ # Data sanitization and transformation +│ ├── orders/ # Orders-specific data processing +│ ├── orders-by-product-type/ # Orders by product type data processing +│ ├── coupons/ # Coupons data processing +│ └── order-attribution/ # Attribution data processing +├── providers/ # React Context providers +│ ├── index.ts # Provider exports +│ └── query-client-provider.tsx # React Query client setup +├── defaults/ # Default parameters and configurations +├── utils/ # Utility functions +│ ├── date.ts # Date manipulation utilities (timezone-aware) +│ ├── ensure-core-settings.ts # Core settings initialization +│ ├── interval.ts # Interval calculation and optimization +│ ├── search.ts # Search parameter utilities +│ └── types.ts # Shared utility types (Override, etc.) +└── types.ts # TypeScript type definitions +``` + +### Data Flow + +1. **Route Prefetching**: `beforeLoad` calls `prefetchReport()` to load + data +2. **Component Consumption**: Components use `useReport()` to access cached + data +3. **Automatic Processing**: Raw API responses are sanitized + (strings → numbers) +4. **Comparison Handling**: Primary and comparison queries are managed + automatically +5. **Cache Management**: React Query handles caching, background updates, + and invalidation + +## Date Utilities + +This package provides timezone-aware date utilities that integrate with +WordPress site settings: + +### `localTZDate( value?, timezone? )` + +Creates a timezone-aware date using the site's configured timezone by +default. + +```typescript +import { localTZDate } from '@next-woo-analytics/data'; + +const now = localTZDate(); // Current time in site timezone +const custom = localTZDate( '2024-01-15', 'America/New_York' ); +``` + +**Parameters:** +- `value` (optional): `number | string | Date` - Date value to convert +- `timezone` (optional): `string` - Target timezone (defaults to site + timezone) + +**Returns:** `TZDate` - Timezone-aware date object + + +### `dateToISOStringWithLocalTZ( date, timezone? )` + +Converts a date to ISO string with the site's timezone offset applied. + +```typescript +const withTZ = dateToISOStringWithLocalTZ( new Date() ); +// Returns: "2024-01-15T14:30:00.000-05:00" (with site timezone offset) +``` + +**Parameters:** +- `date`: `Date` - Date to convert +- `timezone` (optional): `string` - Target timezone (defaults to site + timezone) + +**Returns:** `string` - ISO string with timezone offset + +### `getSiteTimezone()` + +Returns the WordPress site's configured timezone string. + +```typescript +const timezone = getSiteTimezone(); +// Returns: "America/New_York" or "+05:30" (offset format) +``` + +**Returns:** `string` - Site timezone from WordPress settings + +**Note:** This function will throw an error if called before core settings +are loaded. Use `ensureCoreSettingsReady()` in route loaders to prevent +this. + +### `ensureCoreSettingsReady()` + +Ensures WordPress core settings (site and general settings) are loaded +before accessing timezone-dependent functions. + +```typescript +// In route loaders or beforeLoad hooks +await ensureCoreSettingsReady(); +// Now getSiteTimezone() can be safely called +``` + +**Returns:** `Promise` - Resolves when settings are loaded + +**Features:** +- Memoizes the promise to avoid duplicate requests +- Prevents race conditions during navigation +- Essential for route prefetching and hover preloading + +These functions automatically use the WordPress site's timezone settings +and provide consistent date handling across the analytics interface. + +## Complete API Exports + +This package exports the following public API: + +### Components +- `AnalyticsQueryClientProvider` - React Query provider wrapper + +### Hooks +- `useReportOrders` - Hook for fetching orders report data +- `useReportOrdersByProductType` - Hook for fetching orders by product type with filtering +- `useReportOrderAttribution` - Hook for fetching order attribution data +- `useReportCoupons` - Hook for fetching coupons report data +- `useReport` - Legacy main hook for fetching report data (deprecated) + +### Functions +- `prefetchReport` - Prefetch data for routes +- `normalizeReportParams` - Normalize and validate parameters +- `getDefaultIntervalForPeriod` - Get optimal interval for time period + +### Date Utilities +- `localTZDate` - Create timezone-aware dates +- `dateToISOStringWithLocalTZ` - Convert to ISO with timezone +- `getSiteTimezone` - Get WordPress site timezone +- `ensureCoreSettingsReady` - Ensure settings are loaded + +### Constants +- `ORDER_ATTRIBUTION_VIEWS` - Available attribution view types + +### Types +- `ReportDataMap` - TypeScript type mapping for report data structures diff --git a/projects/packages/premium-analytics/packages/data/package.json b/projects/packages/premium-analytics/packages/data/package.json new file mode 100644 index 000000000000..cae8d1032019 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/package.json @@ -0,0 +1,23 @@ +{ + "name": "@next-woo-analytics/data", + "version": "1.0.0", + "type": "module", + "wpModule": true, + "main": "src/index.ts", + "exports": { + ".": "./build/src/index.js" + }, + "dependencies": { + "date-fns": "*", + "@date-fns/tz": "*", + "@tanstack/react-query": "*", + "@tanstack/react-router": "*", + "@wordpress/api-fetch": "*", + "@wordpress/url": "*", + "@next-woo-analytics/datetime": "workspace:*", + "@automattic/admin-toolkit": "*" + }, + "devDependencies": { + "@tanstack/react-query-devtools": "*" + } +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts new file mode 100644 index 000000000000..eb04e41b4633 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts @@ -0,0 +1,4 @@ +/** + * Constants for API endpoints + */ +export const reportsPath = '/wc/v3/woocommerce-analytics/proxy/reports'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/index.ts b/projects/packages/premium-analytics/packages/data/src/api/index.ts new file mode 100644 index 000000000000..6b8a8be840b6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts @@ -0,0 +1,48 @@ +/** + * Internal dependencies + */ +import type { RequestReportOrdersParams } from './report-orders-fetch'; +import type { RequestReportOrderAttributionSummaryParams } from './report-order-attribution-summary-fetch'; +import type { RequestReportOrderAttributionByProductParams } from './report-order-attribution-by-product-fetch'; +import type { RequestReportCouponsParams } from './report-coupons-fetch'; +import type { RequestReportCouponsByDateParams } from './report-coupons-by-date-fetch'; +import type { RequestReportCustomersParams } from './report-customers-fetch'; +import type { RequestReportProductsParams } from './report-products-fetch'; +import type { RequestReportVisitorsParams } from './report-visitors-fetch'; +import type { RequestReportVisitorsByLocationParams } from './report-visitors-by-location-fetch'; +import type { RequestReportBookingsParams } from './report-bookings-fetch'; +import type { RequestReportSessionsByDeviceParams } from './report-sessions-by-device-fetch'; + +export type ReportQueryParams = Partial< + RequestReportOrdersParams & + RequestReportOrderAttributionSummaryParams & + RequestReportOrderAttributionByProductParams & + RequestReportCouponsParams & + RequestReportCouponsByDateParams & + RequestReportCustomersParams & + RequestReportProductsParams & + RequestReportVisitorsParams & + RequestReportVisitorsByLocationParams & + RequestReportBookingsParams & + RequestReportSessionsByDeviceParams +>; + +export { fetchReportOrders } from './report-orders-fetch'; +export { + fetchReportOrderAttributionSummary, + ORDER_ATTRIBUTION_VIEWS, +} from './report-order-attribution-summary-fetch'; +export { fetchReportOrderAttributionByProduct } from './report-order-attribution-by-product-fetch'; +export { fetchReportCoupons } from './report-coupons-fetch'; +export { fetchReportCouponsByDate } from './report-coupons-by-date-fetch'; +export { fetchReportCustomers } from './report-customers-fetch'; +export { fetchReportProducts } from './report-products-fetch'; +export { fetchReportVisitors } from './report-visitors-fetch'; +export { fetchReportVisitorsByLocation } from './report-visitors-by-location-fetch'; +export { fetchReportBookings } from './report-bookings-fetch'; +export { fetchReportSessionsByDevice } from './report-sessions-by-device-fetch'; +export { exportReport } from './report-export-fetch'; +export type { + ExportReportParams, + ExportReportResponse, +} from './report-export-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts new file mode 100644 index 000000000000..8dd1607b5e9e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts @@ -0,0 +1,5 @@ +export { fetchReportBookings } from './report-bookings-fetch'; +export type { + ReportsBookingsByDateResponse, + RequestReportBookingsParams, +} from './report-bookings-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts new file mode 100644 index 000000000000..f85b19d565df --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; + +type ReportsBookingsByDateSummary = { + status_unpaid: string; + status_pending_confirmation: string; + status_confirmed: string; + status_paid: string; + status_cancelled: string; + status_complete: string; + attendance_status_booked: string; + attendance_status_no_show: string; + attendance_status_checked_in: string; + date_start: string; + date_end: string; +}; + +type BookingsReportDataItem = ReportsBookingsByDateSummary & { + time_interval: string; +}; + +export type ReportsBookingsByDateResponse = { + data: BookingsReportDataItem[]; + summary: ReportsBookingsByDateSummary; +}; + +export type RequestReportBookingsParams = BaseReportParams & { + filters?: FilterCondition[]; +}; + +export async function fetchReportBookings( { + from, + to, + interval, + filters, + date_type, +}: RequestReportBookingsParams ): Promise< ReportsBookingsByDateResponse > { + const apiUrl = `${ reportsPath }/bookings/by-date`; + + const path = addQueryArgs( apiUrl, { + from, + to, + interval, + filters, + date_type, + } ); + + return apiFetch( { path } ) as Promise< ReportsBookingsByDateResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts new file mode 100644 index 000000000000..f9cc046877b8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportConversionRate, + type RequestReportConversionRateParams, +} from './report-conversion-rate-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts new file mode 100644 index 000000000000..eb6a2aa2e8ed --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import type { FilterCondition } from '../../types/filter-condition'; +import { reportsPath } from '../constants'; + +type ReportsConversionRateByDateSummary = { + active_sessions: string; + visitors: string; + with_cart_addition: string; + reached_checkout: string; + completed_checkout: string; + date_end: string; + date_start: string; +}; + +type ConversionRateReportDataItem = { + date_start: string; + date_end: string; + active_sessions: string; + visitors: string; + with_cart_addition: string; + reached_checkout: string; + completed_checkout: string; +}; + +type ReportsConversionRateByDateResponse = { + data: ConversionRateReportDataItem[]; + summary: ReportsConversionRateByDateSummary; +}; + +export type RequestReportConversionRateParams = BaseReportParams & { + filters?: FilterCondition[]; +}; + +export async function fetchReportConversionRate( { + from, + to, + interval, + filters, +}: RequestReportConversionRateParams ): Promise< ReportsConversionRateByDateResponse > { + const path = addQueryArgs( `${ reportsPath }/sessions/by-conversion-rate`, { + from, + to, + interval, + filters, + } ); + + return apiFetch( { + path, + } ) as Promise< ReportsConversionRateByDateResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts new file mode 100644 index 000000000000..cd9dbe298e36 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportCouponsByDate, + type RequestReportCouponsByDateParams, +} from './report-coupons-by-date-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts new file mode 100644 index 000000000000..0115d56b0ce6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; + +type CouponsByDateDataItem = { + time_interval: string; + date_start: string; + date_end: string; + total_orders: string; + orders_with_coupon: string; + orders_without_coupon: string; + total_sales: string; + sales_with_coupon: string; + sales_without_coupon: string; + total_discount_amount: string; + net_sales_after_discount: string; + coupon_usage_percentage: string; +}; + +type CouponsByDateSummary = { + total_orders: string; + orders_with_coupon: string; + orders_without_coupon: string; + total_sales: string; + sales_with_coupon: string; + sales_without_coupon: string; + total_discount_amount: string; + net_sales_after_discount: string; + coupon_usage_percentage: string; + date_start: string; + date_end: string; +}; + +export type ReportsCouponsByDateResponse = { + summary: CouponsByDateSummary; + data: CouponsByDateDataItem[]; +}; + +export type RequestReportCouponsByDateParams = BaseReportParams & { + filters?: FilterCondition[]; +}; + +export async function fetchReportCouponsByDate( { + from, + to, + interval, + filters, + date_type, +}: RequestReportCouponsByDateParams ): Promise< ReportsCouponsByDateResponse > { + const path = addQueryArgs( `${ reportsPath }/coupons/by-date`, { + from, + to, + interval, + filters, + date_type, + } ); + + return apiFetch< ReportsCouponsByDateResponse >( { path } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts new file mode 100644 index 000000000000..4c4e60f45fd0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportCoupons, + type RequestReportCouponsParams, +} from './report-coupons-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts new file mode 100644 index 000000000000..d591c37b1e22 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; + +type CouponsDataItem = { + coupon_code: string; + discount_amount: string; + total_sales: string; + orders_count: string; +}; + +type CouponsDataSummary = { + total_sales: string; + total_discount_amount: string; + total_orders: string; + date_start: string; + date_end: string; +}; + +export type ReportsCouponsResponse = { + summary: CouponsDataSummary; + data: CouponsDataItem[]; +}; + +export type RequestReportCouponsParams = BaseReportParams & { + filters?: FilterCondition[]; +}; + +export async function fetchReportCoupons( { + from, + to, + interval, + filters, + date_type, +}: RequestReportCouponsParams ): Promise< ReportsCouponsResponse > { + const path = addQueryArgs( `${ reportsPath }/coupons/`, { + from, + to, + interval, + filters, + date_type, + orderby: 'total_sales', + } ); + + return apiFetch< ReportsCouponsResponse >( { path } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts new file mode 100644 index 000000000000..ec5993677f34 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts @@ -0,0 +1 @@ +export * from './report-customers-by-date-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts new file mode 100644 index 000000000000..6e6c6a60e81d --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +type ReportsCustomersByDateSummary = { + total_net_sales: string; + total_gross_sales: string; + total_discounts: string; + total_refunds: string; + total_orders: string; + total_average_order_value: string; + total_avg_items_per_order: string; + total_customers: string; + new_customers: string; + returning_customers: string; + new_customer_sales: string; + new_customer_gross_sales: string; + new_customer_discounts: string; + new_customer_refunds: string; + new_customer_orders: string; + new_customer_avg_order_value: string; + new_customer_avg_items_per_order: string; + returning_customer_sales: string; + returning_customer_gross_sales: string; + returning_customer_discounts: string; + returning_customer_refunds: string; + returning_customer_orders: string; + returning_customer_avg_order_value: string; + returning_customer_avg_items_per_order: string; + date_start: string; + date_end: string; +}; + +type CustomersReportDataItem = { + time_interval: string; + date_start: string; + date_end: string; + total_customers: string; + new_customers: string; + returning_customers: string; + orders_count: string; + new_customer_orders: string; + returning_customer_orders: string; + net_sales: string; + new_customer_net_sales: string; + returning_customer_net_sales: string; +}; + +type ReportsCustomersByDateResponse = { + data: CustomersReportDataItem[]; + summary: ReportsCustomersByDateSummary; +}; + +export type RequestReportCustomersByDateParams = BaseReportParams; + +export async function fetchReportCustomersByDate( { + from, + to, + interval, + date_type, +}: RequestReportCustomersByDateParams ): Promise< ReportsCustomersByDateResponse > { + const path = addQueryArgs( `${ reportsPath }/customers/by-date`, { + from, + to, + interval, + date_type, + } ); + + return apiFetch( { path } ) as Promise< ReportsCustomersByDateResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts new file mode 100644 index 000000000000..aca7bab2d69d --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportCustomers, + type RequestReportCustomersParams, +} from './report-customers-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts new file mode 100644 index 000000000000..35cdd79456dc --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import type { FilterCondition } from '../../types/filter-condition'; +import { reportsPath } from '../constants'; + +type CustomersNewReturningSummary = { + total_net_sales: string; + total_orders: string; + new_customer_sales: string; + returning_customer_sales: string; + date_start: string; + date_end: string; +}; + +type CustomersNewReturningItem = { + customer_type: 'new' | 'returning'; + net_sales: string; + orders_count: string; +}; + +type ReportsCustomersNewReturningResponse = { + summary: CustomersNewReturningSummary; + data: CustomersNewReturningItem[]; +}; + +export type RequestReportCustomersParams = Omit< + BaseReportParams, + 'interval' +> & { + filters?: FilterCondition[]; +}; + +export async function fetchReportCustomers( { + from, + to, + filters, + date_type, +}: RequestReportCustomersParams ): Promise< ReportsCustomersNewReturningResponse > { + const path = addQueryArgs( `${ reportsPath }/customers/new-returning`, { + from, + to, + filters, + date_type, + } ); + + return apiFetch( { + path, + } ) as Promise< ReportsCustomersNewReturningResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts new file mode 100644 index 000000000000..c59fd9485cc6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts @@ -0,0 +1,5 @@ +export { exportReport } from './report-export-fetch'; +export type { + ExportReportParams, + ExportReportResponse, +} from './report-export-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts new file mode 100644 index 000000000000..5edc5de7f4c7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Export request parameters + */ +export interface ExportReportParams { + reportType: string | string[]; + from: string; // ISO 8601 date string + to: string; // ISO 8601 date string + interval?: string; + compareFrom?: string; // ISO 8601 date string + compareTo?: string; // ISO 8601 date string +} + +/** + * Export response from the API + */ +export interface ExportReportResponse { + success: boolean; + message: string; + job_ids?: Record< string, number >; // Multiple report exports + partial?: boolean; // Indicates if some exports failed + errors?: Record< string, string >; // Failed report types and their error messages +} + +/** + * Export one or more reports via email + * + * @param params Export parameters + * @return Promise that resolves to the export response + */ +export async function exportReport( + params: ExportReportParams +): Promise< ExportReportResponse > { + const path = '/wc/v3/woocommerce-analytics/reports/csv-export'; + + const body = { + report_type: Array.isArray( params.reportType ) + ? params.reportType + : [ params.reportType ], + from: params.from, + to: params.to, + interval: params.interval || 'day', + delivery_method: 'email', + ...( params.compareFrom && params.compareTo + ? { + compare_from: params.compareFrom, + compare_to: params.compareTo, + } + : {} ), + }; + + return apiFetch( { + path, + method: 'POST', + data: body, + } ) as Promise< ExportReportResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts new file mode 100644 index 000000000000..2039d9b82507 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts @@ -0,0 +1 @@ +export * from './report-order-attribution-by-product-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts new file mode 100644 index 000000000000..5e2489eb23fa --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import type { ORDER_ATTRIBUTION_VIEWS } from '../report-order-attribution-summary-fetch/report-order-attribution-summary-fetch'; + +type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ]; + +type OrderAttributionByProductInterval = { + time_interval: string; + date_start: string; + date_end: string; + net_sales: string; +}; + +type OrderAttributionByProductItem = { + item: string; + value: string; + intervals: OrderAttributionByProductInterval[]; +}; + +export type OrderAttributionByProductResponse = { + view: OrderAttributionView; + order_by: string; + data: OrderAttributionByProductItem[]; +}; + +export type RequestReportOrderAttributionByProductParams = BaseReportParams & { + view: OrderAttributionView; + filters?: FilterCondition[]; +}; + +/** + * Fetches order attribution by product data from the WC Analytics REST API + * + * This endpoint supports product filtering similar to fetchReportOrdersByProductType. + * Unlike the regular order-attribution endpoint, this one: + * - Does not support compare_from/compare_to parameters + * - Returns data in a flatter structure (no current_period/previous_period nesting) + * - Requires separate requests for comparison data + * + * @param params - Query parameters + * @return Promise resolving to order attribution by product response + */ +export async function fetchReportOrderAttributionByProduct( + params: RequestReportOrderAttributionByProductParams +): Promise< OrderAttributionByProductResponse > { + const { from, to, interval, view, filters, date_type } = params; + + const queryParams: Record< string, any > = { + from, + to, + interval, + view, + date_type, + }; + + // Add filters to query params if provided + if ( filters && filters.length > 0 ) { + queryParams.filters = filters; + } + + const path = addQueryArgs( + `${ reportsPath }/order-attribution-by-product/${ view }/summary`, + queryParams + ); + + return apiFetch< OrderAttributionByProductResponse >( { path } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts new file mode 100644 index 000000000000..285b15296160 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts @@ -0,0 +1,6 @@ +export { + fetchReportOrderAttributionSummary, + ORDER_ATTRIBUTION_VIEWS, + type RequestReportOrderAttributionSummaryParams, + type OrderAttributionSummaryResponse, +} from './report-order-attribution-summary-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts new file mode 100644 index 000000000000..1ee8349c738f --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +export const ORDER_ATTRIBUTION_VIEWS = [ + 'channel', + 'source', + 'campaign', + 'device', + 'channel-source', +] as const; + +type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ]; + +type OrderAttributionInterval = { + time_interval: string; + date_start: string; + date_end: string; + net_sales: string; +}; + +type OrderAttributionPeriod = { + value: string; + intervals: OrderAttributionInterval[]; +}; + +type OrderAttributionSummaryItem = { + item: string; + current_period: OrderAttributionPeriod; + previous_period: OrderAttributionPeriod; +}; + +export type OrderAttributionSummaryResponse = { + view: OrderAttributionView; + order_by: string; + data: OrderAttributionSummaryItem[]; +}; + +export type RequestReportOrderAttributionSummaryParams = BaseReportParams & { + view: OrderAttributionView; + compare_from: string; + compare_to: string; +}; + +/** + * Fetches order attribution summary data from the WC Analytics REST API + * + * Note: Order attribution summary endpoint returns both primary and comparison + * data in a single response, unlike orders endpoint which requires + * separate requests. The endpoint requires compare_from and compare_to parameters; + * when no comparison is needed, it uses the same values as the primary range. + * + * @param params - Query parameters + * @return Promise resolving to order attribution summary response + */ +export async function fetchReportOrderAttributionSummary( + params: RequestReportOrderAttributionSummaryParams +): Promise< OrderAttributionSummaryResponse > { + const { from, to, interval, view, compare_from, compare_to, date_type } = + params; + + /* + * Order attribution endpoint requires compare_from and compare_to. + * When no comparison is needed, use the same values as primary range. + */ + const queryParams: Record< string, string | undefined > = { + from, + to, + interval, + view, + compare_from, + compare_to, + date_type, + }; + + const path = addQueryArgs( + `${ reportsPath }/order-attribution/${ view }/summary`, + queryParams + ); + + return apiFetch< OrderAttributionSummaryResponse >( { path } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts new file mode 100644 index 000000000000..2762252eb226 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts @@ -0,0 +1,5 @@ +export { + fetchReportOrders, + type RequestReportOrdersParams, + type ReportsOrdersByDateResponse, +} from './report-orders-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts new file mode 100644 index 000000000000..452fd2b53371 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import { hasProductFilters } from '../../utils/product-filters'; + +type ReportsOrdersByDateSummary = { + average_order_value: string; + avg_items: string; + cogs_amount: string; + coupons: string; + date_end: string; + date_start: string; + orders_no: string; + orders_value_gross: string; + orders_value_net: string; + paid_orders_count: string; + paid_net_sales: string; + product_net_revenue: string; + profit_margin: string; + refunds: string; + total_sales: string; + unpaid_orders_count: string; + unpaid_net_sales: string; +}; + +type OrdersReportDataItem = ReportsOrdersByDateSummary & { + time_interval?: string; +}; + +export type ReportsOrdersByDateResponse = { + data: OrdersReportDataItem[]; + summary: ReportsOrdersByDateSummary; +}; + +export type RequestReportOrdersParams = BaseReportParams & { + filters?: FilterCondition[]; +}; + +export async function fetchReportOrders( { + from, + to, + interval, + filters, + date_type, +}: RequestReportOrdersParams ): Promise< ReportsOrdersByDateResponse > { + const hasProductFiltersValue = hasProductFilters( filters ); + const apiUrl = hasProductFiltersValue + ? `${ reportsPath }/orders-by-product-type/by-date` + : `${ reportsPath }/orders/by-date`; + + const path = addQueryArgs( apiUrl, { + from, + to, + interval, + filters, + date_type, + } ); + + return apiFetch( { path } ) as Promise< ReportsOrdersByDateResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts new file mode 100644 index 000000000000..d1d82b303a52 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +export { + fetchReportProducts, + type RequestReportProductsParams, +} from './report-products-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts new file mode 100644 index 000000000000..30b14661cde2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +import type { FilterCondition } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { reportsPath } from '../constants'; +import { BaseReportParams } from '../../utils/types'; + +export type RequestReportProductsParams = Omit< + BaseReportParams, + 'interval' +> & { + limit?: number; + orderby?: string; + order?: 'asc' | 'desc'; + filters?: FilterCondition[]; +}; + +type ReportProductsResponse = { + data: { + product_id: string; + product_name: string; + product_net_revenue: string; + product_gross_revenue: string; + product_type: string; + orders_count: string; + sku: string; + total_quantity: string; + stock_status: string; + }[]; + summary: { + total_orders: string; + total_products: string; + total_quantity: string; + total_revenue: string; + }; +}; + +/** + * Fetches products report data from the WooCommerce Analytics API + */ +export async function fetchReportProducts( + params: RequestReportProductsParams +): Promise< ReportProductsResponse > { + const queryArgs: Record< string, any > = { + from: params.from, + to: params.to, + date_type: params.date_type, + }; + + if ( params.limit ) { + queryArgs.limit = params.limit; + } + + if ( params.orderby ) { + queryArgs.orderby = params.orderby; + } + + if ( params.order ) { + queryArgs.order = params.order; + } + + // Add filters to query params if provided + if ( params.filters && params.filters.length > 0 ) { + queryArgs.filters = params.filters; + } + + return apiFetch< ReportProductsResponse >( { + path: addQueryArgs( `${ reportsPath }/products`, queryArgs ), + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts new file mode 100644 index 000000000000..2da42f37b7e6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportSessionsByDevice, + type RequestReportSessionsByDeviceParams, +} from './report-sessions-by-device-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts new file mode 100644 index 000000000000..4c28378487a5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +/** + * Raw response item from the sessions/by-device endpoint. + */ +type SessionsByDeviceItem = { + device_type: string; + active_sessions: string; +}; + +/** + * Summary data from the sessions/by-device endpoint. + */ +type SessionsByDeviceSummary = { + active_sessions: string; + total_orders: string; + date_start: string; + date_end: string; +}; + +/** + * Raw response structure from the sessions/by-device endpoint. + */ +type ReportsSessionsByDeviceResponse = { + summary: SessionsByDeviceSummary; + data: SessionsByDeviceItem[]; +}; + +export type RequestReportSessionsByDeviceParams = Omit< + BaseReportParams, + 'interval' +>; + +/** + * Fetch sessions by device type report data. + * + * This endpoint returns a breakdown of sessions by device category + * (Mobile, Desktop, Tablet) for the specified date range. + * + * @param params - Request parameters + * @param params.from - Start date in YYYY-MM-DD format + * @param params.to - End date in YYYY-MM-DD format + */ +export async function fetchReportSessionsByDevice( { + from, + to, +}: RequestReportSessionsByDeviceParams ): Promise< ReportsSessionsByDeviceResponse > { + const path = addQueryArgs( `${ reportsPath }/sessions/by-device`, { + from, + to, + } ); + + return apiFetch( { path } ) as Promise< ReportsSessionsByDeviceResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts new file mode 100644 index 000000000000..18115cbe9222 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportVisitorsByLocation, + type RequestReportVisitorsByLocationParams, +} from './report-visitors-by-location-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts new file mode 100644 index 000000000000..43b5f05a1d22 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +type VisitorsByLocationReportDataItem = { + country_code: string; + label: string; + region?: string; + visitors: string; +}; + +type ReportsVisitorsByLocationSummary = { + visitors: string; + date_start: string; + date_end: string; +}; + +type ReportsVisitorsByLocationResponse = { + data: VisitorsByLocationReportDataItem[]; + summary?: ReportsVisitorsByLocationSummary; +}; + +export type RequestReportVisitorsByLocationParams = BaseReportParams & { + group_by: 'country' | 'region'; + country_code?: string; + limit?: number; +}; + +/** + * Fetch visitors grouped by location (country or region) for the selected period. + * + * This endpoint is proxied through `/wc/v3/woocommerce-analytics/proxy/reports/...` + * and ultimately served by wpcom analytics. + */ +export async function fetchReportVisitorsByLocation( { + from, + to, + interval, + group_by, + country_code, + limit, +}: RequestReportVisitorsByLocationParams ): Promise< ReportsVisitorsByLocationResponse > { + const path = addQueryArgs( `${ reportsPath }/sessions/by-location`, { + from, + to, + interval, + group_by, + country_code, + limit, + } ); + + return apiFetch( { path } ) as Promise< ReportsVisitorsByLocationResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts new file mode 100644 index 000000000000..3a17020aae0c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts @@ -0,0 +1,4 @@ +export { + fetchReportVisitors, + type RequestReportVisitorsParams, +} from './report-visitors-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts new file mode 100644 index 000000000000..dcf58139007c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +type ReportsVisitorsByDateSummary = { + active_sessions: string; + date_end: string; + date_start: string; + visitors: string; +}; + +type VisitorsReportDataItem = ReportsVisitorsByDateSummary & { + time_interval: string; +}; + +type ReportsVisitorsByDateResponse = { + data: VisitorsReportDataItem[]; + summary: ReportsVisitorsByDateSummary; +}; + +export type RequestReportVisitorsParams = BaseReportParams; + +export async function fetchReportVisitors( { + from, + to, + interval, +}: RequestReportVisitorsParams ): Promise< ReportsVisitorsByDateResponse > { + const path = addQueryArgs( `${ reportsPath }/sessions/by-date`, { + from, + to, + interval, + } ); + + return apiFetch( { path } ) as Promise< ReportsVisitorsByDateResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts new file mode 100644 index 000000000000..5c39f60b76ce --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts @@ -0,0 +1,107 @@ +/** + * Mock WordPress dependencies so date.ts can load. The select mock + * returns site settings with timezone: 'UTC' so getSiteTimezone() + * returns UTC, making localTZDate create UTC-aware TZDates. + */ +jest.mock( '@wordpress/core-data', () => ( { + store: 'core', +} ) ); + +jest.mock( '@wordpress/data', () => ( { + select: jest.fn( () => ( { + getEntityRecord: jest.fn( () => ( { timezone: 'UTC' } ) ), + } ) ), +} ) ); + +jest.mock( '../../utils/ensure-core-settings', () => ( { + ensureCoreSettingsReady: jest.fn( () => Promise.resolve() ), +} ) ); + +/** + * Internal dependencies + */ +import { getDefaultPreset, getDefaultQueryParams } from '../reports'; + +describe( 'getDefaultQueryParams - preset override', () => { + beforeEach( () => { + jest.useFakeTimers(); + jest.setSystemTime( new Date( '2025-03-15T12:00:00.000Z' ) ); + } ); + + afterEach( () => { + jest.useRealTimers(); + } ); + + it( 'defaults to last-30-days when no preset is given', () => { + expect( getDefaultQueryParams().preset ).toBe( 'last-30-days' ); + } ); + + it( 'uses today preset when passed', () => { + expect( getDefaultQueryParams( false, 'today' ).preset ).toBe( + 'today' + ); + } ); + + it( 'uses last-7-days preset when passed', () => { + expect( getDefaultQueryParams( false, 'last-7-days' ).preset ).toBe( + 'last-7-days' + ); + } ); + + it( 'uses last-30-days preset when passed', () => { + expect( getDefaultQueryParams( false, 'last-30-days' ).preset ).toBe( + 'last-30-days' + ); + } ); +} ); + +describe( 'getDefaultPreset', () => { + beforeEach( () => { + jest.useFakeTimers(); + jest.setSystemTime( new Date( '2025-03-15T12:00:00.000Z' ) ); + } ); + + afterEach( () => { + jest.useRealTimers(); + } ); + + it( 'returns last-30-days when no launched date', () => { + expect( getDefaultPreset() ).toBe( 'last-30-days' ); + } ); + + it( 'returns last-30-days for undefined', () => { + expect( getDefaultPreset( undefined ) ).toBe( 'last-30-days' ); + } ); + + it( 'returns today when store launched today', () => { + expect( getDefaultPreset( '2025-03-15T00:00:00Z' ) ).toBe( 'today' ); + } ); + + it( 'returns last-7-days when launched 3 days ago', () => { + expect( getDefaultPreset( '2025-03-12T00:00:00Z' ) ).toBe( + 'last-7-days' + ); + } ); + + it( 'returns last-7-days when launched exactly 7 days ago', () => { + expect( getDefaultPreset( '2025-03-08T00:00:00Z' ) ).toBe( + 'last-7-days' + ); + } ); + + it( 'returns last-30-days when launched 8 days ago', () => { + expect( getDefaultPreset( '2025-03-07T00:00:00Z' ) ).toBe( + 'last-30-days' + ); + } ); + + it( 'returns last-30-days when launched months ago', () => { + expect( getDefaultPreset( '2024-01-01T00:00:00Z' ) ).toBe( + 'last-30-days' + ); + } ); + + it( 'returns today when launched in the future', () => { + expect( getDefaultPreset( '2025-04-01T00:00:00Z' ) ).toBe( 'today' ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/index.ts b/projects/packages/premium-analytics/packages/data/src/defaults/index.ts new file mode 100644 index 000000000000..1231063dab14 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/defaults/index.ts @@ -0,0 +1 @@ +export { getDefaultPreset, getDefaultQueryParams } from './reports'; diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts new file mode 100644 index 000000000000..12ad30a3b7a6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import { getComparisonRangeFromPreset } from '@next-woo-analytics/datetime'; +import { differenceInCalendarDays, startOfDay } from 'date-fns'; + +/** + * Internal dependencies + */ +import { + localTZDate, + dateToISOStringWithLocalTZ, + getDefaultIntervalForPeriod, + computeDateRangeFromPreset, + type PresetType, + type ReportParams, +} from '../utils'; + +const DEFAULT_PRESET: PresetType = 'last-30-days'; + +/** + * Pick the default date-range preset based on how long + * the store has been live. + * + * - Not launched / unknown → last-30-days (safe default) + * - Launched today → today + * - Launched ≤ 7 days ago → last-7-days + * - Launched > 7 days ago → last-30-days + */ +export function getDefaultPreset( launchedDate?: string ): PresetType { + if ( ! launchedDate ) { + return DEFAULT_PRESET; + } + + const today = startOfDay( localTZDate() ); + const launched = startOfDay( localTZDate( launchedDate ) ); + const daysSinceLaunch = differenceInCalendarDays( today, launched ); + + if ( daysSinceLaunch <= 0 ) { + return 'today'; + } + + if ( daysSinceLaunch <= 7 ) { + return 'last-7-days'; + } + + return DEFAULT_PRESET; +} + +/** + * Build report query parameters (from, to, interval, preset) + * for the given date-range preset. Defaults to `last-30-days`. + * + * Callers that need a dynamic default (e.g. based on store + * age) should resolve the preset externally and pass it in. + */ +export const getDefaultQueryParams = ( + /** + * Include previous-period comparison range. + */ + withComparison: boolean = false, + + /** + * Date-range preset. Defaults to `last-30-days`. + */ + preset: PresetType = DEFAULT_PRESET +): ReportParams => { + const range = computeDateRangeFromPreset( preset ); + + if ( ! range ) { + throw new Error( `Unknown preset: ${ preset }` ); + } + + const { from: fromString, to: toString } = range; + + const interval = getDefaultIntervalForPeriod( + undefined, + fromString, + toString + ); + + if ( ! withComparison ) { + return { + from: fromString, + to: toString, + preset, + interval, + }; + } + + const from = localTZDate( new Date( fromString ) ); + const to = localTZDate( new Date( toString ) ); + + const comparisonParams = getComparisonRangeFromPreset( + { + from, + to, + }, + 'previous-period' + ); + + return { + from: fromString, + to: toString, + preset, + interval, + compare_from: comparisonParams?.from + ? dateToISOStringWithLocalTZ( comparisonParams?.from ) + : undefined, + compare_to: comparisonParams?.to + ? dateToISOStringWithLocalTZ( comparisonParams?.to ) + : undefined, + compare_preset: 'previous-period', + comp: '1', + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/index.ts b/projects/packages/premium-analytics/packages/data/src/hooks/index.ts new file mode 100644 index 000000000000..ceb26db69eed --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/index.ts @@ -0,0 +1,12 @@ +export { useReportOrders } from './use-report-orders'; +export { useReportOrderAttribution } from './use-report-order-attribution'; +export { useReportCoupons } from './use-report-coupons'; +export { useReportCouponsByDate } from './use-report-coupons-by-date'; +export { useReportCustomers } from './use-report-customers'; +export { useReportConversionRate } from './use-report-conversion-rate'; +export { useReportBookings } from './use-report-bookings'; + +/** + * @deprecated Use individual hooks instead: useReportOrders, useReportOrderAttribution, useReportCoupons + */ +export { useReport } from './use-report'; diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts new file mode 100644 index 000000000000..5c5b1649f419 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import { useQuery } from '@tanstack/react-query'; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { sanitizeReportProductsResponse } from '../processing/products'; +import type { ProductImage } from '../types/product-image'; + +// Infer the product ID type from the sanitized products response +type ProductId = ReturnType< + typeof sanitizeReportProductsResponse +>[ 'data' ][ number ][ 'product_id' ]; + +export interface UseProductImagesParams { + productIds: ProductId[]; +} + +export interface ProductImageResponse { + id: number; + name: string; + images: { + id: number; + src: string; + name: string; + alt: string; + }[]; +} + +/** + * Fetches product images from the WooCommerce REST API + * @param productIds + */ +async function fetchProductImages( + productIds: ProductId[] +): Promise< ( ProductImage & { productId: ProductId } )[] > { + if ( ! productIds.length ) { + return []; + } + + // Use the include parameter to get only the products we need + const queryArgs = { + include: productIds.join( ',' ), + per_page: productIds.length, + }; + + try { + const response = await apiFetch< ProductImageResponse[] >( { + path: addQueryArgs( '/wc/v3/products', queryArgs ), + } ); + + return response.map( ( product ) => ( { + productId: product.id, + imageUrl: product.images?.[ 0 ]?.src || '', + imageAlt: product.images?.[ 0 ]?.alt || product.name, + } ) ); + } catch { + return []; + } +} + +const getProductImagesQueryKey = ( params: UseProductImagesParams ) => + [ 'product-images', params.productIds.sort().join( ',' ) ] as const; + +/** + * Hook to fetch product images for a list of product IDs + * @param params - Object containing the list of product IDs to fetch images for + */ +export function useProductImages( params: UseProductImagesParams ) { + return useQuery( { + queryKey: getProductImagesQueryKey( params ), + queryFn: async () => { + const images = await fetchProductImages( params.productIds ); + return images.reduce( + ( + acc: Record< number, ProductImage >, + image: ProductImage & { productId: number } + ) => { + acc[ image.productId ] = { + imageUrl: image.imageUrl, + imageAlt: image.imageAlt, + }; + return acc; + }, + {} + ); + }, + enabled: params.productIds.length > 0, + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts new file mode 100644 index 000000000000..fc91af38a3fb --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { reportBookingsQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +export function useReportBookings( params: ReportParams ) { + return useReport( ( p ) => reportBookingsQuery( p ), params, { + disabledComparisonKey: [ + 'reports', + 'bookings', + 'by-date', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts new file mode 100644 index 000000000000..9bc2d545d60c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ +import { reportConversionRateQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportConversionRateOptions = { + enabled?: boolean; +}; + +export function useReportConversionRate( + params: ReportParams, + options?: UseReportConversionRateOptions +) { + return useReport( ( p ) => reportConversionRateQuery( p ), params, { + enabled: options?.enabled, + disabledComparisonKey: [ + 'reports', + 'conversion-rate', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts new file mode 100644 index 000000000000..6ba9daa58d92 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { reportCouponsByDateQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +export function useReportCouponsByDate( params: ReportParams ) { + return useReport( ( p ) => reportCouponsByDateQuery( p ), params, { + disabledComparisonKey: [ + 'reports', + 'couponsByDate', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts new file mode 100644 index 000000000000..2b2fb11a3617 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { reportCouponsQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +export function useReportCoupons( params: ReportParams ) { + return useReport( ( p ) => reportCouponsQuery( p ), params, { + disabledComparisonKey: [ + 'reports', + 'coupons', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts new file mode 100644 index 000000000000..1ddd8c538af0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { reportCustomersByDateQuery } from '../queries/report-customers-by-date-query'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportCustomersByDateOptions = { + enabled?: boolean; +}; + +export function useReportCustomersByDate( + params: ReportParams, + options?: UseReportCustomersByDateOptions +) { + return useReport( ( p ) => reportCustomersByDateQuery( p ), params, { + enabled: options?.enabled, + disabledComparisonKey: [ + 'reports', + 'customers', + 'by-date', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts new file mode 100644 index 000000000000..3f4a7a2aee6e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { reportCustomersQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +export function useReportCustomers( params: ReportParams ) { + return useReport( ( p ) => reportCustomersQuery( p ), params, { + disabledComparisonKey: [ + 'reports', + 'customers', + 'new-returning', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts new file mode 100644 index 000000000000..7bf18f3e341e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts @@ -0,0 +1,66 @@ +/** + * Internal dependencies + */ +import { reportOrderAttributionSummaryQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportOrderAttributionOptions = { + enabled?: boolean; +}; + +const DISABLED_COMPARISON_KEY = [ + 'reports', + 'order-attribution', + '__comparison__', + 'included-in-primary', +]; + +export function useReportOrderAttribution( + params: ReportParams, + options?: UseReportOrderAttributionOptions +) { + /* + * Compare from and to are required for order attribution summary query. + * When they aren't provided, use the same dates as the primary period. + */ + const compareFrom = params.compare_from ?? params.from; + const compareTo = params.compare_to ?? params.to; + + return useReport( + ( p, queryType ) => { + // Order attribution requires the view parameter + if ( ! params.view ) { + return { + queryKey: [ + 'reports', + 'order-attribution', + '__disabled__', + 'no-view-param', + ], + enabled: false, + }; + } + + if ( queryType === 'comparison' ) { + return { + queryKey: DISABLED_COMPARISON_KEY, + enabled: false, + }; + } + + return reportOrderAttributionSummaryQuery( { + ...p, + view: params.view, + compare_from: compareFrom, + compare_to: compareTo, + date_type: params.date_type, + } ); + }, + params, + { + enabled: options?.enabled, + disabledComparisonKey: DISABLED_COMPARISON_KEY, + } + ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts new file mode 100644 index 000000000000..336de035aede --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { reportOrdersQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportOrdersOptions = { + enabled?: boolean; +}; + +export function useReportOrders( + params: ReportParams, + options?: UseReportOrdersOptions +) { + return useReport( ( p ) => reportOrdersQuery( p ), params, { + enabled: options?.enabled, + disabledComparisonKey: [ + 'reports', + 'orders', + 'by-date', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts new file mode 100644 index 000000000000..bdd30704ffe8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { reportProductsQuery } from '../queries/report-products-query'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +export function useReportProducts( params: ReportParams, limit = 5 ) { + return useReport( ( p ) => reportProductsQuery( { ...p, limit } ), params, { + disabledComparisonKey: [ + 'reports', + 'products', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts new file mode 100644 index 000000000000..535980817370 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts @@ -0,0 +1,45 @@ +/** + * Internal dependencies + */ +import { reportSessionsByDeviceQuery } from '../queries/report-sessions-by-device-query'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportSessionsByDeviceOptions = { + enabled?: boolean; +}; + +/** + * Hook for fetching sessions by device type report data. + * + * Returns a breakdown of website sessions by device category + * (Mobile, Desktop, Tablet) for the specified date range. + * + * @param params - Report parameters including date range and comparison dates + * @param options - Optional configuration + * + * @example + * ```typescript + * const { primary, comparison, hasComparison, isLoading, hasData } = + * useReportSessionsByDevice( reportParams ); + * + * // Access the data + * const { summary, data } = primary.data; + * const totalSessions = summary.total_sessions; + * ``` + */ +export function useReportSessionsByDevice( + params: ReportParams, + options?: UseReportSessionsByDeviceOptions +) { + return useReport( ( p ) => reportSessionsByDeviceQuery( p ), params, { + enabled: options?.enabled, + disabledComparisonKey: [ + 'reports', + 'sessions', + 'by-device', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts new file mode 100644 index 000000000000..b4252eb16676 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import { reportVisitorsByLocationQuery } from '../queries/report-visitors-by-location-query'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportVisitorsByLocationOptions = { + enabled?: boolean; + groupBy?: 'country' | 'region'; + countryCode?: string; + limit?: number; +}; + +export function useReportVisitorsByLocation( + params: ReportParams, + options?: UseReportVisitorsByLocationOptions +) { + return useReport( + ( p ) => + reportVisitorsByLocationQuery( { + ...p, + group_by: options?.groupBy ?? 'country', + country_code: options?.countryCode, + limit: options?.limit, + } ), + params, + { + enabled: options?.enabled, + disabledComparisonKey: [ + 'reports', + 'visitors', + 'by-location', + '__comparison__', + 'disabled', + ], + } + ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts new file mode 100644 index 000000000000..7fd1805f2744 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { reportVisitorsQuery } from '../queries/report-visitors-query'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportVisitorsOptions = { + enabled?: boolean; +}; + +export function useReportVisitors( + params: ReportParams, + options?: UseReportVisitorsOptions +) { + return useReport( ( p ) => reportVisitorsQuery( p ), params, { + enabled: options?.enabled, + disabledComparisonKey: [ + 'reports', + 'visitors', + 'by-date', + '__comparison__', + 'disabled', + ], + } ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts new file mode 100644 index 000000000000..9d90ab78683c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +/** + * Internal dependencies + */ +import { hasComparisonEnabled, type ReportParams } from '../utils/search'; + +type UseReportOptions = { + enabled?: boolean; + disabledComparisonKey?: string[]; +}; + +type QueryFactory< TData > = ( + params: any, + queryType: 'primary' | 'comparison' +) => UseQueryOptions< TData >; + +/** + * Generic hook for fetching report data with comparison support. + * + * This hook handles the common pattern of fetching primary and comparison + * data for analytics reports. It automatically manages the comparison query + * based on the presence of comparison dates in the params. + * + * @template TData - The type of data returned by the query + * + * @param queryFactory - Function that creates a query options object from params + * @param params - Report parameters including dates, filters, and comparison dates + * @param options - Optional configuration + * @param options.enabled - Whether the queries should be enabled (default: true) + * @param options.disabledComparisonKey - Query key to use when comparison is disabled + * + * @return Object containing primary and comparison query results + * + * @example + * ```typescript + * const { primary, comparison, hasComparison, isLoading, hasData } = useReport( + * (params) => reportOrdersQuery(params, hasProductFilters), + * reportParams, + * { + * enabled: true, + * disabledComparisonKey: ['reports', 'orders', '__comparison__', 'disabled'], + * } + * ); + * ``` + */ +export function useReport< TData >( + queryFactory: QueryFactory< TData >, + params: ReportParams, + options?: UseReportOptions +) { + const queryEnabled = options?.enabled ?? true; + const comparisonEnabled = hasComparisonEnabled( params ); + + // Create primary query + const primaryQueryOptions = queryFactory( + { + from: params.from, + to: params.to, + interval: params.interval, + filters: params.filters, + date_type: params.date_type, + }, + 'primary' + ); + + // Create comparison query if comparison is enabled + const comparisonQueryOptions = comparisonEnabled + ? queryFactory( + { + from: params.compare_from, + to: params.compare_to, + interval: params.interval, + filters: params.filters, + date_type: params.date_type, + }, + 'comparison' + ) + : { + queryKey: options?.disabledComparisonKey ?? [ + 'reports', + '__comparison__', + 'disabled', + ], + }; + + const primary = useQuery( { + ...primaryQueryOptions, + enabled: queryEnabled && ( primaryQueryOptions.enabled ?? true ), + } ); + + const comparison = useQuery( { + ...comparisonQueryOptions, + enabled: + queryEnabled && + comparisonEnabled && + ( comparisonQueryOptions.enabled ?? true ), + } ); + + // Compute common derived states + const isLoading = primary.isLoading || comparison.isLoading; + const isFetching = primary.isFetching || comparison.isFetching; + + /** + * Check if data exists using standardized response fields. + * + * All sanitized report responses follow a consistent structure: + * - `summary`: Always present (aggregated metrics) + * - `data`: Array of time-series or items (orders, bookings, products, etc.) + * - `steps`: Array of funnel steps (conversion-rate only) + * + * We check multiple fields because different endpoints return different combinations: + * - Time-series reports (orders, bookings, visitors): { summary, data } + * - Conversion funnel: { summary, data, steps, overallRate } + * - List reports (products, coupons): { summary, data } + * + * The use of `as any` is intentional here to handle the generic TData type, + * since we cannot add constraints to the generic without breaking existing usage. + * + * Note: With placeholderData enabled in queries, this hasData check is sufficient + * to determine loading states: + * - If hasData is true: We have data to display (show with loading indicator if fetching) + * - If hasData is false: No data to display (show skeleton) + */ + const hasData = + Boolean( ( primary.data as any )?.summary ) || + Boolean( ( primary.data as any )?.data?.length ) || + Boolean( ( primary.data as any )?.steps?.length ) || + Boolean( ( comparison.data as any )?.summary ) || + Boolean( ( comparison.data as any )?.data?.length ) || + Boolean( ( comparison.data as any )?.steps?.length ); + + // Combined refetch function that refetches both queries. + // If both primary and comparison queries fail, clicking "Retry" should refetch both. + const primaryRefetch = primary.refetch; + const comparisonRefetch = comparison.refetch; + const refetch = useCallback( async () => { + await Promise.all( [ + primaryRefetch(), + comparisonEnabled ? comparisonRefetch() : Promise.resolve(), + ] ); + }, [ comparisonEnabled, primaryRefetch, comparisonRefetch ] ); + + return { + primary, + comparison, + hasComparison: comparisonEnabled, + isLoading, + isFetching, + hasData, + // Error handling + isError: primary.isError || comparison.isError, + error: primary.error ?? comparison.error, + refetch, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/index.ts b/projects/packages/premium-analytics/packages/data/src/index.ts new file mode 100644 index 000000000000..0d685aeb5de7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/index.ts @@ -0,0 +1,53 @@ +export { + AnalyticsQueryClientProvider, + queryClient, +} from './providers/query-client-provider'; +export { + GlobalErrorProvider, + useGlobalError, +} from './providers/global-error-context'; +export { + globalErrorManager, + type GlobalErrorType, +} from './providers/global-error-manager'; +export { useReportOrders } from './hooks/use-report-orders'; +export { useReportOrderAttribution } from './hooks/use-report-order-attribution'; +export { useReportCoupons } from './hooks/use-report-coupons'; +export { useReportCouponsByDate } from './hooks/use-report-coupons-by-date'; +export { useReportCustomers } from './hooks/use-report-customers'; +export { useReportCustomersByDate } from './hooks/use-report-customers-by-date'; +export { useReportConversionRate } from './hooks/use-report-conversion-rate'; +export { useReportProducts } from './hooks/use-report-products'; +export { useProductImages } from './hooks/use-product-images'; +export { useReportVisitors } from './hooks/use-report-visitors'; +export { useReportVisitorsByLocation } from './hooks/use-report-visitors-by-location'; +export { useReportBookings } from './hooks/use-report-bookings'; +export { useReportSessionsByDevice } from './hooks/use-report-sessions-by-device'; +export { prefetchReport } from './prefetch'; +export { + normalizeReportParams, + hasComparisonEnabled, + type PresetType, + type ReportParams, +} from './utils/search'; +export { + dateToISOStringWithLocalTZ, + ensureCoreSettingsReady, + getSiteTimezone, + getSiteGmtOffset, + localTZDate, + hasProductFilters, + isSelectablePreset, +} from './utils'; +export type { ReportDataMap } from './types'; +export type { ReportQueryParams } from './api'; +export type { FilterCondition } from './types/filter-condition'; +export type { ProductType } from './types/product-type'; +export { ORDER_ATTRIBUTION_VIEWS } from './api/report-order-attribution-summary-fetch'; +export { + getDefaultIntervalForPeriod, + getDateFormatFromInterval, +} from './utils/interval'; +export { getDefaultPreset, getDefaultQueryParams } from './defaults'; +export { exportReport } from './api'; +export type { ExportReportParams, ExportReportResponse } from './api'; diff --git a/projects/packages/premium-analytics/packages/data/src/prefetch/index.ts b/projects/packages/premium-analytics/packages/data/src/prefetch/index.ts new file mode 100644 index 000000000000..88eaef8a0f2a --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/prefetch/index.ts @@ -0,0 +1 @@ +export { prefetchReport } from './prefetch-report'; diff --git a/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts new file mode 100644 index 000000000000..b5f494b174f7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts @@ -0,0 +1,122 @@ +/** + * Internal dependencies + */ +import { queryClient } from '../providers'; +import { + reportOrdersQuery, + reportOrderAttributionSummaryQuery, + reportCouponsQuery, + reportCouponsByDateQuery, + reportCustomersQuery, + reportCustomersByDateQuery, + reportVisitorsQuery, + reportVisitorsByLocationQuery, + reportSessionsByDeviceQuery, + reportProductsQuery, + reportConversionRateQuery, +} from '../queries'; + +type RequestReportParamsMap = { + orders: Parameters< typeof reportOrdersQuery >[ 0 ]; + 'order-attribution': Parameters< + typeof reportOrderAttributionSummaryQuery + >[ 0 ]; + coupons: Parameters< typeof reportCouponsQuery >[ 0 ]; + 'coupons-by-date': Parameters< typeof reportCouponsByDateQuery >[ 0 ]; + customers: Parameters< typeof reportCustomersQuery >[ 0 ]; + 'customers-by-date': Parameters< typeof reportCustomersByDateQuery >[ 0 ]; + visitors: Parameters< typeof reportVisitorsQuery >[ 0 ]; + 'visitors-by-location': Parameters< + typeof reportVisitorsByLocationQuery + >[ 0 ]; + 'sessions-by-device': Parameters< typeof reportSessionsByDeviceQuery >[ 0 ]; + products: Parameters< typeof reportProductsQuery >[ 0 ]; + 'conversion-rate': Parameters< typeof reportConversionRateQuery >[ 0 ]; +}; + +export async function prefetchReport< T extends keyof RequestReportParamsMap >( + reportType: T = 'orders' as T, + params: RequestReportParamsMap[ T ] +) { + switch ( reportType ) { + case 'orders': + return queryClient.ensureQueryData( + reportOrdersQuery( + params as RequestReportParamsMap[ 'orders' ] + ) + ); + + case 'order-attribution': + return queryClient.ensureQueryData( + reportOrderAttributionSummaryQuery( + params as RequestReportParamsMap[ 'order-attribution' ] + ) + ); + + case 'coupons': + return queryClient.ensureQueryData( + reportCouponsQuery( + params as RequestReportParamsMap[ 'coupons' ] + ) + ); + + case 'coupons-by-date': + return queryClient.ensureQueryData( + reportCouponsByDateQuery( + params as RequestReportParamsMap[ 'coupons-by-date' ] + ) + ); + + case 'customers': + return queryClient.ensureQueryData( + reportCustomersQuery( + params as RequestReportParamsMap[ 'customers' ] + ) + ); + + case 'customers-by-date': + return queryClient.ensureQueryData( + reportCustomersByDateQuery( + params as RequestReportParamsMap[ 'customers-by-date' ] + ) + ); + + case 'visitors': + return queryClient.ensureQueryData( + reportVisitorsQuery( + params as RequestReportParamsMap[ 'visitors' ] + ) + ); + + case 'visitors-by-location': + return queryClient.ensureQueryData( + reportVisitorsByLocationQuery( + params as RequestReportParamsMap[ 'visitors-by-location' ] + ) + ); + + case 'sessions-by-device': + return queryClient.ensureQueryData( + reportSessionsByDeviceQuery( + params as RequestReportParamsMap[ 'sessions-by-device' ] + ) + ); + + case 'products': + return queryClient.ensureQueryData( + reportProductsQuery( + params as RequestReportParamsMap[ 'products' ] + ) + ); + + case 'conversion-rate': + return queryClient.ensureQueryData( + reportConversionRateQuery( + params as RequestReportParamsMap[ 'conversion-rate' ] + ) + ); + + default: + throw new Error( `Unsupported report type: ${ reportType }` ); + } +} diff --git a/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts new file mode 100644 index 000000000000..0d656d471de8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts @@ -0,0 +1,119 @@ +/** + * Internal dependencies + */ +import { fetchReportBookings } from '../../api/report-bookings-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseInt } from '../../utils/parsing'; + +type ReportsBookingsByDateResponse = Awaited< + ReturnType< typeof fetchReportBookings > +>; +type RawBookingsReportDataItem = + ReportsBookingsByDateResponse[ 'data' ][ number ]; +type RawBookingsReportSummaryItem = ReportsBookingsByDateResponse[ 'summary' ]; + +type SanitizedBookingsByDateItem = Override< + RawBookingsReportDataItem, + { + status_unpaid: number; + status_pending_confirmation: number; + status_confirmed: number; + status_paid: number; + status_cancelled: number; + status_complete: number; + attendance_status_booked: number; + attendance_status_no_show: number; + attendance_status_checked_in: number; + } +>; + +type SanitizedBookingsSummaryItem = Override< + RawBookingsReportSummaryItem, + { + status_unpaid: number; + status_pending_confirmation: number; + status_confirmed: number; + status_paid: number; + status_cancelled: number; + status_complete: number; + attendance_status_booked: number; + attendance_status_no_show: number; + attendance_status_checked_in: number; + } +>; + +/** + * Sanitize/process a single booking item by converting strings to numbers + */ +function sanitizeBookingItem( + item: RawBookingsReportDataItem +): SanitizedBookingsByDateItem { + return { + ...item, + status_unpaid: safeParseInt( item.status_unpaid ), + status_pending_confirmation: safeParseInt( + item.status_pending_confirmation + ), + status_confirmed: safeParseInt( item.status_confirmed ), + status_paid: safeParseInt( item.status_paid ), + status_cancelled: safeParseInt( item.status_cancelled ), + status_complete: safeParseInt( item.status_complete ), + attendance_status_booked: safeParseInt( item.attendance_status_booked ), + attendance_status_no_show: safeParseInt( + item.attendance_status_no_show + ), + attendance_status_checked_in: safeParseInt( + item.attendance_status_checked_in + ), + }; +} + +/** + * Sanitize/process a single booking summary item by converting strings to numbers + */ +function sanitizeBookingSummaryItem( + item: RawBookingsReportSummaryItem +): SanitizedBookingsSummaryItem { + return { + ...item, + status_unpaid: safeParseInt( item.status_unpaid ), + status_pending_confirmation: safeParseInt( + item.status_pending_confirmation + ), + status_confirmed: safeParseInt( item.status_confirmed ), + status_paid: safeParseInt( item.status_paid ), + status_cancelled: safeParseInt( item.status_cancelled ), + status_complete: safeParseInt( item.status_complete ), + attendance_status_booked: safeParseInt( item.attendance_status_booked ), + attendance_status_no_show: safeParseInt( + item.attendance_status_no_show + ), + attendance_status_checked_in: safeParseInt( + item.attendance_status_checked_in + ), + }; +} + +/** + * Processed response with numeric values + */ +type SanitizedBookingsByDateResponse = { + summary: SanitizedBookingsSummaryItem; + data: SanitizedBookingsByDateItem[]; +}; + +/** + * Sanitize the response from the reports/bookings/by-date endpoint + * Converts string values to numbers for easier calculations and charting. + * + * The `summary` and `data` items have different structures (summary lacks time_interval), + * so we use different sanitizer functions for each. + */ +export const sanitizeReportBookingsResponse = ( + response: ReportsBookingsByDateResponse +): SanitizedBookingsByDateResponse => { + return { + summary: sanitizeBookingSummaryItem( response.summary ), + data: response.data.map( sanitizeBookingItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts new file mode 100644 index 000000000000..8f715a627702 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { fetchReportConversionRate } from '../../api/report-conversion-rate-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseInt } from '../../utils/parsing'; + +type ReportsConversionRateByDateResponse = Awaited< + ReturnType< typeof fetchReportConversionRate > +>; +type RawConversionRateReportDataItem = + ReportsConversionRateByDateResponse[ 'data' ][ number ]; +type SanitizedConversionRateByDateItem = Override< + RawConversionRateReportDataItem, + { + active_sessions: number; + visitors: number; + with_cart_addition: number; + reached_checkout: number; + completed_checkout: number; + conversion_rate: number; // calculated field + } +>; + +/** + * Sanitize/process a single conversion rate item by converting strings to numbers + * and calculating the conversion rate + */ +function sanitizeConversionRateItem( + item: RawConversionRateReportDataItem +): SanitizedConversionRateByDateItem { + const activeSessionsNum = safeParseInt( item.active_sessions ); + const visitorsNum = safeParseInt( item.visitors ); + const withCartAdditionNum = safeParseInt( item.with_cart_addition ); + const reachedCheckoutNum = safeParseInt( item.reached_checkout ); + const completedCheckoutNum = safeParseInt( item.completed_checkout ); + + // Calculate conversion rate as decimal (e.g., 0.035 for 3.5%) + // This format works with formatMetricValue 'percentage' type + const conversionRate = + activeSessionsNum > 0 ? completedCheckoutNum / activeSessionsNum : 0; + + return { + ...item, + active_sessions: activeSessionsNum, + visitors: visitorsNum, + with_cart_addition: withCartAdditionNum, + reached_checkout: reachedCheckoutNum, + completed_checkout: completedCheckoutNum, + conversion_rate: conversionRate, + }; +} + +/** + * Funnel step for conversion rate visualization + */ +type FunnelStep = { + id: string; + label: string; + count: number; + rate: number; +}; + +/** + * Processed response with funnel steps and overall conversion rate + */ +type SanitizedConversionRateByDateResponse = { + summary: SanitizedConversionRateByDateItem; + data: SanitizedConversionRateByDateItem[]; + steps: FunnelStep[]; + overallRate: number; +}; + +/** + * Sanitize the response from the sessions/by-conversion-rate endpoint + * Converts string values to numbers for easier calculations and charting. + * + * The `summary` single item has basically the same structure + * as the `data` array items, so we can use the same mapper function for both. + */ +export const sanitizeReportConversionRateResponse = ( + response: ReportsConversionRateByDateResponse +): SanitizedConversionRateByDateResponse => { + // Handle cases where response might not have the expected structure + const defaultSummary = { + active_sessions: '0', + visitors: '0', + with_cart_addition: '0', + reached_checkout: '0', + completed_checkout: '0', + date_start: '', + date_end: '', + }; + + const sanitizedSummary = sanitizeConversionRateItem( + response?.summary || defaultSummary + ); + + // Create funnel steps from the summary data + const steps: FunnelStep[] = [ + { + id: 'sessions', + label: __( 'Sessions', 'woocommerce-analytics' ), + count: sanitizedSummary.active_sessions, + rate: 100, // Starting point + }, + { + id: 'cart-addition', + label: __( 'Cart', 'woocommerce-analytics' ), + count: sanitizedSummary.with_cart_addition, + rate: + sanitizedSummary.active_sessions > 0 + ? ( sanitizedSummary.with_cart_addition / + sanitizedSummary.active_sessions ) * + 100 + : 0, + }, + { + id: 'checkout', + label: __( 'Checkout', 'woocommerce-analytics' ), + count: sanitizedSummary.reached_checkout, + rate: + sanitizedSummary.active_sessions > 0 + ? ( sanitizedSummary.reached_checkout / + sanitizedSummary.active_sessions ) * + 100 + : 0, + }, + { + id: 'completed', + label: __( 'Purchase', 'woocommerce-analytics' ), + count: sanitizedSummary.completed_checkout, + rate: + sanitizedSummary.active_sessions > 0 + ? ( sanitizedSummary.completed_checkout / + sanitizedSummary.active_sessions ) * + 100 + : 0, + }, + ]; + + return { + summary: sanitizedSummary, + data: response?.data + ? response.data.map( sanitizeConversionRateItem ) + : [], + steps, + overallRate: sanitizedSummary.conversion_rate, + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts new file mode 100644 index 000000000000..53b5720d37a0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts @@ -0,0 +1,100 @@ +/** + * Internal dependencies + */ +import { fetchReportCouponsByDate } from '../../api/report-coupons-by-date-fetch'; +import type { Override } from '../../utils/types'; + +type ReportsCouponsByDateResponse = Awaited< + ReturnType< typeof fetchReportCouponsByDate > +>; +type RawSummary = ReportsCouponsByDateResponse[ 'summary' ]; +type RawDataItem = ReportsCouponsByDateResponse[ 'data' ][ number ]; + +/** + * Processed summary with numeric values. + */ +type SanitizedCouponsByDateSummary = Override< + RawSummary, + { + total_orders: number; + orders_with_coupon: number; + orders_without_coupon: number; + total_sales: number; + sales_with_coupon: number; + sales_without_coupon: number; + total_discount_amount: number; + net_sales_after_discount: number; + coupon_usage_percentage: number; + } +>; + +/** + * Processed data item with numeric values. + */ +type SanitizedCouponsByDateDataItem = Override< + RawDataItem, + { + total_orders: number; + orders_with_coupon: number; + orders_without_coupon: number; + total_sales: number; + sales_with_coupon: number; + sales_without_coupon: number; + total_discount_amount: number; + net_sales_after_discount: number; + coupon_usage_percentage: number; + } +>; + +/** + * Processed response with numeric values. + */ +type SanitizedCouponsByDateResponse = { + summary: SanitizedCouponsByDateSummary; + data: SanitizedCouponsByDateDataItem[]; +}; + +function sanitizeItem( item: RawDataItem ): SanitizedCouponsByDateDataItem { + return { + ...item, + total_orders: parseInt( item.total_orders, 10 ), + orders_with_coupon: parseInt( item.orders_with_coupon, 10 ), + orders_without_coupon: parseInt( item.orders_without_coupon, 10 ), + total_sales: parseFloat( item.total_sales ), + sales_with_coupon: parseFloat( item.sales_with_coupon ), + sales_without_coupon: parseFloat( item.sales_without_coupon ), + total_discount_amount: parseFloat( item.total_discount_amount ), + net_sales_after_discount: parseFloat( item.net_sales_after_discount ), + coupon_usage_percentage: parseFloat( item.coupon_usage_percentage ), + }; +} + +function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { + return { + ...summary, + total_orders: parseInt( summary.total_orders, 10 ), + orders_with_coupon: parseInt( summary.orders_with_coupon, 10 ), + orders_without_coupon: parseInt( summary.orders_without_coupon, 10 ), + total_sales: parseFloat( summary.total_sales ), + sales_with_coupon: parseFloat( summary.sales_with_coupon ), + sales_without_coupon: parseFloat( summary.sales_without_coupon ), + total_discount_amount: parseFloat( summary.total_discount_amount ), + net_sales_after_discount: parseFloat( + summary.net_sales_after_discount + ), + coupon_usage_percentage: parseFloat( summary.coupon_usage_percentage ), + }; +} + +/** + * Sanitize the response from the reports/coupons/by-date endpoint. + * Converts string values to numbers for calculations and charting. + */ +export const sanitizeReportCouponsByDateResponse = ( + response: ReportsCouponsByDateResponse +): SanitizedCouponsByDateResponse => { + return { + summary: sanitizeSummary( response.summary ), + data: response.data.map( sanitizeItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts new file mode 100644 index 000000000000..04c76205f2c5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import { fetchReportCoupons } from '../../api/report-coupons-fetch'; +import type { Override } from '../../utils/types'; + +type ReportsCouponsResponse = Awaited< + ReturnType< typeof fetchReportCoupons > +>; +type RawCouponsDataItem = ReportsCouponsResponse[ 'data' ][ number ]; +type RawCouponsDataSummary = ReportsCouponsResponse[ 'summary' ]; + +/** + * Processed data item (numbers for calculations) + */ +type SanitizedCouponsDataItem = Override< + RawCouponsDataItem, + { + discount_amount: number; + total_sales: number; + orders_count: number; + } +>; + +/** + * Processed summary (numbers for calculations) + */ +type SanitizedCouponsDataSummary = Override< + RawCouponsDataSummary, + { + total_sales: number; + total_discount_amount: number; + total_orders: number; + } +>; + +/** + * Processed response with numeric values + */ +type SanitizedCouponsResponse = { + summary: SanitizedCouponsDataSummary; + data: SanitizedCouponsDataItem[]; +}; + +/** + * Sanitize/process a single coupon item by converting strings to numbers + */ +function sanitizeCouponItem( + item: RawCouponsDataItem +): SanitizedCouponsDataItem { + return { + ...item, + discount_amount: parseFloat( item.discount_amount ), + total_sales: parseFloat( item.total_sales ), + orders_count: parseInt( item.orders_count, 10 ), + }; +} + +/** + * Sanitize/process summary by converting strings to numbers + */ +function sanitizeCouponSummary( + summary: RawCouponsDataSummary +): SanitizedCouponsDataSummary { + return { + ...summary, + total_sales: parseFloat( summary.total_sales ), + total_discount_amount: parseFloat( summary.total_discount_amount ), + total_orders: parseInt( summary.total_orders, 10 ), + }; +} + +/** + * Sanitize the response from the reports/coupons endpoint + * Converts string values to numbers for easier calculations and charting. + */ +export const sanitizeReportCouponsResponse = ( + response: ReportsCouponsResponse +): SanitizedCouponsResponse => { + return { + summary: sanitizeCouponSummary( response.summary ), + data: response.data.map( sanitizeCouponItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts new file mode 100644 index 000000000000..1c61302ae3c5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts @@ -0,0 +1,179 @@ +/** + * Internal dependencies + */ +import { fetchReportCustomersByDate } from '../../api/report-customers-by-date-fetch'; +import type { Override } from '../../utils/types'; + +type ReportsCustomersByDateResponse = Awaited< + ReturnType< typeof fetchReportCustomersByDate > +>; +type RawCustomersByDateSummary = ReportsCustomersByDateResponse[ 'summary' ]; +type RawCustomersByDateItem = + ReportsCustomersByDateResponse[ 'data' ][ number ]; + +/** + * Processed summary (numbers for calculations) + */ +type SanitizedCustomersByDateSummary = Override< + RawCustomersByDateSummary, + { + total_net_sales: number; + total_gross_sales: number; + total_discounts: number; + total_refunds: number; + total_orders: number; + total_average_order_value: number; + total_avg_items_per_order: number; + total_customers: number; + new_customers: number; + returning_customers: number; + new_customer_sales: number; + new_customer_gross_sales: number; + new_customer_discounts: number; + new_customer_refunds: number; + new_customer_orders: number; + new_customer_avg_order_value: number; + new_customer_avg_items_per_order: number; + returning_customer_sales: number; + returning_customer_gross_sales: number; + returning_customer_discounts: number; + returning_customer_refunds: number; + returning_customer_orders: number; + returning_customer_avg_order_value: number; + returning_customer_avg_items_per_order: number; + // Add computed field for compatibility + customers: number; + } +>; + +/** + * Processed item (numbers for calculations) + */ +type SanitizedCustomersByDateItem = Override< + RawCustomersByDateItem, + { + total_customers: number; + new_customers: number; + returning_customers: number; + orders_count: number; + new_customer_orders: number; + returning_customer_orders: number; + net_sales: number; + new_customer_net_sales: number; + returning_customer_net_sales: number; + // Add computed field for compatibility + customers: number; + } +>; + +/** + * Processed response with numeric values + */ +export type SanitizedCustomersByDateResponse = { + summary: SanitizedCustomersByDateSummary; + data: SanitizedCustomersByDateItem[]; +}; + +/** + * Sanitize/process a single customer item by converting strings to numbers + */ +function sanitizeCustomerByDateItem( + item: RawCustomersByDateItem +): SanitizedCustomersByDateItem { + const totalCustomers = parseInt( item.total_customers, 10 ); + return { + ...item, + total_customers: totalCustomers, + new_customers: parseInt( item.new_customers, 10 ), + returning_customers: parseInt( item.returning_customers, 10 ), + orders_count: parseInt( item.orders_count, 10 ), + new_customer_orders: parseInt( item.new_customer_orders, 10 ), + returning_customer_orders: parseInt( + item.returning_customer_orders, + 10 + ), + net_sales: parseFloat( item.net_sales ), + new_customer_net_sales: parseFloat( item.new_customer_net_sales ), + returning_customer_net_sales: parseFloat( + item.returning_customer_net_sales + ), + // Add alias for compatibility with chart builder + customers: totalCustomers, + }; +} + +/** + * Sanitize/process the summary by converting strings to numbers + */ +function sanitizeCustomerByDateSummary( + summary: RawCustomersByDateSummary +): SanitizedCustomersByDateSummary { + const totalCustomers = parseInt( summary.total_customers, 10 ); + return { + ...summary, + total_net_sales: parseFloat( summary.total_net_sales ), + total_gross_sales: parseFloat( summary.total_gross_sales ), + total_discounts: parseFloat( summary.total_discounts ), + total_refunds: parseFloat( summary.total_refunds ), + total_orders: parseInt( summary.total_orders, 10 ), + total_average_order_value: parseFloat( + summary.total_average_order_value + ), + total_avg_items_per_order: parseFloat( + summary.total_avg_items_per_order + ), + total_customers: totalCustomers, + new_customers: parseInt( summary.new_customers, 10 ), + returning_customers: parseInt( summary.returning_customers, 10 ), + new_customer_sales: parseFloat( summary.new_customer_sales ), + new_customer_gross_sales: parseFloat( + summary.new_customer_gross_sales + ), + new_customer_discounts: parseFloat( summary.new_customer_discounts ), + new_customer_refunds: parseFloat( summary.new_customer_refunds ), + new_customer_orders: parseInt( summary.new_customer_orders, 10 ), + new_customer_avg_order_value: parseFloat( + summary.new_customer_avg_order_value + ), + new_customer_avg_items_per_order: parseFloat( + summary.new_customer_avg_items_per_order + ), + returning_customer_sales: parseFloat( + summary.returning_customer_sales + ), + returning_customer_gross_sales: parseFloat( + summary.returning_customer_gross_sales + ), + returning_customer_discounts: parseFloat( + summary.returning_customer_discounts + ), + returning_customer_refunds: parseFloat( + summary.returning_customer_refunds + ), + returning_customer_orders: parseInt( + summary.returning_customer_orders, + 10 + ), + returning_customer_avg_order_value: parseFloat( + summary.returning_customer_avg_order_value + ), + returning_customer_avg_items_per_order: parseFloat( + summary.returning_customer_avg_items_per_order + ), + // Add alias for compatibility with chart builder + customers: totalCustomers, + }; +} + +/** + * Sanitize the response from the reports/customers/by-date endpoint + * Converts string values to numbers for easier calculations and charting. + */ +export const sanitizeReportCustomersByDateResponse = ( + response: ReportsCustomersByDateResponse +): SanitizedCustomersByDateResponse => { + return { + summary: sanitizeCustomerByDateSummary( response.summary ), + data: response.data.map( sanitizeCustomerByDateItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts new file mode 100644 index 000000000000..be0fffcf7844 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts @@ -0,0 +1,88 @@ +/** + * Internal dependencies + */ +import { fetchReportCustomers } from '../../api/report-customers-fetch'; +import type { Override } from '../../utils/types'; + +type ReportsCustomersNewReturningResponse = Awaited< + ReturnType< typeof fetchReportCustomers > +>; +type RawCustomersNewReturningSummary = + ReportsCustomersNewReturningResponse[ 'summary' ]; +type RawCustomersNewReturningItem = + ReportsCustomersNewReturningResponse[ 'data' ][ number ]; + +/** + * Processed summary (numbers for calculations) + */ +type SanitizedCustomersNewReturningSummary = Override< + RawCustomersNewReturningSummary, + { + total_net_sales: number; + total_orders: number; + new_customer_sales: number; + returning_customer_sales: number; + } +>; + +/** + * Processed item (numbers for calculations) + */ +type SanitizedCustomersNewReturningItem = Override< + RawCustomersNewReturningItem, + { + net_sales: number; + orders_count: number; + } +>; + +/** + * Processed response with numeric values + */ +type SanitizedCustomersNewReturningResponse = { + summary: SanitizedCustomersNewReturningSummary; + data: SanitizedCustomersNewReturningItem[]; +}; + +/** + * Sanitize/process a single customer item by converting strings to numbers + */ +function sanitizeCustomerItem( + item: RawCustomersNewReturningItem +): SanitizedCustomersNewReturningItem { + return { + ...item, + net_sales: parseFloat( item.net_sales ), + orders_count: parseInt( item.orders_count, 10 ), + }; +} + +/** + * Sanitize/process the summary by converting strings to numbers + */ +function sanitizeCustomerSummary( + summary: RawCustomersNewReturningSummary +): SanitizedCustomersNewReturningSummary { + return { + ...summary, + total_net_sales: parseFloat( summary.total_net_sales ), + total_orders: parseInt( summary.total_orders, 10 ), + new_customer_sales: parseFloat( summary.new_customer_sales ), + returning_customer_sales: parseFloat( + summary.returning_customer_sales + ), + }; +} + +/** + * Sanitize the response from the reports/customers/new-returning endpoint + * Converts string values to numbers for easier calculations and charting. + */ +export const sanitizeReportCustomersResponse = ( + response: ReportsCustomersNewReturningResponse +): SanitizedCustomersNewReturningResponse => { + return { + summary: sanitizeCustomerSummary( response.summary ), + data: response.data.map( sanitizeCustomerItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/index.ts new file mode 100644 index 000000000000..2e9bf6a848fa --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/index.ts @@ -0,0 +1,12 @@ +// Resource-specific processing +export * from './orders'; +export * from './customers'; +export * from './products'; +export * from './visitors'; +export * from './visitors-by-location'; + +// TODO: Add coupons processing functions +// export * from './coupons'; + +// TODO: Add order attribution processing functions +// export * from './order-attribution'; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts new file mode 100644 index 000000000000..c98abdb066b7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts @@ -0,0 +1,2 @@ +export * from './sanitize-order-attribution-summary-response'; +export * from './normalize-order-attribution-by-product-response'; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts new file mode 100644 index 000000000000..129d281b23a4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts @@ -0,0 +1,90 @@ +/** + * Internal dependencies + */ +import type { OrderAttributionByProductResponse } from '../../api/report-order-attribution-by-product-fetch'; +import type { OrderAttributionSummaryResponse } from '../../api/report-order-attribution-summary-fetch'; + +/** + * Normalizes the order-attribution-by-product API response to match the + * structure of the regular order-attribution API response. + * + * The new API has a flatter structure without current_period/previous_period nesting, + * so we need to transform it to match the expected format for widgets. + * + * @param currentResponse - Response from the current period request + * @param previousResponse - Optional response from the comparison period request + * @return Normalized response matching OrderAttributionSummaryResponse structure + */ +export function normalizeOrderAttributionByProductResponse( + currentResponse: OrderAttributionByProductResponse, + previousResponse?: OrderAttributionByProductResponse +): OrderAttributionSummaryResponse { + // Create a map for quick lookup of previous period data by item + const previousDataMap = new Map< + string, + ( typeof currentResponse.data )[ 0 ] + >(); + if ( previousResponse ) { + previousResponse.data.forEach( ( item ) => { + previousDataMap.set( item.item, item ); + } ); + } + + // Transform the flat structure to nested structure + const normalizedData = currentResponse.data.map( ( currentItem ) => { + const previousItem = previousDataMap.get( currentItem.item ); + + // If no previous response provided (no comparison), use current data for both periods + // This matches the behavior of the existing API when compare_from/to equal from/to + const previousValue = previousItem?.value || currentItem.value; + const previousIntervals = + previousItem?.intervals || currentItem.intervals; + + return { + item: currentItem.item, + current_period: { + value: currentItem.value, + intervals: currentItem.intervals, + }, + previous_period: { + value: previousValue, + intervals: previousIntervals, + }, + }; + } ); + + // Handle items that exist in previous period but not in current + // This ensures we don't lose data when an item had sales in the previous period but not current + if ( previousResponse ) { + previousResponse.data.forEach( ( previousItem ) => { + const existsInCurrent = currentResponse.data.some( + ( item ) => item.item === previousItem.item + ); + + if ( ! existsInCurrent ) { + normalizedData.push( { + item: previousItem.item, + current_period: { + value: '0', + intervals: previousItem.intervals.map( + ( interval ) => ( { + ...interval, + net_sales: '0', + } ) + ), + }, + previous_period: { + value: previousItem.value, + intervals: previousItem.intervals, + }, + } ); + } + } ); + } + + return { + view: currentResponse.view, + order_by: currentResponse.order_by, + data: normalizedData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts new file mode 100644 index 000000000000..7634d328fae4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts @@ -0,0 +1,115 @@ +/** + * Internal dependencies + */ +import { sanitizeStringNumber } from '../utils'; +import { fetchReportOrderAttributionSummary } from '../../api/report-order-attribution-summary-fetch'; + +type OrderAttributionSummaryResponse = Awaited< + ReturnType< typeof fetchReportOrderAttributionSummary > +>; + +type OrderAttributionView = OrderAttributionSummaryResponse[ 'view' ]; + +/** + * Internal types for processing + */ +type OrderAttributionInterval = { + time_interval: string; + date_start: string; + date_end: string; + net_sales: string; +}; + +type OrderAttributionPeriod = { + value: string; + intervals: OrderAttributionInterval[]; +}; + +type OrderAttributionSummaryItem = { + item: string; + current_period: OrderAttributionPeriod; + previous_period: OrderAttributionPeriod; +}; + +/** + * Processed (sanitized) response types + */ +type SanitizedOrderAttributionInterval = { + time_interval: string; + date_start: string; + date_end: string; + net_sales: number; +}; + +type SanitizedOrderAttributionPeriod = { + value: number; + intervals: SanitizedOrderAttributionInterval[]; +}; + +type SanitizedOrderAttributionSummaryItem = { + item: string; + current_period: SanitizedOrderAttributionPeriod; + previous_period: SanitizedOrderAttributionPeriod; +}; + +export type SanitizedOrderAttributionSummaryResponse = { + view: OrderAttributionView; + order_by: string; + data: SanitizedOrderAttributionSummaryItem[]; +}; + +/** + * Sanitizes a single interval by converting string net_sales to number + */ +function sanitizeOrderAttributionInterval( + interval: OrderAttributionInterval +): SanitizedOrderAttributionInterval { + return { + time_interval: interval.time_interval, + date_start: interval.date_start, + date_end: interval.date_end, + net_sales: sanitizeStringNumber( interval.net_sales ), + }; +} + +/** + * Sanitizes a period by converting value to number and intervals + */ +function sanitizeOrderAttributionPeriod( + period: OrderAttributionPeriod +): SanitizedOrderAttributionPeriod { + return { + value: sanitizeStringNumber( period.value ), + intervals: period.intervals.map( sanitizeOrderAttributionInterval ), + }; +} + +/** + * Sanitizes a single order attribution summary item + */ +function sanitizeOrderAttributionSummaryItem( + item: OrderAttributionSummaryItem +): SanitizedOrderAttributionSummaryItem { + return { + item: item.item, + current_period: sanitizeOrderAttributionPeriod( item.current_period ), + previous_period: sanitizeOrderAttributionPeriod( item.previous_period ), + }; +} + +/** + * Sanitizes the order attribution summary response by converting all string + * numbers to actual numbers + * + * @param response - Raw API response from /summary endpoint + * @return Sanitized response with numbers instead of strings + */ +export function sanitizeReportOrderAttributionSummaryResponse( + response: OrderAttributionSummaryResponse +): SanitizedOrderAttributionSummaryResponse { + return { + view: response.view, + order_by: response.order_by, + data: response.data.map( sanitizeOrderAttributionSummaryItem ), + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts new file mode 100644 index 000000000000..faf1f71af07e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts @@ -0,0 +1,82 @@ +/** + * Internal dependencies + */ +import type { + ReportsOrdersByDateResponse, + RequestReportOrdersParams, +} from '../../api/report-orders-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseFloat, safeParseInt } from '../../utils/parsing'; + +/** + * Re-export the request params type for backwards compatibility. + * The orders-by-product-type endpoint uses the same request/response + * types as the orders endpoint. + */ +export type { RequestReportOrdersParams as RequestReportOrdersByProductTypeParams }; + +type ReportsOrdersByProductTypeByDateResponse = ReportsOrdersByDateResponse; +type RawOrdersByProductTypeReportDataItem = + ReportsOrdersByProductTypeByDateResponse[ 'data' ][ number ]; +type SanitizedOrdersByProductTypeByDateItem = Override< + RawOrdersByProductTypeReportDataItem, + { + average_order_value: number; + avg_items: number; + cogs_amount: number; + coupons: number; + orders_no: number; + orders_value_gross: number; + orders_value_net: number; + product_net_revenue: number; + profit_margin: number; + refunds: number; + total_sales: number; + } +>; + +/** + * Sanitize/process a single orders by product type item by converting strings to numbers + */ +function sanitizeOrdersByProductTypeItem( + item: RawOrdersByProductTypeReportDataItem +): SanitizedOrdersByProductTypeByDateItem { + return { + ...item, + average_order_value: safeParseFloat( item.average_order_value ), + avg_items: safeParseFloat( item.avg_items ), + cogs_amount: safeParseFloat( item.cogs_amount ), + coupons: safeParseInt( item.coupons ), + orders_no: safeParseInt( item.orders_no ), + orders_value_gross: safeParseFloat( item.orders_value_gross ), + orders_value_net: safeParseFloat( item.orders_value_net ), + product_net_revenue: safeParseFloat( item.product_net_revenue ), + profit_margin: safeParseFloat( item.profit_margin ), + refunds: safeParseFloat( item.refunds ), + total_sales: safeParseFloat( item.total_sales ), + }; +} + +/** + * Processed response with numeric values + */ +type SanitizedOrdersByProductTypeByDateResponse = { + summary: SanitizedOrdersByProductTypeByDateItem; + data: SanitizedOrdersByProductTypeByDateItem[]; +}; + +/** + * Sanitize the response from the reports/orders-by-product-type/by-date endpoint + * Converts string values to numbers for easier calculations and charting. + * + * The `summary` single item has basically the same structure + * as the `data` array items, so we can use the same mapper function for both. + */ +export const sanitizeReportOrdersByProductTypeResponse = ( + response: ReportsOrdersByProductTypeByDateResponse +): SanitizedOrdersByProductTypeByDateResponse => { + return { + summary: sanitizeOrdersByProductTypeItem( response.summary ), + data: response.data.map( sanitizeOrdersByProductTypeItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts new file mode 100644 index 000000000000..32dc9661797b --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts @@ -0,0 +1,81 @@ +/** + * Internal dependencies + */ +import { fetchReportOrders } from '../../api/report-orders-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseFloat, safeParseInt } from '../../utils/parsing'; + +type ReportsOrdersByDateResponse = Awaited< + ReturnType< typeof fetchReportOrders > +>; +type RawOrdersReportDataItem = ReportsOrdersByDateResponse[ 'data' ][ number ]; +type SanitizedOrdersByDateItem = Override< + RawOrdersReportDataItem, + { + average_order_value: number; + avg_items: number; + cogs_amount: number; + coupons: number; + orders_no: number; + orders_value_gross: number; + orders_value_net: number; + paid_orders_count: number; + paid_net_sales: number; + product_net_revenue: number; + profit_margin: number; + refunds: number; + total_sales: number; + unpaid_orders_count: number; + unpaid_net_sales: number; + } +>; + +/** + * Sanitize/process a single order item by converting strings to numbers + */ +function sanitizeOrderItem( + item: RawOrdersReportDataItem +): SanitizedOrdersByDateItem { + return { + ...item, + average_order_value: safeParseFloat( item.average_order_value ), + avg_items: safeParseFloat( item.avg_items ), + cogs_amount: safeParseFloat( item.cogs_amount ), + coupons: safeParseInt( item.coupons ), + orders_no: safeParseInt( item.orders_no ), + orders_value_gross: safeParseFloat( item.orders_value_gross ), + orders_value_net: safeParseFloat( item.orders_value_net ), + paid_orders_count: safeParseInt( item.paid_orders_count ), + paid_net_sales: safeParseFloat( item.paid_net_sales ), + product_net_revenue: safeParseFloat( item.product_net_revenue ), + profit_margin: safeParseFloat( item.profit_margin ), + refunds: safeParseFloat( item.refunds ), + total_sales: safeParseFloat( item.total_sales ), + unpaid_orders_count: safeParseInt( item.unpaid_orders_count ), + unpaid_net_sales: safeParseFloat( item.unpaid_net_sales ), + }; +} + +/** + * Processed response with numeric values + */ +type SanitizedOrdersByDateResponse = { + summary: SanitizedOrdersByDateItem; + data: SanitizedOrdersByDateItem[]; +}; + +/** + * Sanitize the response from the reports/orders/by-date endpoint + * Converts string values to numbers for easier calculations and charting. + * + * The `summary` single item has basically the same structure + * as the `data` array items, so we can use the same mapper function for both. + */ +export const sanitizeReportOrdersResponse = ( + response: ReportsOrdersByDateResponse +): SanitizedOrdersByDateResponse => { + return { + summary: sanitizeOrderItem( response.summary ), + data: response.data.map( sanitizeOrderItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts new file mode 100644 index 000000000000..a3937da22a13 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts @@ -0,0 +1,83 @@ +/** + * Internal dependencies + */ +import { fetchReportProducts } from '../../api/report-products-fetch'; +import type { Override } from '../../utils/types'; + +type ReportProductsResponse = Awaited< + ReturnType< typeof fetchReportProducts > +>; + +type RawProductsReportDataItem = ReportProductsResponse[ 'data' ][ number ]; +type RawProductsReportSummary = ReportProductsResponse[ 'summary' ]; + +type SanitizedProductsItem = Override< + RawProductsReportDataItem, + { + product_id: number; + orders_count: number; + product_net_revenue: number; + total_quantity: number; + } +>; + +type SanitizedProductsSummary = Override< + RawProductsReportSummary, + { + total_orders: number; + total_products: number; + total_quantity: number; + total_revenue: number; + } +>; + +/** + * Sanitize/process a single product item by converting strings to numbers + */ +function sanitizeProductItem( + item: RawProductsReportDataItem +): SanitizedProductsItem { + return { + ...item, + product_id: parseInt( item.product_id, 10 ), + orders_count: parseInt( item.orders_count, 10 ), + product_net_revenue: parseFloat( item.product_net_revenue ), + total_quantity: parseInt( item.total_quantity, 10 ), + }; +} + +function sanitizeProductSummary( + summary: RawProductsReportSummary +): SanitizedProductsSummary { + return { + ...summary, + total_orders: parseInt( summary.total_orders, 10 ), + total_products: parseInt( summary.total_products, 10 ), + total_quantity: parseInt( summary.total_quantity, 10 ), + total_revenue: parseFloat( summary.total_revenue ), + }; +} + +/** + * Processed response with numeric values + */ +type SanitizedProductsResponse = { + summary: SanitizedProductsSummary; + data: SanitizedProductsItem[]; +}; + +/** + * Sanitize the response from the reports/products endpoint + * Converts string values to numbers for easier calculations and charting. + * + * The `summary` single item has basically the same structure + * as the `data` array items, so we can use the same mapper function for both. + */ +export const sanitizeReportProductsResponse = ( + response: ReportProductsResponse +): SanitizedProductsResponse => { + return { + summary: sanitizeProductSummary( response.summary ), + data: ( response.data || [] ).map( sanitizeProductItem ), + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts new file mode 100644 index 000000000000..f88fc25fc0c4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts @@ -0,0 +1,83 @@ +/** + * Internal dependencies + */ +import { fetchReportSessionsByDevice } from '../../api/report-sessions-by-device-fetch'; + +/** + * Inferred types from fetch response + */ +type ReportsSessionsByDeviceResponse = Awaited< + ReturnType< typeof fetchReportSessionsByDevice > +>; + +/** + * Raw item type from API response + */ +type SessionsByDeviceItem = + ReportsSessionsByDeviceResponse[ 'data' ][ number ]; + +/** + * Sanitized item with numeric values + */ +type SanitizedSessionsByDeviceItem = { + device_type: string; + active_sessions: number; +}; + +/** + * Summary with total sessions + */ +type SessionsByDeviceSummary = { + total_sessions: number; +}; + +/** + * Processed response structure + */ +type SanitizedSessionsByDeviceResponse = { + summary: SessionsByDeviceSummary; + data: SanitizedSessionsByDeviceItem[]; +}; + +/** + * Sanitize a single sessions by device item by converting strings to numbers. + * + * @param item - Raw item from API response + */ +function sanitizeSessionsByDeviceItem( + item: SessionsByDeviceItem +): SanitizedSessionsByDeviceItem { + return { + device_type: item.device_type || '', + active_sessions: parseInt( item.active_sessions, 10 ) || 0, + }; +} + +/** + * Sanitize the response from the sessions/by-device endpoint. + * + * Converts string values to numbers for easier calculations and charting. + * Also calculates total sessions summary. + * + * @param response - Raw API response with summary and items + */ +export const sanitizeReportSessionsByDeviceResponse = ( + response: ReportsSessionsByDeviceResponse +): SanitizedSessionsByDeviceResponse => { + const items = response?.data ?? []; + const data = items + .filter( ( item ) => item.device_type ) // Filter out empty device types + .map( sanitizeSessionsByDeviceItem ); + + const totalSessions = data.reduce( + ( acc, item ) => acc + item.active_sessions, + 0 + ); + + return { + summary: { + total_sessions: totalSessions, + }, + data, + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/utils.ts b/projects/packages/premium-analytics/packages/data/src/processing/utils.ts new file mode 100644 index 000000000000..c0dc5636992a --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/utils.ts @@ -0,0 +1,11 @@ +/** + * Converts a string number to an actual number, with fallback to 0 + * for invalid values. + * + * @param value - String number from API + * @return Parsed number or 0 if invalid + */ +export function sanitizeStringNumber( value: string ): number { + const parsed = parseFloat( value ); + return isNaN( parsed ) ? 0 : parsed; +} diff --git a/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts new file mode 100644 index 000000000000..509856885bc9 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts @@ -0,0 +1,77 @@ +/** + * Internal dependencies + */ +import { fetchReportVisitorsByLocation } from '../../api/report-visitors-by-location-fetch'; +import type { Override } from '../../utils/types'; + +/** + * Inferred types + */ +type ReportsVisitorsByLocationResponse = Awaited< + ReturnType< typeof fetchReportVisitorsByLocation > +>; +type RawVisitorsByLocationItem = + ReportsVisitorsByLocationResponse[ 'data' ][ number ]; +type RawVisitorsByLocationSummary = NonNullable< + ReportsVisitorsByLocationResponse[ 'summary' ] +>; + +type SanitizedVisitorsByLocationItem = Override< + RawVisitorsByLocationItem, + { + visitors: number; + } +>; + +type SanitizedVisitorsByLocationSummary = Override< + RawVisitorsByLocationSummary, + { + visitors: number; + } +>; + +function sanitizeVisitorsByLocationItem( + item: RawVisitorsByLocationItem +): SanitizedVisitorsByLocationItem { + const visitors = Number.parseInt( item.visitors, 10 ); + + return { + ...item, + visitors: Number.isNaN( visitors ) ? 0 : visitors, + }; +} + +function sanitizeVisitorsByLocationSummary( + summary: RawVisitorsByLocationSummary +): SanitizedVisitorsByLocationSummary { + const visitors = Number.parseInt( summary.visitors, 10 ); + + return { + ...summary, + visitors: Number.isNaN( visitors ) ? 0 : visitors, + }; +} + +type SanitizedVisitorsByLocationResponse = { + summary: SanitizedVisitorsByLocationSummary; + data: SanitizedVisitorsByLocationItem[]; +}; + +export const sanitizeReportVisitorsByLocationResponse = ( + response: ReportsVisitorsByLocationResponse +): SanitizedVisitorsByLocationResponse => { + const defaultSummary: RawVisitorsByLocationSummary = { + visitors: '0', + date_start: '', + date_end: '', + }; + + return { + summary: sanitizeVisitorsByLocationSummary( + response?.summary ?? defaultSummary + ), + data: response?.data + ? response.data.map( sanitizeVisitorsByLocationItem ) + : [], + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts new file mode 100644 index 000000000000..3a787c37cba6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts @@ -0,0 +1,83 @@ +/** + * Internal dependencies + */ +import { fetchReportVisitors } from '../../api/report-visitors-fetch'; +import type { Override } from '../../utils/types'; + +/** + * Inferred types + */ +type ReportsVisitorsByDateResponse = Awaited< + ReturnType< typeof fetchReportVisitors > +>; +type RawVisitorsReportDataItem = + ReportsVisitorsByDateResponse[ 'data' ][ number ]; +type RawVisitorsReportDataSummary = ReportsVisitorsByDateResponse[ 'summary' ]; + +type SanitizedVisitorsByDateItem = Override< + RawVisitorsReportDataItem, + { + active_sessions: number; + visitors: number; + time_interval?: string; + } +>; + +type SanitizedVisitorsByDateSummary = Override< + RawVisitorsReportDataSummary, + { + active_sessions: number; + visitors: number; + } +>; + +type SanitizeVisitorsItemArg = Override< + RawVisitorsReportDataItem, + { + time_interval?: string; + } +>; + +/** + * Sanitize/process a single visitors item by converting strings to numbers + */ +function sanitizeVisitorsItem( + item: SanitizeVisitorsItemArg +): SanitizedVisitorsByDateItem { + return { + ...item, + active_sessions: parseInt( item.active_sessions, 10 ), + visitors: parseInt( item.visitors, 10 ), + }; +} + +/** + * Processed response with numeric values + */ +type SanitizedVisitorsByDateResponse = { + summary: SanitizedVisitorsByDateSummary; + data: SanitizedVisitorsByDateItem[]; +}; + +/** + * Sanitize the response from the sessions/by-date endpoint + * Converts string values to numbers for easier calculations and charting. + * + * The `summary` single item has basically the same structure + * as the `data` array items, so we can use the same mapper function for both. + */ +export const sanitizeReportVisitorsResponse = ( + response: ReportsVisitorsByDateResponse +): SanitizedVisitorsByDateResponse => { + const defaultSummary = { + active_sessions: '0', + visitors: '0', + date_start: '', + date_end: '', + }; + + return { + summary: sanitizeVisitorsItem( response?.summary ?? defaultSummary ), + data: response?.data ? response.data.map( sanitizeVisitorsItem ) : [], + }; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx new file mode 100644 index 000000000000..1a03b0e0dd64 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx @@ -0,0 +1,114 @@ +/** + * External dependencies + */ +import { + createContext, + useContext, + useEffect, + useMemo, + useSyncExternalStore, + type ReactNode, +} from 'react'; +import { onlineManager } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { + globalErrorManager, + type GlobalErrorType, +} from './global-error-manager'; + +interface GlobalErrorContextValue { + globalError: GlobalErrorType; + setGlobalError: ( error: GlobalErrorType ) => void; + clearGlobalError: () => void; + isGlobalError: boolean; +} + +const GlobalErrorContext = createContext< GlobalErrorContextValue | null >( + null +); + +/** + * Connects React to the global error manager via useSyncExternalStore. + * Also subscribes to network status changes via onlineManager. + */ +export function GlobalErrorProvider( { children }: { children: ReactNode } ) { + const globalError = useSyncExternalStore( + globalErrorManager.subscribe, + globalErrorManager.getError, + globalErrorManager.getError + ); + + /** + * Subscribe to TanStack Query's onlineManager to detect network status. + * + * When offline, TanStack Query pauses queries (doesn't execute them), + * so QueryCache.onError never fires. We detect offline status here and + * properly clean up the subscription when the provider unmounts. + */ + useEffect( () => { + // Check initial online status on mount + if ( ! onlineManager.isOnline() ) { + globalErrorManager.setError( 'network' ); + } + + const unsubscribe = onlineManager.subscribe( ( isOnline ) => { + if ( ! isOnline ) { + globalErrorManager.setError( 'network' ); + } else if ( globalErrorManager.getError() === 'network' ) { + globalErrorManager.clearError(); + } + } ); + + return unsubscribe; + }, [] ); + + const contextValue = useMemo( + () => ( { + globalError, + setGlobalError: globalErrorManager.setError, + clearGlobalError: globalErrorManager.clearError, + isGlobalError: globalError !== null, + } ), + [ globalError ] + ); + + return ( + + { children } + + ); +} + +let hasWarnedAboutMissingProvider = false; + +const defaultContextValue: GlobalErrorContextValue = { + globalError: null, + setGlobalError: () => {}, + clearGlobalError: () => {}, + isGlobalError: false, +}; + +/** + * Access global error state. Returns defaults if used outside GlobalErrorProvider. + */ +export function useGlobalError(): GlobalErrorContextValue { + const context = useContext( GlobalErrorContext ); + + if ( context ) { + return context; + } + + if ( ! hasWarnedAboutMissingProvider ) { + hasWarnedAboutMissingProvider = true; + // eslint-disable-next-line no-console + console.warn( + 'useGlobalError was called outside of GlobalErrorProvider. ' + + 'Wrap your component tree with GlobalErrorProvider.' + ); + } + + return defaultContextValue; +} diff --git a/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts b/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts new file mode 100644 index 000000000000..5cc52b9b003a --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ + +export type GlobalErrorType = 'network' | 'auth' | 'server' | null; + +type Listener = () => void; + +/** + * Manages global error state outside of React, enabling error state to be set + * from onlineManager subscription and consumed via useSyncExternalStore. + */ +class GlobalErrorManager { + private error: GlobalErrorType = null; + private listeners = new Set< Listener >(); + + getError = (): GlobalErrorType => this.error; + + setError = ( error: GlobalErrorType ): void => { + if ( this.error === error ) { + return; + } + this.error = error; + this.listeners.forEach( ( listener ) => listener() ); + }; + + clearError = (): void => this.setError( null ); + + subscribe = ( listener: Listener ): ( () => void ) => { + this.listeners.add( listener ); + return () => this.listeners.delete( listener ); + }; +} + +export const globalErrorManager = new GlobalErrorManager(); diff --git a/projects/packages/premium-analytics/packages/data/src/providers/index.ts b/projects/packages/premium-analytics/packages/data/src/providers/index.ts new file mode 100644 index 000000000000..641d073e3b48 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/index.ts @@ -0,0 +1,11 @@ +export { + queryClient, + AnalyticsQueryClientProvider, +} from './query-client-provider'; + +export { GlobalErrorProvider, useGlobalError } from './global-error-context'; + +export { + globalErrorManager, + type GlobalErrorType, +} from './global-error-manager'; diff --git a/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx new file mode 100644 index 000000000000..273020c75201 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +import { + QueryClient, + QueryClientProvider, + QueryCache, +} from '@tanstack/react-query'; +import { useExperiments } from '@automattic/admin-toolkit'; +import { ReactNode, lazy, Suspense } from 'react'; + +/** + * Internal dependencies + */ +import { globalErrorManager } from './global-error-manager'; + +const DEFAULT_STALE_TIME = 5 * 60 * 1000; +const DEFAULT_GC_TIME = 10 * 60 * 1000; + +const ReactQueryDevtoolsProduction = lazy( () => + // eslint-disable-next-line import/no-extraneous-dependencies -- DevTools is intentionally in devDependencies, only loaded when enabled + import( '@tanstack/react-query-devtools/production' ).then( ( d ) => ( { + default: d.ReactQueryDevtools, + } ) ) +); + +/** + * Extract HTTP status code from various error formats. + * WordPress REST API errors may have different shapes. + */ +function getErrorStatus( error: unknown ): number | null { + if ( ! error || typeof error !== 'object' ) { + return null; + } + + const err = error as Record< string, unknown >; + + // Standard fetch Response error + if ( typeof err.status === 'number' ) { + return err.status; + } + + // WordPress REST API error format + if ( err.data && typeof err.data === 'object' ) { + const data = err.data as Record< string, unknown >; + if ( typeof data.status === 'number' ) { + return data.status; + } + } + + // Nested response object + if ( err.response && typeof err.response === 'object' ) { + const response = err.response as Record< string, unknown >; + if ( typeof response.status === 'number' ) { + return response.status; + } + } + + return null; +} + +/** + * QueryCache with global error detection for auth and server errors. + * + * Error codes handled: + * - 401: Authentication failure (session expired, invalid token) + * - 502: Bad gateway (proxy/load balancer can't reach upstream) + * - 503: Service unavailable (server overloaded or under maintenance) + * - 504: Gateway timeout (request took too long) + * + * This is QueryClient configuration (not a side effect subscription), so it's + * appropriate at module level. The globalErrorManager singleton is used here + * because QueryClient must be instantiated once (singleton pattern), but the + * error state is safely consumed via useSyncExternalStore in GlobalErrorProvider. + * + * Network errors are handled separately in GlobalErrorProvider via onlineManager. + */ +const queryCache = new QueryCache( { + onError: ( error ) => { + const currentError = globalErrorManager.getError(); + + // Don't override network error (highest priority) + if ( currentError === 'network' ) { + return; + } + + const status = getErrorStatus( error ); + + if ( status === 401 ) { + // Auth errors take precedence over server errors, but not network errors. + if ( currentError !== 'auth' ) { + globalErrorManager.setError( 'auth' ); + } + } else if ( status === 502 || status === 503 || status === 504 ) { + // Server errors: only set if no higher-priority error exists. + if ( currentError !== 'auth' && currentError !== 'server' ) { + globalErrorManager.setError( 'server' ); + } + } + }, + onSuccess: () => { + // Clear transient server errors once queries start succeeding again. + if ( globalErrorManager.getError() === 'server' ) { + globalErrorManager.clearError(); + } + }, +} ); + +export const queryClient = new QueryClient( { + queryCache, + defaultOptions: { + queries: { + /* + * Stale time is the time after which the data + * is considered stale and a new request is made. + * Stale time: 5 minutes + */ + staleTime: DEFAULT_STALE_TIME, + + /* + * GC time is the time after which the data is considered garbage + * collected and removed from the cache. + * GC time: 10 minutes + */ + gcTime: DEFAULT_GC_TIME, + + /** + * Noop fetcher to prevent react-query errors for empty queries in console. + */ + queryFn: () => Promise.resolve( undefined ), + }, + }, +} ); + +export const AnalyticsQueryClientProvider = ( { + children, +}: { + children: ReactNode; +} ) => { + const { enabledExperiments } = useExperiments(); + return ( + + <>{ children } + { enabledExperiments[ 'tanstack/query-dev-tool' ] && ( + + + + ) } + + ); +}; diff --git a/projects/packages/premium-analytics/packages/data/src/queries/index.ts b/projects/packages/premium-analytics/packages/data/src/queries/index.ts new file mode 100644 index 000000000000..f5b0604ab3f9 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/index.ts @@ -0,0 +1,12 @@ +export { reportOrdersQuery } from './report-orders-query'; +export { reportOrderAttributionSummaryQuery } from './report-order-attribution-summary-query'; +export { reportCouponsQuery } from './report-coupons-query'; +export { reportCouponsByDateQuery } from './report-coupons-by-date-query'; +export { reportCustomersQuery } from './report-customers-query'; +export { reportCustomersByDateQuery } from './report-customers-by-date-query'; +export { reportConversionRateQuery } from './report-conversion-rate-query'; +export { reportProductsQuery } from './report-products-query'; +export { reportVisitorsQuery } from './report-visitors-query'; +export { reportVisitorsByLocationQuery } from './report-visitors-by-location-query'; +export { reportSessionsByDeviceQuery } from './report-sessions-by-device-query'; +export { reportBookingsQuery } from './report-bookings-query'; diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts new file mode 100644 index 000000000000..f1a4cdf921c4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportBookings } from '../api'; +import { sanitizeReportBookingsResponse } from '../processing/bookings'; +import type { ReportDataMap } from '../types'; + +type RequestReportBookingsParams = Parameters< + typeof fetchReportBookings +>[ 0 ]; + +const getReportBookingsQueryKey = ( p: RequestReportBookingsParams ) => + [ + 'reports', + 'bookings', + 'by-date', + p.from, + p.to, + p.interval, + p.date_type, + p.filters, + ] as const; + +export function reportBookingsQuery( + params: RequestReportBookingsParams +): UseQueryOptions< ReportDataMap[ 'bookings' ] > { + return { + queryKey: getReportBookingsQueryKey( params ), + queryFn: async () => { + const response = await fetchReportBookings( params ); + return sanitizeReportBookingsResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts new file mode 100644 index 000000000000..5133245335d3 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportConversionRate } from '../api/report-conversion-rate-fetch'; +import { sanitizeReportConversionRateResponse } from '../processing/conversion-rate'; +import type { RequestReportConversionRateParams } from '../api/report-conversion-rate-fetch'; + +const getReportConversionRateQueryKey = ( + p: RequestReportConversionRateParams +) => + [ + 'reports', + 'conversion-rate', + p.from, + p.to, + p.interval, + p.date_type, + p.filters, + ] as const; + +export function reportConversionRateQuery( + params: RequestReportConversionRateParams +): UseQueryOptions< + ReturnType< typeof sanitizeReportConversionRateResponse > +> { + return { + queryKey: getReportConversionRateQueryKey( params ), + queryFn: async () => { + const response = await fetchReportConversionRate( params ); + return sanitizeReportConversionRateResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts new file mode 100644 index 000000000000..fe9e4d09b18e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCouponsByDate } from '../api'; +import { sanitizeReportCouponsByDateResponse } from '../processing/coupons-by-date'; +import type { ReportDataMap } from '../types'; +import { FilterCondition } from '../types/filter-condition'; + +type RequestReportCouponsByDateParams = Parameters< + typeof fetchReportCouponsByDate +>[ 0 ] & { + filters?: FilterCondition[]; +}; + +const getQueryKey = ( p: RequestReportCouponsByDateParams ) => + [ + 'reports', + 'couponsByDate', + p.from, + p.to, + p.interval, + p.date_type, + p.filters, + ] as const; + +export function reportCouponsByDateQuery( + params: RequestReportCouponsByDateParams +): UseQueryOptions< ReportDataMap[ 'couponsByDate' ] > { + return { + queryKey: getQueryKey( params ), + queryFn: async () => { + const response = await fetchReportCouponsByDate( params ); + return sanitizeReportCouponsByDateResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts new file mode 100644 index 000000000000..05244b7017ba --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCoupons } from '../api'; +import { sanitizeReportCouponsResponse } from '../processing/coupons'; +import type { ReportDataMap } from '../types'; +import { FilterCondition } from '../types/filter-condition'; + +type RequestReportCouponsParams = Parameters< + typeof fetchReportCoupons +>[ 0 ] & { + filters?: FilterCondition[]; +}; + +const getReportCouponsQueryKey = ( p: RequestReportCouponsParams ) => + [ + 'reports', + 'coupons', + p.from, + p.to, + p.interval, + p.date_type, + p.filters, + ] as const; + +export function reportCouponsQuery( + params: RequestReportCouponsParams +): UseQueryOptions< ReportDataMap[ 'coupons' ] > { + return { + queryKey: getReportCouponsQueryKey( params ), + queryFn: async () => { + const response = await fetchReportCoupons( params ); + return sanitizeReportCouponsResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts new file mode 100644 index 000000000000..b030320e91e4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCustomersByDate } from '../api/report-customers-by-date-fetch'; +import { sanitizeReportCustomersByDateResponse } from '../processing/customers-by-date'; +import type { ReportDataMap } from '../types'; + +type RequestReportCustomersByDateParams = Parameters< + typeof fetchReportCustomersByDate +>[ 0 ]; + +const getReportCustomersByDateQueryKey = ( + p: RequestReportCustomersByDateParams +) => + [ + 'reports', + 'customers', + 'by-date', + p.from, + p.to, + p.interval, + p.date_type, + ] as const; + +export function reportCustomersByDateQuery( + params: RequestReportCustomersByDateParams +): UseQueryOptions< ReportDataMap[ 'customersByDate' ] > { + return { + queryKey: getReportCustomersByDateQueryKey( params ), + queryFn: async () => { + const response = await fetchReportCustomersByDate( params ); + return sanitizeReportCustomersByDateResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts new file mode 100644 index 000000000000..c214fd28c6ed --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCustomers } from '../api'; +import { sanitizeReportCustomersResponse } from '../processing/customers'; +import type { ReportDataMap } from '../types'; + +type RequestReportCustomersParams = Parameters< + typeof fetchReportCustomers +>[ 0 ]; + +const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [ + 'reports', + 'customers', + 'new-returning', + p.from, + p.to, + p.date_type, + p.filters, +]; + +export function reportCustomersQuery( + params: RequestReportCustomersParams +): UseQueryOptions< ReportDataMap[ 'customers' ] > { + return { + queryKey: getReportCustomersQueryKey( params ), + queryFn: async () => { + const response = await fetchReportCustomers( params ); + return sanitizeReportCustomersResponse( response ); + }, + + /** + * Enable the query only if the from and to are set. + * Note: interval is not required for customers endpoint. + */ + enabled: !! ( params.from && params.to ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts new file mode 100644 index 000000000000..deda11889eaf --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts @@ -0,0 +1,144 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { + fetchReportOrderAttributionSummary, + fetchReportOrderAttributionByProduct, +} from '../api'; +import { + sanitizeReportOrderAttributionSummaryResponse, + normalizeOrderAttributionByProductResponse, + type SanitizedOrderAttributionSummaryResponse, +} from '../processing/order-attribution'; +import type { FilterCondition } from '../types/filter-condition'; +import { hasProductFilters } from '../utils/product-filters'; + +type ReportOrderAttributionSummaryParams = Parameters< + typeof fetchReportOrderAttributionSummary +>[ 0 ] & { + filters?: FilterCondition[]; +}; + +/** + * Creates a query key for order attribution queries. + * + * Note: All comparison parameters are included in the query key because + * order attribution returns both primary and comparison data in a single response. + */ +const getReportOrderAttributionQueryKey = ( + params: ReportOrderAttributionSummaryParams +) => + [ + 'reports', + 'order-attribution', + params.view, + params.from, + params.to, + params.interval, + params.date_type, + params.compare_from, + params.compare_to, + params.filters, + ] as const; + +/** + * React Query configuration for order attribution summary data. + * + * This query is designed to be used with `use-report` hook, which provides + * standardized loading states and comparison handling. + * + * Important architectural notes: + * - Unlike other report queries, order attribution includes comparison data in the + * PRIMARY response, not in a separate comparison query + * - When used with `use-report`, the comparison query is disabled (it's a no-op) + * - This query supports two API endpoints: + * 1. Regular order-attribution API: Returns both periods in a single response + * 2. By-product API: Fetches periods separately, then normalizes to match (1) + * + * @param params - Query parameters including date ranges and optional filters + * @return React Query options with query key, fetch function, and enabled state + */ +export function reportOrderAttributionSummaryQuery( + params: ReportOrderAttributionSummaryParams +): UseQueryOptions< SanitizedOrderAttributionSummaryResponse > { + return { + queryKey: getReportOrderAttributionQueryKey( params ), + queryFn: async () => { + const hasProductFiltersValue = hasProductFilters( params.filters ); + + // Choose API based on whether product filters are present + if ( hasProductFiltersValue ) { + // By-product API path: Fetch primary and comparison periods in parallel + const { compare_from, compare_to } = params; + + // Determine if we need to fetch comparison period + const shouldFetchComparison = + compare_from && + compare_to && + ( compare_from !== params.from || + compare_to !== params.to ); + + // Fetch both periods in parallel for better performance + const [ currentResponse, previousResponse ] = await Promise.all( + [ + fetchReportOrderAttributionByProduct( { + from: params.from, + to: params.to, + interval: params.interval, + view: params.view, + filters: params.filters, + date_type: params.date_type, + } ), + shouldFetchComparison + ? fetchReportOrderAttributionByProduct( { + from: compare_from, + to: compare_to, + interval: params.interval, + view: params.view, + filters: params.filters, + date_type: params.date_type, + } ) + : Promise.resolve( undefined ), + ] + ); + + // Normalize to match the regular API structure (includes both periods) + const normalizedResponse = + normalizeOrderAttributionByProductResponse( + currentResponse, + previousResponse + ); + + return sanitizeReportOrderAttributionSummaryResponse( + normalizedResponse + ); + } + + // Regular API path: Returns both primary and comparison in one response + const response = await fetchReportOrderAttributionSummary( params ); + return sanitizeReportOrderAttributionSummaryResponse( response ); + }, + + /** + * Enable the query only when all required parameters are present. + * The 'view' parameter is required for order attribution queries. + */ + enabled: !! ( + params.from && + params.to && + params.interval && + params.view + ), + + /** + * Keep previous data while fetching to prevent flash of empty state. + * This provides a smoother user experience during data refetching. + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts new file mode 100644 index 000000000000..50703f37728c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportOrders } from '../api'; +import { sanitizeReportOrdersResponse } from '../processing/orders'; +import type { ReportDataMap } from '../types'; + +type RequestReportOrdersParams = Parameters< typeof fetchReportOrders >[ 0 ]; + +const getReportOrdersQueryKey = ( p: RequestReportOrdersParams ) => [ + 'reports', + 'orders', + p.from, + p.to, + p.interval, + p.date_type, + p.filters || [], +]; + +export function reportOrdersQuery( + params: RequestReportOrdersParams +): UseQueryOptions< ReportDataMap[ 'orders' ] > { + return { + queryKey: getReportOrdersQueryKey( params ), + queryFn: async () => { + const response = await fetchReportOrders( params ); + return sanitizeReportOrdersResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts new file mode 100644 index 000000000000..4696117be2e2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportProducts } from '../api/report-products-fetch'; +import { sanitizeReportProductsResponse } from '../processing/products'; + +type RequestReportProductsParams = Parameters< + typeof fetchReportProducts +>[ 0 ]; + +type SanitizedProductsResponse = ReturnType< + typeof sanitizeReportProductsResponse +>; + +const getReportProductsQueryKey = ( p: RequestReportProductsParams ) => + [ + 'reports', + 'products', + p.from, + p.to, + p.date_type, + p.limit, + p.orderby, + p.order, + p.filters, + ] as const; + +export function reportProductsQuery( + params: RequestReportProductsParams +): UseQueryOptions< SanitizedProductsResponse > { + return { + queryKey: getReportProductsQueryKey( params ), + queryFn: async () => { + const response = await fetchReportProducts( params ); + return sanitizeReportProductsResponse( response ); + }, + + /** + * Enable the query only if the from and to are set. + */ + enabled: !! ( params.from && params.to ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts new file mode 100644 index 000000000000..8e425b7e7127 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportSessionsByDevice } from '../api/report-sessions-by-device-fetch'; +import { sanitizeReportSessionsByDeviceResponse } from '../processing/sessions-by-device'; +import type { ReportDataMap } from '../types'; + +type RequestReportSessionsByDeviceParams = Parameters< + typeof fetchReportSessionsByDevice +>[ 0 ]; + +const getReportSessionsByDeviceQueryKey = ( + p: RequestReportSessionsByDeviceParams +) => [ 'reports', 'sessions', 'by-device', p.from, p.to ] as const; + +/** + * Creates query options for fetching sessions by device report data. + * + * @param params - Request parameters with from/to dates + */ +export function reportSessionsByDeviceQuery( + params: RequestReportSessionsByDeviceParams +): UseQueryOptions< ReportDataMap[ 'sessionsByDevice' ] > { + return { + queryKey: getReportSessionsByDeviceQueryKey( params ), + queryFn: async () => { + const response = await fetchReportSessionsByDevice( params ); + return sanitizeReportSessionsByDeviceResponse( response ); + }, + + /** + * Enable the query only if from and to dates are set. + * Note: This endpoint doesn't use interval (it's not a time-series). + */ + enabled: !! ( params.from && params.to ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts new file mode 100644 index 000000000000..a75988200065 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportVisitorsByLocation } from '../api'; +import { sanitizeReportVisitorsByLocationResponse } from '../processing/visitors-by-location'; +import type { ReportDataMap } from '../types'; + +type RequestReportVisitorsByLocationParams = Parameters< + typeof fetchReportVisitorsByLocation +>[ 0 ]; + +const getReportVisitorsByLocationQueryKey = ( + p: RequestReportVisitorsByLocationParams +) => + [ + 'reports', + 'visitors', + 'by-location', + p.group_by, + p.country_code ?? null, + p.from, + p.to, + p.interval, + p.limit ?? null, + ] as const; + +export function reportVisitorsByLocationQuery( + params: RequestReportVisitorsByLocationParams +): UseQueryOptions< ReportDataMap[ 'visitorsByLocation' ] > { + return { + queryKey: getReportVisitorsByLocationQueryKey( params ), + queryFn: async () => { + const response = await fetchReportVisitorsByLocation( params ); + return sanitizeReportVisitorsByLocationResponse( response ); + }, + + enabled: !! ( params.from && params.to && params.interval ), + + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts new file mode 100644 index 000000000000..d3749c184256 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportVisitors } from '../api'; +import { sanitizeReportVisitorsResponse } from '../processing/visitors'; +import type { ReportDataMap } from '../types'; + +type RequestReportVisitorsParams = Parameters< + typeof fetchReportVisitors +>[ 0 ]; + +const getReportVisitorsQueryKey = ( p: RequestReportVisitorsParams ) => + [ + 'reports', + 'visitors', + 'by-date', + p.from, + p.to, + p.interval, + p.date_type, + ] as const; + +export function reportVisitorsQuery( + params: RequestReportVisitorsParams +): UseQueryOptions< ReportDataMap[ 'visitors' ] > { + return { + queryKey: getReportVisitorsQueryKey( params ), + queryFn: async () => { + const response = await fetchReportVisitors( params ); + return sanitizeReportVisitorsResponse( response ); + }, + + /** + * Enable the query only if the from, to, and interval are set. + */ + enabled: !! ( params.from && params.to && params.interval ), + + /** + * Keep previous data while fetching new data to prevent blank states + */ + placeholderData: ( previousData ) => previousData, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/types.ts b/projects/packages/premium-analytics/packages/data/src/types.ts new file mode 100644 index 000000000000..8ad61e0cbbd0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/types.ts @@ -0,0 +1,120 @@ +/** + * Internal dependencies + */ +import { + sanitizeReportOrdersResponse, + sanitizeReportProductsResponse, +} from './processing'; +import { sanitizeReportCustomersResponse } from './processing/customers'; +import { sanitizeReportCustomersByDateResponse } from './processing/customers-by-date'; +import { sanitizeReportOrderAttributionSummaryResponse } from './processing/order-attribution'; +import { sanitizeReportCouponsResponse } from './processing/coupons'; +import { sanitizeReportCouponsByDateResponse } from './processing/coupons-by-date'; +import { sanitizeReportVisitorsResponse } from './processing/visitors'; +import { sanitizeReportVisitorsByLocationResponse } from './processing/visitors-by-location'; +import { sanitizeReportConversionRateResponse } from './processing/conversion-rate'; +import { sanitizeReportOrdersByProductTypeResponse } from './processing/orders-by-product-type'; +import { sanitizeReportBookingsResponse } from './processing/bookings'; +import { sanitizeReportSessionsByDeviceResponse } from './processing/sessions-by-device'; +import type { ReportParams } from './utils/search'; + +export type ReportType = + | 'orders' + | 'orders-by-product-type' + | 'order-attribution' + | 'coupons' + | 'couponsByDate' + | 'customers' + | 'customersByDate' + | 'products' + | 'visitors' + | 'visitorsByLocation' + | 'conversionRate' + | 'bookings' + | 'sessionsByDevice'; + +export type QueryParams = ReportParams & { + p?: string; // encoded pathname +}; + +// Inferred from processing/orders.ts +type SanitizedOrdersByDateResponse = ReturnType< + typeof sanitizeReportOrdersResponse +>; + +// Inferred from processing/order-attribution.ts +type SanitizedOrderAttributionSummaryResponse = ReturnType< + typeof sanitizeReportOrderAttributionSummaryResponse +>; + +// Inferred from processing/coupons.ts +type SanitizedCouponsResponse = ReturnType< + typeof sanitizeReportCouponsResponse +>; + +// Inferred from processing/coupons-by-date/index.ts +type SanitizedCouponsByDateResponse = ReturnType< + typeof sanitizeReportCouponsByDateResponse +>; + +// Inferred from processing/customers.ts +type SanitizedCustomersResponse = ReturnType< + typeof sanitizeReportCustomersResponse +>; + +// Inferred from processing/customers-by-date/index.ts +type SanitizedCustomersByDateResponse = ReturnType< + typeof sanitizeReportCustomersByDateResponse +>; + +// Inferred from processing/products.ts +type SanitizedProductsResponse = ReturnType< + typeof sanitizeReportProductsResponse +>; + +// Inferred from processing/visitors.ts +type SanitizedVisitorsResponse = ReturnType< + typeof sanitizeReportVisitorsResponse +>; + +// Inferred from processing/visitors-by-location.ts +type SanitizedVisitorsByLocationResponse = ReturnType< + typeof sanitizeReportVisitorsByLocationResponse +>; + +// Inferred from processing/conversion-rate.ts +type SanitizedConversionRateResponse = ReturnType< + typeof sanitizeReportConversionRateResponse +>; + +// Inferred from processing/orders-by-product-type.ts +type SanitizedOrdersByProductTypeResponse = ReturnType< + typeof sanitizeReportOrdersByProductTypeResponse +>; + +// Inferred from processing/bookings.ts +type SanitizedBookingsResponse = ReturnType< + typeof sanitizeReportBookingsResponse +>; + +// Inferred from processing/sessions-by-device.ts +type SanitizedSessionsByDeviceResponse = ReturnType< + typeof sanitizeReportSessionsByDeviceResponse +>; + +// Type mapping for report types to their PROCESSED data structures +export interface ReportDataMap { + orders: SanitizedOrdersByDateResponse; // Returns processed data with numbers + 'orders-by-product-type': SanitizedOrdersByProductTypeResponse; // Returns processed orders by product type data with numbers + 'order-attribution': SanitizedOrderAttributionSummaryResponse; // Returns processed attribution data + coupons: SanitizedCouponsResponse; // Returns processed coupons data with numbers + couponsByDate: SanitizedCouponsByDateResponse; // Returns processed coupons-by-date data with numbers + customers: SanitizedCustomersResponse; // Returns processed customers data with numbers + customersByDate: SanitizedCustomersByDateResponse; // Returns processed customers by date data with numbers + products: SanitizedProductsResponse; // Returns raw products data + visitors: SanitizedVisitorsResponse; // Returns processed visitors data with numbers + visitorsByLocation: SanitizedVisitorsByLocationResponse; // Returns processed visitors grouped by location (country or region) + conversionRate: SanitizedConversionRateResponse; // Returns processed conversion rate data with numbers + bookings: SanitizedBookingsResponse; // Returns processed bookings data with numbers + sessionsByDevice: SanitizedSessionsByDeviceResponse; // Returns processed sessions by device data with numbers +} diff --git a/projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts b/projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts new file mode 100644 index 000000000000..d6bb433880a2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts @@ -0,0 +1,23 @@ +/** + * Filter condition types for API queries + */ + +// Different type of filters have different comparison operators +// @see https://github.a8c.com/Automattic/wpcom/tree/72572945acd96d29adf9ea8f38fc3e99c9a4a668/wp-content/rest-api-plugins/endpoints/woocommerce-analytics/Reports/Filter +export type FilterCondition = { + key: string; + value: string | string[]; + compare: + | '=' + | 'IN' + | 'NOT IN' + | '!=' + | '>' + | '<' + | '>=' + | '<=' + | 'BETWEEN' + | 'NOT BETWEEN' + | 'LIKE' + | 'NOT LIKE'; +}; diff --git a/projects/packages/premium-analytics/packages/data/src/types/product-image.ts b/projects/packages/premium-analytics/packages/data/src/types/product-image.ts new file mode 100644 index 000000000000..8fc5b65ad822 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/types/product-image.ts @@ -0,0 +1,7 @@ +/** + * Product image type definition + */ +export interface ProductImage { + imageUrl: string; + imageAlt: string; +} diff --git a/projects/packages/premium-analytics/packages/data/src/types/product-type.ts b/projects/packages/premium-analytics/packages/data/src/types/product-type.ts new file mode 100644 index 000000000000..2b5a41ab1cf8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/types/product-type.ts @@ -0,0 +1,4 @@ +/** + * Product type categories for filtering and organization + */ +export type ProductType = 'general' | 'products' | 'bookings'; diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts new file mode 100644 index 000000000000..2ad7b3fb685d --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { + startOfDay, + endOfDay, + subDays, + subMonths, + subYears, + startOfMonth, + endOfMonth, + startOfYear, + endOfYear, +} from 'date-fns'; +import { tz } from '@date-fns/tz'; + +/** + * Mocks – getSiteTimezone and dateToISOStringWithLocalTZ + * depend on WordPress core store. + * We mock them to remove that dependency. + * + * dateToISOStringWithLocalTZ normalizes to UTC Z-format + * (matching native Date.toISOString) since the mock timezone + * is +00:00 and all dates are UTC. + */ +jest.mock( '../date', () => ( { + getSiteTimezone: jest.fn( () => '+00:00' ), + dateToISOStringWithLocalTZ: jest.fn( ( date: Date ) => + new Date( date.getTime() ).toISOString() + ), +} ) ); + +/** + * Internal dependencies + */ +import { computeDateRangeFromPreset } from '../preset-date-range'; + +/* + * Pin "now" to 2026-02-19 12:00:00 UTC for deterministic results. + * + * Expected dates are computed in UTC. Since TZ is mocked to +00:00, + * computePrimaryRange runs in UTC and dateToISOStringWithLocalTZ + * normalizes to Z-format via the mock. + */ +const NOW = new Date( '2026-02-19T12:00:00.000Z' ); +const UTC = tz( '+00:00' ); + +/* + * Normalize a TZDate or Date to Z-format ISO string, + * ensuring the expected values match the mock's output format. + */ +function toZ( date: Date ): string { + return new Date( date.getTime() ).toISOString(); +} + +const TODAY_START = startOfDay( NOW, { in: UTC } ); +const TODAY_END = endOfDay( NOW, { in: UTC } ); +const YESTERDAY_END = endOfDay( subDays( TODAY_START, 1 ), { in: UTC } ); +const LAST_MONTH = subMonths( TODAY_START, 1 ); + +beforeAll( () => { + jest.useFakeTimers(); + jest.setSystemTime( NOW ); +} ); + +afterAll( () => { + jest.useRealTimers(); +} ); + +describe( 'computeDateRangeFromPreset', () => { + it( 'returns today range for "today"', () => { + const range = computeDateRangeFromPreset( 'today' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toZ( TODAY_START ) ); + expect( range!.to ).toBe( toZ( TODAY_END ) ); + } ); + + it( 'returns yesterday range for "yesterday"', () => { + const range = computeDateRangeFromPreset( 'yesterday' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toZ( subDays( TODAY_START, 1 ) ) ); + expect( range!.to ).toBe( toZ( YESTERDAY_END ) ); + } ); + + it( 'returns 7-day range ending yesterday for "last-7-days"', () => { + const range = computeDateRangeFromPreset( 'last-7-days' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toZ( subDays( TODAY_START, 7 ) ) ); + expect( range!.to ).toBe( toZ( YESTERDAY_END ) ); + } ); + + it( 'returns 30-day range ending yesterday for "last-30-days"', () => { + const range = computeDateRangeFromPreset( 'last-30-days' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toZ( subDays( TODAY_START, 30 ) ) ); + expect( range!.to ).toBe( toZ( YESTERDAY_END ) ); + } ); + + it( 'returns 90-day range ending yesterday for "last-90-days"', () => { + const range = computeDateRangeFromPreset( 'last-90-days' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toZ( subDays( TODAY_START, 90 ) ) ); + expect( range!.to ).toBe( toZ( YESTERDAY_END ) ); + } ); + + it( 'returns 365-day range ending yesterday for "last-365-days"', () => { + const range = computeDateRangeFromPreset( 'last-365-days' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toZ( subDays( TODAY_START, 365 ) ) ); + expect( range!.to ).toBe( toZ( YESTERDAY_END ) ); + } ); + + it( 'returns last calendar month for "last-month"', () => { + const range = computeDateRangeFromPreset( 'last-month' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( + toZ( startOfMonth( LAST_MONTH, { in: UTC } ) ) + ); + expect( range!.to ).toBe( + toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) + ); + } ); + + it( 'returns last 12 calendar months for "last-12-months"', () => { + const range = computeDateRangeFromPreset( 'last-12-months' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( + toZ( startOfMonth( subMonths( TODAY_START, 12 ), { in: UTC } ) ) + ); + expect( range!.to ).toBe( + toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) + ); + } ); + + it( 'returns last calendar year for "last-year"', () => { + const range = computeDateRangeFromPreset( 'last-year' ); + const lastYear = subYears( TODAY_START, 1 ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( + toZ( startOfYear( lastYear, { in: UTC } ) ) + ); + expect( range!.to ).toBe( toZ( endOfYear( lastYear, { in: UTC } ) ) ); + } ); + + it( 'returns undefined for unrecognized preset', () => { + // @ts-expect-error – testing with invalid preset on purpose + const range = computeDateRangeFromPreset( 'not-a-preset' ); + + expect( range ).toBeUndefined(); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts new file mode 100644 index 000000000000..88ca70758db1 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts @@ -0,0 +1,82 @@ +/** + * Mocks – break the dependency chain to @wordpress/core-data. + */ +jest.mock( '../../defaults', () => ( { + getDefaultQueryParams: jest.fn(), +} ) ); + +jest.mock( '../preset-date-range', () => ( { + computeDateRangeFromPreset: jest.fn(), +} ) ); + +jest.mock( '../interval', () => ( { + getDefaultIntervalForPeriod: jest.fn(), +} ) ); + +/** + * Internal dependencies + */ +import { hasComparisonEnabled } from '../search'; + +describe( 'hasComparisonEnabled', () => { + it( 'returns true when all comparison fields are present', () => { + expect( + hasComparisonEnabled( { + comp: '1', + compare_from: '2026-01-01T00:00:00.000-05:00', + compare_to: '2026-01-31T23:59:59.999-05:00', + } ) + ).toBe( true ); + } ); + + it( 'returns false when comp is undefined', () => { + expect( + hasComparisonEnabled( { + compare_from: '2026-01-01T00:00:00.000-05:00', + compare_to: '2026-01-31T23:59:59.999-05:00', + } ) + ).toBe( false ); + } ); + + it( 'returns false when compare_from is missing', () => { + expect( + hasComparisonEnabled( { + comp: '1', + compare_to: '2026-01-31T23:59:59.999-05:00', + } ) + ).toBe( false ); + } ); + + it( 'returns false when compare_to is missing', () => { + expect( + hasComparisonEnabled( { + comp: '1', + compare_from: '2026-01-01T00:00:00.000-05:00', + } ) + ).toBe( false ); + } ); + + it( 'returns false when compare_from is whitespace', () => { + expect( + hasComparisonEnabled( { + comp: '1', + compare_from: ' ', + compare_to: '2026-01-31T23:59:59.999-05:00', + } ) + ).toBe( false ); + } ); + + it( 'returns false when compare_to is whitespace', () => { + expect( + hasComparisonEnabled( { + comp: '1', + compare_from: '2026-01-01T00:00:00.000-05:00', + compare_to: ' ', + } ) + ).toBe( false ); + } ); + + it( 'returns false for empty object', () => { + expect( hasComparisonEnabled( {} ) ).toBe( false ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts new file mode 100644 index 000000000000..25850845d3c8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts @@ -0,0 +1,294 @@ +/** + * Mocks – must appear before the import of the module under test. + */ +jest.mock( '../../defaults', () => ( { + getDefaultQueryParams: jest.fn(), +} ) ); + +jest.mock( '../preset-date-range', () => ( { + computeDateRangeFromPreset: jest.fn(), +} ) ); + +jest.mock( '../interval', () => ( { + getDefaultIntervalForPeriod: jest.fn(), +} ) ); + +/** + * Internal dependencies + */ +import { normalizeReportParams } from '../search'; +import { getDefaultQueryParams } from '../../defaults'; +import { computeDateRangeFromPreset } from '../preset-date-range'; +import { getDefaultIntervalForPeriod } from '../interval'; +import type { ReportParams } from '../search'; + +const mockGetDefaults = getDefaultQueryParams as jest.MockedFunction< + typeof getDefaultQueryParams +>; +const mockComputeRange = computeDateRangeFromPreset as jest.MockedFunction< + typeof computeDateRangeFromPreset +>; +const mockGetInterval = getDefaultIntervalForPeriod as jest.MockedFunction< + typeof getDefaultIntervalForPeriod +>; + +/* + * Deterministic date strings. + * FRESH = what computeDateRangeFromPreset returns "today". + * STALE = what the URL had from a previous day. + */ +const FRESH_FROM = '2026-01-20T00:00:00.000-05:00'; +const FRESH_TO = '2026-02-18T23:59:59.999-05:00'; +const STALE_FROM = '2026-01-19T00:00:00.000-05:00'; +const STALE_TO = '2026-02-17T23:59:59.999-05:00'; + +const DEFAULTS_WITH_COMPARISON: ReportParams = { + from: FRESH_FROM, + to: FRESH_TO, + preset: 'last-30-days', + interval: 'day', + compare_from: '2025-12-21T00:00:00.000-05:00', + compare_to: '2026-01-19T23:59:59.999-05:00', + compare_preset: 'previous-period', + comp: '1', +}; + +beforeEach( () => { + jest.clearAllMocks(); + + // Sensible defaults for every test – override per-scenario as needed. + mockGetDefaults.mockReturnValue( { ...DEFAULTS_WITH_COMPARISON } ); + mockComputeRange.mockReturnValue( { + from: FRESH_FROM, + to: FRESH_TO, + } ); + mockGetInterval.mockReturnValue( 'day' ); +} ); + +describe( 'normalizeReportParams', () => { + /* + * Scenario 1 – Fresh load (no params in URL) + * The user visits /dashboard with no query string. + * Expected: defaults kick in, preset "last-30-days" is used, + * and default comparison is applied. + */ + it( 'applies defaults with preset and comparison on fresh load', () => { + const result = normalizeReportParams(); + + // Preset should come from defaults. + expect( result.preset ).toBe( 'last-30-days' ); + expect( mockComputeRange ).toHaveBeenCalledWith( 'last-30-days' ); + + // Dates should come from computeDateRangeFromPreset. + expect( result.from ).toBe( FRESH_FROM ); + expect( result.to ).toBe( FRESH_TO ); + + // Default comparison should be applied (search is undefined + // → !search?.from → true → default branch). + expect( result.comp ).toBe( '1' ); + expect( result.compare_from ).toBe( + DEFAULTS_WITH_COMPARISON.compare_from + ); + expect( result.compare_to ).toBe( DEFAULTS_WITH_COMPARISON.compare_to ); + } ); + + /* + * Scenario 2 – Same-day reload with preset + * The URL has preset=last-30-days and from/to that match today's + * computation. The dates are still fresh → no redirect needed. + */ + it( 'returns same dates when preset range is still fresh', () => { + const result = normalizeReportParams( { + from: FRESH_FROM, + to: FRESH_TO, + preset: 'last-30-days', + interval: 'day', + } ); + + expect( result.from ).toBe( FRESH_FROM ); + expect( result.to ).toBe( FRESH_TO ); + expect( result.preset ).toBe( 'last-30-days' ); + + // No comparison in search → no comparison in output + // (search.from is present → !search.from is false + // → default comparison branch is skipped). + expect( result.comp ).toBeUndefined(); + } ); + + /* + * Scenario 3 – Next-day reload with stale dates + * The URL has yesterday's dates but the same preset. + * computeDateRangeFromPreset returns fresh dates → redirect. + */ + it( 'recalculates dates when preset range is stale', () => { + const result = normalizeReportParams( { + from: STALE_FROM, + to: STALE_TO, + preset: 'last-30-days', + interval: 'day', + } ); + + // Should use the fresh range from the preset, not stale URL dates. + expect( result.from ).toBe( FRESH_FROM ); + expect( result.to ).toBe( FRESH_TO ); + expect( result.preset ).toBe( 'last-30-days' ); + expect( mockComputeRange ).toHaveBeenCalledWith( 'last-30-days' ); + } ); + + /* + * Scenario 4 – Custom range (no preset) + * The user picked explicit from/to dates without a preset. + * The dates should be used as-is, no recalculation. + */ + it( 'uses explicit dates as-is when no preset is set', () => { + const customFrom = '2026-01-01T00:00:00.000-05:00'; + const customTo = '2026-01-31T23:59:59.999-05:00'; + + const result = normalizeReportParams( { + from: customFrom, + to: customTo, + } ); + + expect( result.from ).toBe( customFrom ); + expect( result.to ).toBe( customTo ); + expect( result.preset ).toBeUndefined(); + // computeDateRangeFromPreset should NOT be called. + expect( mockComputeRange ).not.toHaveBeenCalled(); + } ); + + it( 'uses explicit dates as-is when preset is custom', () => { + const customFrom = '2026-01-01T00:00:00.000-05:00'; + const customTo = '2026-01-31T23:59:59.999-05:00'; + + const result = normalizeReportParams( { + from: customFrom, + to: customTo, + preset: 'custom', + } ); + + expect( result.from ).toBe( customFrom ); + expect( result.to ).toBe( customTo ); + expect( result.preset ).toBeUndefined(); + expect( mockComputeRange ).not.toHaveBeenCalled(); + } ); + + /* + * Scenario 5 – Preset with stale comparison enabled + * The URL has a stale preset and comparison params. + * Primary range is recalculated; comparison is preserved from URL. + */ + it( 'recalculates primary but preserves comparison from URL', () => { + const compFrom = '2025-12-20T00:00:00.000-05:00'; + const compTo = '2026-01-18T23:59:59.999-05:00'; + + const result = normalizeReportParams( { + from: STALE_FROM, + to: STALE_TO, + preset: 'last-30-days', + interval: 'day', + comp: '1', + compare_from: compFrom, + compare_to: compTo, + compare_preset: 'previous-period', + } ); + + // Primary recalculated from preset. + expect( result.from ).toBe( FRESH_FROM ); + expect( result.to ).toBe( FRESH_TO ); + + // Comparison passed through from search. + expect( result.comp ).toBe( '1' ); + expect( result.compare_from ).toBe( compFrom ); + expect( result.compare_to ).toBe( compTo ); + expect( result.compare_preset ).toBe( 'previous-period' ); + } ); + + /* + * Scenario 6 – Preset without comparison + * The URL has a stale preset but comparison is disabled. + * Primary is recalculated; comparison params are absent. + */ + it( 'recalculates primary with no comparison when comp is absent', () => { + const result = normalizeReportParams( { + from: STALE_FROM, + to: STALE_TO, + preset: 'last-30-days', + interval: 'day', + } ); + + // Primary recalculated. + expect( result.from ).toBe( FRESH_FROM ); + expect( result.to ).toBe( FRESH_TO ); + + // No comparison in search, and search.from is present + // → default comparison is NOT applied. + expect( result.comp ).toBeUndefined(); + expect( result.compare_from ).toBeUndefined(); + expect( result.compare_to ).toBeUndefined(); + } ); + + /* + * Edge case – Invalid preset in URL is ignored. + */ + it( 'ignores invalid preset and uses URL dates', () => { + const customFrom = '2026-02-01T00:00:00.000-05:00'; + const customTo = '2026-02-15T23:59:59.999-05:00'; + + const result = normalizeReportParams( { + from: customFrom, + to: customTo, + // @ts-expect-error – testing with invalid preset on purpose + preset: 'not-a-real-preset', + } ); + + expect( result.from ).toBe( customFrom ); + expect( result.to ).toBe( customTo ); + expect( result.preset ).toBeUndefined(); + expect( mockComputeRange ).not.toHaveBeenCalled(); + } ); + + /* + * Edge case – computeDateRangeFromPreset returns undefined + * (e.g., an unimplemented preset). Falls back to search dates. + */ + it( 'falls back to URL dates when preset has no range implementation', () => { + mockComputeRange.mockReturnValue( undefined ); + + const result = normalizeReportParams( { + from: STALE_FROM, + to: STALE_TO, + preset: 'last-30-days', + } ); + + // Preset should be cleared. + expect( result.preset ).toBeUndefined(); + // Falls back to search dates. + expect( result.from ).toBe( STALE_FROM ); + expect( result.to ).toBe( STALE_TO ); + } ); + + /* + * Edge case – date_type is preserved from search. + */ + it( 'preserves date_type from search', () => { + const result = normalizeReportParams( { + from: FRESH_FROM, + to: FRESH_TO, + date_type: 'paid', + } ); + + expect( result.date_type ).toBe( 'paid' ); + } ); + + /* + * Edge case – date_type defaults to "created". + */ + it( 'defaults date_type to created', () => { + const result = normalizeReportParams( { + from: FRESH_FROM, + to: FRESH_TO, + } ); + + expect( result.date_type ).toBe( 'created' ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/date.ts b/projects/packages/premium-analytics/packages/data/src/utils/date.ts new file mode 100644 index 000000000000..41f385c01ab7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts @@ -0,0 +1,122 @@ +/** + * External dependencies + */ +import { select } from '@wordpress/data'; +import { store as coreStore, type Settings } from '@wordpress/core-data'; +import { + toLocalTZ, + formatToTimezoneNaiveString as _formatNaive, + dateToISOStringWithTZ as _toISOWithTZ, +} from '@next-woo-analytics/datetime'; +import { type TZDate } from '@date-fns/tz'; + +type FullSettings = Settings & { + gmt_offset: number; +}; + +let DEFAULT_TIME_ZONE: string; +try { + DEFAULT_TIME_ZONE = + Intl.DateTimeFormat().resolvedOptions().timeZone ?? '+00:00'; +} catch { + DEFAULT_TIME_ZONE = '+00:00'; +} + +/** + * Format the GMT offset to a string. + * + * @param {number | undefined} offset - The GMT offset. + * @return {string} The formatted GMT offset. + */ +function formatGmtOffset( offset: number | undefined ): string { + if ( ! offset ) { + return DEFAULT_TIME_ZONE; + } + + const sign = offset >= 0 ? '+' : '-'; + const abs = Math.abs( offset ); + const hours = Math.floor( abs ); + const minutes = Math.floor( ( abs - hours ) * 60 + 1e-6 ); + return `${ sign }${ String( hours ).padStart( 2, '0' ) }:${ String( + minutes + ).padStart( 2, '0' ) }`; +} + +/* + * Get the timezone from the site settings. + * If the timezone is not set, use the GMT offset. + * If the GMT offset is not set, use the default timezone. + * + * @param {string} timezone - The timezone to use. + * @return {string} The timezone. + */ +export function getSiteTimezone() { + const siteSettings = select( coreStore ).getEntityRecord( + 'root', + 'site' + ) as FullSettings; + + if ( ! siteSettings ) { + return DEFAULT_TIME_ZONE; + } + + return siteSettings?.timezone?.length + ? siteSettings?.timezone + : formatGmtOffset( siteSettings?.gmt_offset ) || DEFAULT_TIME_ZONE; +} + +/** + * Returns the site's GMT offset as a string (e.g. "+05:30", "-08:00"). + * If site settings are not loaded, throws an error. + * @return {string} The site's GMT offset. + */ +export function getSiteGmtOffset(): string { + const siteSettings = select( coreStore ).getEntityRecord( + 'root', + 'site' + ) as FullSettings; + if ( ! siteSettings ) { + throw new Error( + 'getSiteGmtOffset() called before core settings are ready' + ); + } + return formatGmtOffset( siteSettings?.gmt_offset ) || DEFAULT_TIME_ZONE; +} + +/** + * Same API and behavior as your current localTZDate: + * - Accepts number | string | Date (or undefined -> now) + * - Uses site timezone by default + * - Returns TZDate (timezone-aware) + */ +export function localTZDate( + value?: number | string | Date, + timezone?: string +): TZDate { + const tz = timezone ?? getSiteTimezone(); + return toLocalTZ( value, tz ); +} + +/** + * Same semantics as your current helper: + * TZ-aware -> timezone-naive "YYYY-MM-DDTHH:mm:ss.SSS" + */ +export function formatToTimezoneNaiveString( + date: Date, + timezone?: string +): string { + const tz = timezone ?? getSiteTimezone(); + return _formatNaive( date, tz ); +} + +/** + * Same semantics as your current helper: + * TZ-aware -> ISO with offset "YYYY-MM-DDTHH:mm:ss.SSSxxx" + */ +export function dateToISOStringWithLocalTZ( + date: Date, + timezone?: string +): string { + const tz = timezone ?? getSiteTimezone(); + return _toISOWithTZ( date, tz ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts new file mode 100644 index 000000000000..697202c07c86 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { resolveSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +let readyPromise: Promise< void > | null = null; + +/** + * Ensures that 'site' and 'general settings' are in the coreStore. + * Memoizes the same promise to avoid races and duplicate requests. + */ +export function ensureCoreSettingsReady(): Promise< void > { + if ( ! readyPromise ) { + readyPromise = Promise.all( [ + resolveSelect( coreStore ).getEntityRecord( 'root', 'site' ), + resolveSelect( coreStore ).getEntityRecord( + 'root', + 'settings', + 'general' + ), + ] ).then( () => void 0 ); + } + return readyPromise; +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/index.ts b/projects/packages/premium-analytics/packages/data/src/utils/index.ts new file mode 100644 index 000000000000..1883dee36011 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/index.ts @@ -0,0 +1,15 @@ +export { + localTZDate, + dateToISOStringWithLocalTZ, + formatToTimezoneNaiveString, + getSiteTimezone, + getSiteGmtOffset, +} from './date'; +export { ensureCoreSettingsReady } from './ensure-core-settings'; +export { getDefaultIntervalForPeriod } from './interval'; +export { safeParseInt, safeParseFloat } from './parsing'; +export { computeDateRangeFromPreset } from './preset-date-range'; +export { hasProductFilters } from './product-filters'; +export type { PresetType, ReportParams } from './search'; +export { isSelectablePreset } from '@next-woo-analytics/datetime'; +export type { Override, BaseReportParams } from './types'; diff --git a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts new file mode 100644 index 000000000000..e73a9a4e04b2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { differenceInHours } from 'date-fns'; + +/** + * Internal dependencies + */ +import type { IntervalType } from './search'; +import { localTZDate } from './date'; + +function getAllowedIntervalsByRange( + from: string, + to: string +): IntervalType[] { + // Use hours instead of days to handle ranges that are 1 second short of a full day. + // E.g., '2024-11-01 00:00:00' to '2025-10-31 23:59:59' is 8759 hours (364.958 days), + // which rounds to 365 days, correctly categorizing it as a yearly interval. + const daysDiff = Math.round( + Math.abs( + differenceInHours( localTZDate( to ), localTZDate( from ) ) / 24 + ) + ); + + if ( daysDiff >= 1095 ) { + return [ 'quarter', 'year' ]; + } else if ( daysDiff >= 365 ) { + return [ 'month', 'quarter' ]; + } else if ( daysDiff >= 90 ) { + return [ 'week', 'month' ]; + } else if ( daysDiff >= 28 ) { + return [ 'day', 'week' ]; + } else if ( daysDiff >= 3 ) { + return [ 'day' ]; + } else if ( daysDiff >= 1 ) { + return [ 'hour', 'day' ]; + } + + return [ 'hour', 'day' ]; +} + +/** + * Returns the allowed selectable intervals for a specific period. + * + * @return {Array} Array containing allowed intervals. + */ +function getAllowedIntervalsForPeriod( + period: string | undefined, + from: string, + to: string +): IntervalType[] { + switch ( period ) { + case 'today': + case 'yesterday': + return [ 'hour', 'day' ]; + case 'last-7-days': + return [ 'day' ]; + case 'last-30-days': + case 'last-month': + return [ 'day', 'week' ]; + case 'last-90-days': + return [ 'week', 'month' ]; + case 'last-12-months': + case 'last-365-days': + case 'last-year': + return [ 'month', 'quarter' ]; + default: + return getAllowedIntervalsByRange( from, to ); + } +} + +export function getDefaultIntervalForPeriod( + period: string | undefined, + from: string, + to: string +): IntervalType { + return getAllowedIntervalsForPeriod( period, from, to )?.[ 0 ] ?? 'day'; +} + +export function getDateFormatFromInterval( + period: string | undefined, // Pass in undefined to use the default interval. + from: string, + to: string +): string { + const interval = getDefaultIntervalForPeriod( period, from, to ); + + switch ( interval ) { + case 'hour': + return 'HH:mm'; + case 'day': + case 'week': + return 'MMM d'; + case 'month': + return 'MMM yyyy'; + case 'quarter': + return 'qqq yyyy'; + case 'year': + return 'yyyy'; + default: + return 'MMM d'; + } +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts new file mode 100644 index 000000000000..3ff816683937 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts @@ -0,0 +1,15 @@ +/** + * Safe integer parsing with fallback value + */ +export function safeParseInt( value: unknown, fallback = 0 ): number { + const num = parseInt( String( value ), 10 ); + return isNaN( num ) ? fallback : num; +} + +/** + * Safe float parsing with fallback value + */ +export function safeParseFloat( value: unknown, fallback = 0 ): number { + const num = parseFloat( String( value ) ); + return isNaN( num ) ? fallback : num; +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts new file mode 100644 index 000000000000..6e0ad5b9c023 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { computePrimaryRange } from '@next-woo-analytics/datetime'; +import type { SelectablePresetId } from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date'; + +/** + * Compute the absolute date range for a given preset ID + * based on the current date and the site's timezone. + * + * Thin wrapper over datetime's computePrimaryRange that + * resolves the site timezone and converts Date -> ISO string. + * + * @param presetId - A valid selectable preset identifier. + * @return The computed { from, to } ISO strings, or undefined + * if the preset is not recognized. + */ +export function computeDateRangeFromPreset( + presetId: SelectablePresetId +): { from: string; to: string } | undefined { + const range = computePrimaryRange( presetId, getSiteTimezone() ); + if ( ! range?.from || ! range?.to ) { + return undefined; + } + + return { + from: dateToISOStringWithLocalTZ( range.from ), + to: dateToISOStringWithLocalTZ( range.to ), + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts new file mode 100644 index 000000000000..7c000b4ea339 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ +import type { FilterCondition } from '../types/filter-condition'; + +/** + * Product lookup table level filters. + */ +const PRODUCT_FILTER_KEYS = [ 'product_type', 'virtual', 'downloadable' ]; + +/** + * Checks if any of the provided filters are product-related filters + * + * @param filters - Array of filter conditions to check + * @return True if any filter is product-related, false otherwise + */ +export function hasProductFilters( filters?: FilterCondition[] ): boolean { + if ( ! filters || ! Array.isArray( filters ) || filters.length === 0 ) { + return false; + } + + return filters.some( ( filter ) => + PRODUCT_FILTER_KEYS.includes( filter.key ) + ); +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/search.ts b/projects/packages/premium-analytics/packages/data/src/utils/search.ts new file mode 100644 index 000000000000..12c2869640d6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import { + isSelectablePreset, + type SelectablePresetId, + type ComparisonPresetId, + type PrimaryPresetId, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { getDefaultQueryParams } from '../defaults'; +import { ORDER_ATTRIBUTION_VIEWS } from '../api/report-order-attribution-summary-fetch'; +import { getDefaultIntervalForPeriod } from './interval'; +import { computeDateRangeFromPreset } from './preset-date-range'; +import type { FilterCondition } from '../types/filter-condition'; +import type { DateType } from './types'; + +export type { FilterCondition }; + +/** + * Re-export SelectablePresetId as PresetType for backward compatibility. + * The canonical type now lives in @next-woo-analytics/datetime. + */ +export type PresetType = SelectablePresetId; + +type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ]; + +export type IntervalType = + | 'hour' + | 'day' + | 'week' + | 'month' + | 'quarter' + | 'year'; + +/* + * ReportParams are the expected params present in the client URL. + * They aren't meant to be the reports params + * of the API endpoint (RequestReportOrdersParams) + */ +export type ReportParams = { + from: string; + to: string; + preset?: PresetType; + interval: IntervalType; + period?: string; + compare_from?: string; + compare_to?: string; + compare_preset?: ComparisonPresetId; + comp?: '1'; + view?: OrderAttributionView; // For order attribution reports + filters?: FilterCondition[]; + section?: string; + date_type?: DateType; // For filtering by different date fields (created, paid, completed) +}; + +type PartialComparisonFields = Partial< + Pick< ReportParams, 'comp' | 'compare_from' | 'compare_to' > +>; + +/* + * Checks if the comparison is present in the search params. + */ +export function hasComparisonEnabled< T extends PartialComparisonFields >( + p: T +) { + return ( + p.comp === '1' && !! p.compare_from?.trim() && !! p.compare_to?.trim() + ); +} + +type NormalizeReportParamsArgType = Omit< + ReportParams, + 'from' | 'to' | 'interval' | 'preset' +> & { + from?: string; + to?: string; + interval?: string; + preset?: PrimaryPresetId; +}; + +/** + * Returns normalized params for the report request query. + * When no defined, it will use the defaults. + * + * @param {NormalizeReportParamsArgType} [search] URL search params. + * @param {PresetType} [defaultPreset] Override the fallback preset. + */ +export function normalizeReportParams( + search?: NormalizeReportParamsArgType, + defaultPreset?: PresetType +): ReportParams { + const defaults = defaultPreset + ? getDefaultQueryParams( true, defaultPreset ) + : getDefaultQueryParams( true ); + + // Preset handling: + // - Use search.preset only if valid + // - On fresh load (no from/to), fallback to defaults.preset + // - If user has explicit dates but no/invalid preset, + // keep undefined (custom range) + let preset: PresetType | undefined; + if ( search?.preset && isSelectablePreset( search.preset ) ) { + preset = search.preset; + } else if ( ! search?.from && ! search?.to ) { + preset = defaults.preset; + } + + // When a valid preset is present, recalculate from/to + // so rolling ranges like "Last 30 days" stay fresh + // on every page load instead of using stale URL dates. + // If the preset is valid but has no range implementation, + // clear it to avoid silently falling back to stale dates. + let presetRange: ReturnType< typeof computeDateRangeFromPreset >; + if ( preset ) { + presetRange = computeDateRangeFromPreset( preset ); + if ( ! presetRange ) { + preset = undefined; + } + } + + const from = presetRange?.from ?? search?.from ?? defaults.from; + const to = presetRange?.to ?? search?.to ?? defaults.to; + + // Calculate the interval from the resolved date range. + const interval = getDefaultIntervalForPeriod( undefined, from, to ); + + // Params from `search`, or fallback to defaults. + const normalized: ReportParams = { + from, + to, + interval: interval ?? defaults.interval, + preset, + date_type: search?.date_type ?? 'created', + }; + + // Add comparison params from search if enabled + if ( search && hasComparisonEnabled( search ) ) { + normalized.compare_from = search.compare_from; + normalized.compare_to = search.compare_to; + normalized.compare_preset = search.compare_preset; + normalized.comp = '1'; + } else if ( ! search?.from && hasComparisonEnabled( defaults ) ) { + // Fresh load (missing primary params) - apply default comparison + normalized.compare_from = defaults.compare_from; + normalized.compare_to = defaults.compare_to; + normalized.compare_preset = defaults.compare_preset; + normalized.comp = '1'; + } + + return normalized; +} diff --git a/projects/packages/premium-analytics/packages/data/src/utils/types.ts b/projects/packages/premium-analytics/packages/data/src/utils/types.ts new file mode 100644 index 000000000000..35f200844148 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/types.ts @@ -0,0 +1,29 @@ +/** + * Utility type to override properties of a type. + * Useful for transforming API responses where some properties change type. + * + * @example + * type Raw = { count: string; name: string; } + * type Processed = Override< Raw, { count: number } > + * // Result: { count: number; name: string; } + */ +export type Override< T, U > = Omit< T, keyof U > & U; + +/** + * Date type parameter for filtering reports by different date fields. + * - 'created': Filter by order creation date (date_created_gmt) + * - 'paid': Filter by order payment date (date_paid_gmt) + * - 'completed': Filter by order completion date (date_completed_gmt) + */ +export type DateType = 'created' | 'paid' | 'completed'; + +/** + * Base parameters required by all report endpoints. + * These three parameters are common across all analytics reports. + */ +export type BaseReportParams = { + from: string; + to: string; + interval: string; + date_type?: DateType; +}; diff --git a/projects/packages/premium-analytics/packages/data/tsconfig.json b/projects/packages/premium-analytics/packages/data/tsconfig.json new file mode 100644 index 000000000000..3d73602e2edb --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "declaration": true, + "declarationDir": "build" + }, + "include": ["src/**/*"] +} \ No newline at end of file From 2ebc240fa2981e73b098afd918ce5cb7fe9d8272 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:04:12 +0800 Subject: [PATCH 02/32] refactor(premium-analytics): adapt data package imports and manifest for monorepo --- .../premium-analytics/packages/data/README.md | 24 ++++++++------- .../packages/data/package.json | 30 +++++++++---------- .../report-products-fetch.ts | 2 +- .../packages/data/src/defaults/reports.ts | 2 +- .../src/processing/conversion-rate/index.ts | 8 ++--- .../packages/data/src/utils/date.ts | 2 +- .../packages/data/src/utils/index.ts | 2 +- .../data/src/utils/preset-date-range.ts | 4 +-- .../packages/data/src/utils/search.ts | 4 +-- .../packages/data/tsconfig.json | 9 ------ 10 files changed, 40 insertions(+), 47 deletions(-) delete mode 100644 projects/packages/premium-analytics/packages/data/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/data/README.md b/projects/packages/premium-analytics/packages/data/README.md index 3c6f603476e2..7db93ce258ff 100644 --- a/projects/packages/premium-analytics/packages/data/README.md +++ b/projects/packages/premium-analytics/packages/data/README.md @@ -1,11 +1,13 @@ -# @next-woo-analytics/data +# @jetpack-premium-analytics/data -Data management package for WooCommerce Analytics with React Query -integration. +Data management for Jetpack Premium Analytics with React Query integration. ## Installation -This package is an internal dependency of the WooCommerce Analytics NextAdmin integration. It's automatically available when working within the NextAdmin framework. +This is an internal package of Jetpack Premium Analytics — it is never +published to npm and is resolved entirely in-tree. It's automatically +available to routes and other internal packages within +`@automattic/jetpack-premium-analytics`. ```tsx import { @@ -13,7 +15,7 @@ import { useReport, prefetchReport, // ... other exports -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; ``` ## Features @@ -34,7 +36,7 @@ import { ### Setup ```tsx -import { AnalyticsQueryClientProvider } from '@next-woo-analytics/data'; +import { AnalyticsQueryClientProvider } from '@jetpack-premium-analytics/data'; function App() { return ( @@ -53,7 +55,7 @@ import { useReportOrdersByProductType, useReportOrderAttribution, useReportCoupons -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; function OrdersReport() { // Orders endpoint separates primary and comparison periods @@ -103,7 +105,7 @@ function CouponsReport() { ### Prefetching ```tsx -import { prefetchReport, ensureCoreSettingsReady } from '@next-woo-analytics/data'; +import { prefetchReport, ensureCoreSettingsReady } from '@jetpack-premium-analytics/data'; export const route = { beforeLoad: async () => { @@ -239,7 +241,7 @@ Returns the optimal default interval for a given time period. **Example:** ```tsx -import { getDefaultIntervalForPeriod } from '@next-woo-analytics/data'; +import { getDefaultIntervalForPeriod } from '@jetpack-premium-analytics/data'; const interval = getDefaultIntervalForPeriod( 'last-7-days', from, to ); // Returns 'day' ``` @@ -252,7 +254,7 @@ Constant array of available order attribution views. **Example:** ```tsx -import { ORDER_ATTRIBUTION_VIEWS } from '@next-woo-analytics/data'; +import { ORDER_ATTRIBUTION_VIEWS } from '@jetpack-premium-analytics/data'; // Use in components for view selection const views = ORDER_ATTRIBUTION_VIEWS; // ['channel', 'source', ...] @@ -331,7 +333,7 @@ Creates a timezone-aware date using the site's configured timezone by default. ```typescript -import { localTZDate } from '@next-woo-analytics/data'; +import { localTZDate } from '@jetpack-premium-analytics/data'; const now = localTZDate(); // Current time in site timezone const custom = localTZDate( '2024-01-15', 'America/New_York' ); diff --git a/projects/packages/premium-analytics/packages/data/package.json b/projects/packages/premium-analytics/packages/data/package.json index cae8d1032019..74470af6fbd8 100644 --- a/projects/packages/premium-analytics/packages/data/package.json +++ b/projects/packages/premium-analytics/packages/data/package.json @@ -1,23 +1,23 @@ { - "name": "@next-woo-analytics/data", - "version": "1.0.0", + "name": "@automattic/jetpack-premium-analytics-data", + "version": "0.1.0", + "private": true, "type": "module", - "wpModule": true, "main": "src/index.ts", - "exports": { - ".": "./build/src/index.js" - }, + "types": "src/index.ts", + "sideEffects": false, "dependencies": { - "date-fns": "*", - "@date-fns/tz": "*", - "@tanstack/react-query": "*", - "@tanstack/react-router": "*", - "@wordpress/api-fetch": "*", - "@wordpress/url": "*", - "@next-woo-analytics/datetime": "workspace:*", - "@automattic/admin-toolkit": "*" + "@date-fns/tz": "1.4.1", + "@jetpack-premium-analytics/datetime": "workspace:*", + "@tanstack/react-query": "5.90.8", + "@wordpress/api-fetch": "7.46.0", + "@wordpress/core-data": "7.46.0", + "@wordpress/data": "10.46.0", + "@wordpress/i18n": "^6.9.0", + "@wordpress/url": "4.46.0", + "date-fns": "4.1.0" }, "devDependencies": { - "@tanstack/react-query-devtools": "*" + "@tanstack/react-query-devtools": "5.90.2" } } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts index 30b14661cde2..d05c09bae09d 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts @@ -3,13 +3,13 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import type { FilterCondition } from '@next-woo-analytics/data'; /** * Internal dependencies */ import { reportsPath } from '../constants'; import { BaseReportParams } from '../../utils/types'; +import type { FilterCondition } from '../../types/filter-condition'; export type RequestReportProductsParams = Omit< BaseReportParams, diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts index 12ad30a3b7a6..8ea911e54ec6 100644 --- a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts +++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { getComparisonRangeFromPreset } from '@next-woo-analytics/datetime'; +import { getComparisonRangeFromPreset } from '@jetpack-premium-analytics/datetime'; import { differenceInCalendarDays, startOfDay } from 'date-fns'; /** diff --git a/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts index 8f715a627702..3e33c27c69af 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts @@ -105,13 +105,13 @@ export const sanitizeReportConversionRateResponse = ( const steps: FunnelStep[] = [ { id: 'sessions', - label: __( 'Sessions', 'woocommerce-analytics' ), + label: __( 'Sessions', 'jetpack-premium-analytics' ), count: sanitizedSummary.active_sessions, rate: 100, // Starting point }, { id: 'cart-addition', - label: __( 'Cart', 'woocommerce-analytics' ), + label: __( 'Cart', 'jetpack-premium-analytics' ), count: sanitizedSummary.with_cart_addition, rate: sanitizedSummary.active_sessions > 0 @@ -122,7 +122,7 @@ export const sanitizeReportConversionRateResponse = ( }, { id: 'checkout', - label: __( 'Checkout', 'woocommerce-analytics' ), + label: __( 'Checkout', 'jetpack-premium-analytics' ), count: sanitizedSummary.reached_checkout, rate: sanitizedSummary.active_sessions > 0 @@ -133,7 +133,7 @@ export const sanitizeReportConversionRateResponse = ( }, { id: 'completed', - label: __( 'Purchase', 'woocommerce-analytics' ), + label: __( 'Purchase', 'jetpack-premium-analytics' ), count: sanitizedSummary.completed_checkout, rate: sanitizedSummary.active_sessions > 0 diff --git a/projects/packages/premium-analytics/packages/data/src/utils/date.ts b/projects/packages/premium-analytics/packages/data/src/utils/date.ts index 41f385c01ab7..011e8ae2b768 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/date.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts @@ -7,7 +7,7 @@ import { toLocalTZ, formatToTimezoneNaiveString as _formatNaive, dateToISOStringWithTZ as _toISOWithTZ, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; import { type TZDate } from '@date-fns/tz'; type FullSettings = Settings & { diff --git a/projects/packages/premium-analytics/packages/data/src/utils/index.ts b/projects/packages/premium-analytics/packages/data/src/utils/index.ts index 1883dee36011..0a0763050244 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/index.ts @@ -11,5 +11,5 @@ export { safeParseInt, safeParseFloat } from './parsing'; export { computeDateRangeFromPreset } from './preset-date-range'; export { hasProductFilters } from './product-filters'; export type { PresetType, ReportParams } from './search'; -export { isSelectablePreset } from '@next-woo-analytics/datetime'; +export { isSelectablePreset } from '@jetpack-premium-analytics/datetime'; export type { Override, BaseReportParams } from './types'; diff --git a/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts index 6e0ad5b9c023..d3695405408c 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts @@ -1,8 +1,8 @@ /** * External dependencies */ -import { computePrimaryRange } from '@next-woo-analytics/datetime'; -import type { SelectablePresetId } from '@next-woo-analytics/datetime'; +import { computePrimaryRange } from '@jetpack-premium-analytics/datetime'; +import type { SelectablePresetId } from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/data/src/utils/search.ts b/projects/packages/premium-analytics/packages/data/src/utils/search.ts index 12c2869640d6..4d01bd951f09 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/search.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts @@ -6,7 +6,7 @@ import { type SelectablePresetId, type ComparisonPresetId, type PrimaryPresetId, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -22,7 +22,7 @@ export type { FilterCondition }; /** * Re-export SelectablePresetId as PresetType for backward compatibility. - * The canonical type now lives in @next-woo-analytics/datetime. + * The canonical type now lives in @jetpack-premium-analytics/datetime. */ export type PresetType = SelectablePresetId; diff --git a/projects/packages/premium-analytics/packages/data/tsconfig.json b/projects/packages/premium-analytics/packages/data/tsconfig.json deleted file mode 100644 index 3d73602e2edb..000000000000 --- a/projects/packages/premium-analytics/packages/data/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "build", - "declaration": true, - "declarationDir": "build" - }, - "include": ["src/**/*"] -} \ No newline at end of file From c660f8f9824995f3bd5b4ee98c885da673480eec Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:04:48 +0800 Subject: [PATCH 03/32] refactor(premium-analytics): decouple data package from admin-toolkit experiments --- .../src/providers/query-client-provider.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx index 273020c75201..c6ddf592efbc 100644 --- a/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx +++ b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx @@ -6,7 +6,6 @@ import { QueryClientProvider, QueryCache, } from '@tanstack/react-query'; -import { useExperiments } from '@automattic/admin-toolkit'; import { ReactNode, lazy, Suspense } from 'react'; /** @@ -17,6 +16,28 @@ import { globalErrorManager } from './global-error-manager'; const DEFAULT_STALE_TIME = 5 * 60 * 1000; const DEFAULT_GC_TIME = 10 * 60 * 1000; +/** + * Whether to render the React Query Devtools. + * + * Devtools are opt-in and OFF by default. Enable them by setting a global + * debug flag on `window` (e.g. in the browser console: + * `window.jetpackPremiumAnalyticsQueryDevtools = true`) and reloading. They + * are also enabled automatically outside of production builds. + * + * @return Whether the devtools should be rendered. + */ +function areQueryDevtoolsEnabled(): boolean { + if ( + typeof window !== 'undefined' && + ( window as { jetpackPremiumAnalyticsQueryDevtools?: boolean } ) + .jetpackPremiumAnalyticsQueryDevtools === true + ) { + return true; + } + + return process.env.NODE_ENV !== 'production'; +} + const ReactQueryDevtoolsProduction = lazy( () => // eslint-disable-next-line import/no-extraneous-dependencies -- DevTools is intentionally in devDependencies, only loaded when enabled import( '@tanstack/react-query-devtools/production' ).then( ( d ) => ( { @@ -137,11 +158,10 @@ export const AnalyticsQueryClientProvider = ( { }: { children: ReactNode; } ) => { - const { enabledExperiments } = useExperiments(); return ( <>{ children } - { enabledExperiments[ 'tanstack/query-dev-tool' ] && ( + { areQueryDevtoolsEnabled() && ( From 6219abe2620683c0a00797d721466cdb6c2b5140 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:16:16 +0800 Subject: [PATCH 04/32] style(premium-analytics): align ported data package with jetpack lint and prettier --- .../packages/data/src/api/index.ts | 19 ++--- .../report-bookings-fetch.ts | 12 ++- .../report-conversion-rate-fetch.ts | 13 ++- .../report-coupons-by-date-fetch.ts | 12 ++- .../src/api/report-coupons-fetch/index.ts | 5 +- .../report-coupons-fetch.ts | 12 ++- .../report-customers-by-date-fetch.ts | 11 ++- .../src/api/report-customers-fetch/index.ts | 5 +- .../report-customers-fetch.ts | 18 ++-- .../data/src/api/report-export-fetch/index.ts | 5 +- .../report-export-fetch.ts | 10 +-- ...port-order-attribution-by-product-fetch.ts | 3 +- .../report-order-attribution-summary-fetch.ts | 11 +-- .../report-orders-fetch.ts | 14 +++- .../src/api/report-products-fetch/index.ts | 5 +- .../report-products-fetch.ts | 9 +- .../report-sessions-by-device-fetch.ts | 8 +- .../report-visitors-by-location-fetch.ts | 10 ++- .../src/api/report-visitors-fetch/index.ts | 5 +- .../report-visitors-fetch.ts | 10 ++- .../__tests__/get-default-preset.test.ts | 29 ++----- .../packages/data/src/defaults/reports.ts | 10 +-- .../data/src/hooks/use-product-images.ts | 8 +- .../data/src/hooks/use-report-bookings.ts | 14 ++-- .../src/hooks/use-report-conversion-rate.ts | 14 ++-- .../src/hooks/use-report-coupons-by-date.ts | 13 ++- .../data/src/hooks/use-report-coupons.ts | 13 ++- .../src/hooks/use-report-customers-by-date.ts | 15 ++-- .../data/src/hooks/use-report-customers.ts | 6 +- .../src/hooks/use-report-order-attribution.ts | 12 +-- .../data/src/hooks/use-report-orders.ts | 20 ++--- .../data/src/hooks/use-report-products.ts | 14 ++-- .../hooks/use-report-sessions-by-device.ts | 10 +-- .../hooks/use-report-visitors-by-location.ts | 15 ++-- .../data/src/hooks/use-report-visitors.ts | 20 ++--- .../packages/data/src/hooks/use-report.ts | 12 +-- .../packages/data/src/index.ts | 20 +---- .../data/src/prefetch/prefetch-report.ts | 53 ++++-------- .../data/src/processing/bookings/index.ts | 40 +++------ .../src/processing/conversion-rate/index.ts | 31 +++---- .../src/processing/coupons-by-date/index.ts | 17 ++-- .../data/src/processing/coupons/index.ts | 15 ++-- .../src/processing/customers-by-date/index.ts | 68 +++++---------- .../data/src/processing/customers/index.ts | 17 ++-- ...e-order-attribution-by-product-response.ts | 28 +++---- ...tize-order-attribution-summary-response.ts | 5 +- .../orders-by-product-type/index.ts | 4 +- .../data/src/processing/orders/index.ts | 12 ++- .../data/src/processing/products/index.ts | 18 ++-- .../processing/sessions-by-device/index.ts | 18 ++-- .../processing/visitors-by-location/index.ts | 23 +++--- .../data/src/processing/visitors/index.ts | 13 ++- .../src/providers/global-error-context.tsx | 20 ++--- .../src/providers/global-error-manager.ts | 2 +- .../packages/data/src/providers/index.ts | 10 +-- .../src/providers/query-client-provider.tsx | 19 ++--- .../data/src/queries/report-bookings-query.ts | 24 ++---- .../queries/report-conversion-rate-query.ts | 27 +++--- .../queries/report-coupons-by-date-query.ts | 25 +++--- .../data/src/queries/report-coupons-query.ts | 25 +++--- .../queries/report-customers-by-date-query.ts | 27 +++--- .../src/queries/report-customers-query.ts | 13 +-- .../report-order-attribution-summary-query.ts | 82 ++++++++----------- .../data/src/queries/report-orders-query.ts | 9 +- .../data/src/queries/report-products-query.ts | 17 ++-- .../report-sessions-by-device-query.ts | 14 ++-- .../report-visitors-by-location-query.ts | 12 +-- .../data/src/queries/report-visitors-query.ts | 23 ++---- .../packages/data/src/types.ts | 53 ++++-------- .../compute-date-range-from-preset.test.ts | 32 +++----- .../__tests__/has-comparison-enabled.test.ts | 3 +- .../__tests__/normalize-report-params.test.ts | 9 +- .../packages/data/src/utils/date.ts | 54 ++++++------ .../data/src/utils/ensure-core-settings.ts | 8 +- .../packages/data/src/utils/interval.ts | 32 ++++++-- .../packages/data/src/utils/parsing.ts | 4 + .../data/src/utils/preset-date-range.ts | 3 +- .../data/src/utils/product-filters.ts | 4 +- .../packages/data/src/utils/search.ts | 36 +++----- 79 files changed, 584 insertions(+), 807 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/api/index.ts b/projects/packages/premium-analytics/packages/data/src/api/index.ts index 6b8a8be840b6..771d51ddba7b 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts @@ -1,17 +1,17 @@ /** * Internal dependencies */ -import type { RequestReportOrdersParams } from './report-orders-fetch'; -import type { RequestReportOrderAttributionSummaryParams } from './report-order-attribution-summary-fetch'; -import type { RequestReportOrderAttributionByProductParams } from './report-order-attribution-by-product-fetch'; -import type { RequestReportCouponsParams } from './report-coupons-fetch'; +import type { RequestReportBookingsParams } from './report-bookings-fetch'; import type { RequestReportCouponsByDateParams } from './report-coupons-by-date-fetch'; +import type { RequestReportCouponsParams } from './report-coupons-fetch'; import type { RequestReportCustomersParams } from './report-customers-fetch'; +import type { RequestReportOrderAttributionByProductParams } from './report-order-attribution-by-product-fetch'; +import type { RequestReportOrderAttributionSummaryParams } from './report-order-attribution-summary-fetch'; +import type { RequestReportOrdersParams } from './report-orders-fetch'; import type { RequestReportProductsParams } from './report-products-fetch'; -import type { RequestReportVisitorsParams } from './report-visitors-fetch'; -import type { RequestReportVisitorsByLocationParams } from './report-visitors-by-location-fetch'; -import type { RequestReportBookingsParams } from './report-bookings-fetch'; import type { RequestReportSessionsByDeviceParams } from './report-sessions-by-device-fetch'; +import type { RequestReportVisitorsByLocationParams } from './report-visitors-by-location-fetch'; +import type { RequestReportVisitorsParams } from './report-visitors-fetch'; export type ReportQueryParams = Partial< RequestReportOrdersParams & @@ -42,7 +42,4 @@ export { fetchReportVisitorsByLocation } from './report-visitors-by-location-fet export { fetchReportBookings } from './report-bookings-fetch'; export { fetchReportSessionsByDevice } from './report-sessions-by-device-fetch'; export { exportReport } from './report-export-fetch'; -export type { - ExportReportParams, - ExportReportResponse, -} from './report-export-fetch'; +export type { ExportReportParams, ExportReportResponse } from './report-export-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts index f85b19d565df..c06c8fa36fa2 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type ReportsBookingsByDateSummary = { status_unpaid: string; @@ -38,6 +37,15 @@ export type RequestReportBookingsParams = BaseReportParams & { filters?: FilterCondition[]; }; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.filters + * @param root0.date_type + */ export async function fetchReportBookings( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts index eb6a2aa2e8ed..eaea267f5bdc 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; -import type { FilterCondition } from '../../types/filter-condition'; import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type ReportsConversionRateByDateSummary = { active_sessions: string; @@ -40,6 +39,14 @@ export type RequestReportConversionRateParams = BaseReportParams & { filters?: FilterCondition[]; }; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.filters + */ export async function fetchReportConversionRate( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts index 0115d56b0ce6..d58602ec6645 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type CouponsByDateDataItem = { time_interval: string; @@ -49,6 +48,15 @@ export type RequestReportCouponsByDateParams = BaseReportParams & { filters?: FilterCondition[]; }; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.filters + * @param root0.date_type + */ export async function fetchReportCouponsByDate( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts index 4c4e60f45fd0..54bbee440ef2 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts @@ -1,4 +1 @@ -export { - fetchReportCoupons, - type RequestReportCouponsParams, -} from './report-coupons-fetch'; +export { fetchReportCoupons, type RequestReportCouponsParams } from './report-coupons-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts index d591c37b1e22..c25dc9dd29c0 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type CouponsDataItem = { coupon_code: string; @@ -35,6 +34,15 @@ export type RequestReportCouponsParams = BaseReportParams & { filters?: FilterCondition[]; }; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.filters + * @param root0.date_type + */ export async function fetchReportCoupons( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts index 6e6c6a60e81d..9d9e83b66d5d 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; type ReportsCustomersByDateSummary = { total_net_sales: string; @@ -61,6 +60,14 @@ type ReportsCustomersByDateResponse = { export type RequestReportCustomersByDateParams = BaseReportParams; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.date_type + */ export async function fetchReportCustomersByDate( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts index aca7bab2d69d..b5d7c2f32fe4 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts @@ -1,4 +1 @@ -export { - fetchReportCustomers, - type RequestReportCustomersParams, -} from './report-customers-fetch'; +export { fetchReportCustomers, type RequestReportCustomersParams } from './report-customers-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts index 35cdd79456dc..1747d7c8cdb4 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; -import type { FilterCondition } from '../../types/filter-condition'; import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type CustomersNewReturningSummary = { total_net_sales: string; @@ -31,13 +30,18 @@ type ReportsCustomersNewReturningResponse = { data: CustomersNewReturningItem[]; }; -export type RequestReportCustomersParams = Omit< - BaseReportParams, - 'interval' -> & { +export type RequestReportCustomersParams = Omit< BaseReportParams, 'interval' > & { filters?: FilterCondition[]; }; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.filters + * @param root0.date_type + */ export async function fetchReportCustomers( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts index c59fd9485cc6..4b36b16295d5 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts @@ -1,5 +1,2 @@ export { exportReport } from './report-export-fetch'; -export type { - ExportReportParams, - ExportReportResponse, -} from './report-export-fetch'; +export type { ExportReportParams, ExportReportResponse } from './report-export-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts index 5edc5de7f4c7..645bdff72b3f 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts @@ -29,18 +29,14 @@ export interface ExportReportResponse { /** * Export one or more reports via email * - * @param params Export parameters + * @param params - Export parameters * @return Promise that resolves to the export response */ -export async function exportReport( - params: ExportReportParams -): Promise< ExportReportResponse > { +export async function exportReport( params: ExportReportParams ): Promise< ExportReportResponse > { const path = '/wc/v3/woocommerce-analytics/reports/csv-export'; const body = { - report_type: Array.isArray( params.reportType ) - ? params.reportType - : [ params.reportType ], + report_type: Array.isArray( params.reportType ) ? params.reportType : [ params.reportType ], from: params.from, to: params.to, interval: params.interval || 'day', diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts index 5e2489eb23fa..7e35aff2130e 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; import type { ORDER_ATTRIBUTION_VIEWS } from '../report-order-attribution-summary-fetch/report-order-attribution-summary-fetch'; type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ]; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts index 1ee8349c738f..6c62c763f539 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; export const ORDER_ATTRIBUTION_VIEWS = [ 'channel', @@ -64,8 +63,7 @@ export type RequestReportOrderAttributionSummaryParams = BaseReportParams & { export async function fetchReportOrderAttributionSummary( params: RequestReportOrderAttributionSummaryParams ): Promise< OrderAttributionSummaryResponse > { - const { from, to, interval, view, compare_from, compare_to, date_type } = - params; + const { from, to, interval, view, compare_from, compare_to, date_type } = params; /* * Order attribution endpoint requires compare_from and compare_to. @@ -81,10 +79,7 @@ export async function fetchReportOrderAttributionSummary( date_type, }; - const path = addQueryArgs( - `${ reportsPath }/order-attribution/${ view }/summary`, - queryParams - ); + const path = addQueryArgs( `${ reportsPath }/order-attribution/${ view }/summary`, queryParams ); return apiFetch< OrderAttributionSummaryResponse >( { path } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts index 452fd2b53371..c97e7f3d8aa9 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts @@ -3,14 +3,13 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; +import { hasProductFilters } from '../../utils/product-filters'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; -import { hasProductFilters } from '../../utils/product-filters'; +import type { BaseReportParams } from '../../utils/types'; type ReportsOrdersByDateSummary = { average_order_value: string; @@ -45,6 +44,15 @@ export type RequestReportOrdersParams = BaseReportParams & { filters?: FilterCondition[]; }; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.filters + * @param root0.date_type + */ export async function fetchReportOrders( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts index d1d82b303a52..c786309418cf 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts @@ -1,7 +1,4 @@ /** * Internal dependencies */ -export { - fetchReportProducts, - type RequestReportProductsParams, -} from './report-products-fetch'; +export { fetchReportProducts, type RequestReportProductsParams } from './report-products-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts index d05c09bae09d..8af88655042d 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts @@ -3,18 +3,14 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import { reportsPath } from '../constants'; import { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; -export type RequestReportProductsParams = Omit< - BaseReportParams, - 'interval' -> & { +export type RequestReportProductsParams = Omit< BaseReportParams, 'interval' > & { limit?: number; orderby?: string; order?: 'asc' | 'desc'; @@ -43,6 +39,7 @@ type ReportProductsResponse = { /** * Fetches products report data from the WooCommerce Analytics API + * @param params */ export async function fetchReportProducts( params: RequestReportProductsParams diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts index 4c28378487a5..3909742411dc 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; /** * Raw response item from the sessions/by-device endpoint. @@ -36,10 +35,7 @@ type ReportsSessionsByDeviceResponse = { data: SessionsByDeviceItem[]; }; -export type RequestReportSessionsByDeviceParams = Omit< - BaseReportParams, - 'interval' ->; +export type RequestReportSessionsByDeviceParams = Omit< BaseReportParams, 'interval' >; /** * Fetch sessions by device type report data. diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts index 43b5f05a1d22..bbb9c7fa6193 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; type VisitorsByLocationReportDataItem = { country_code: string; @@ -39,6 +38,13 @@ export type RequestReportVisitorsByLocationParams = BaseReportParams & { * * This endpoint is proxied through `/wc/v3/woocommerce-analytics/proxy/reports/...` * and ultimately served by wpcom analytics. + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + * @param root0.group_by + * @param root0.country_code + * @param root0.limit */ export async function fetchReportVisitorsByLocation( { from, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts index 3a17020aae0c..164ef5351eb7 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts @@ -1,4 +1 @@ -export { - fetchReportVisitors, - type RequestReportVisitorsParams, -} from './report-visitors-fetch'; +export { fetchReportVisitors, type RequestReportVisitorsParams } from './report-visitors-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts index dcf58139007c..93895fca50cd 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; type ReportsVisitorsByDateSummary = { active_sessions: string; @@ -28,6 +27,13 @@ type ReportsVisitorsByDateResponse = { export type RequestReportVisitorsParams = BaseReportParams; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + */ export async function fetchReportVisitors( { from, to, diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts index 5c39f60b76ce..ab6da5ee50fb 100644 --- a/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts @@ -16,7 +16,6 @@ jest.mock( '@wordpress/data', () => ( { jest.mock( '../../utils/ensure-core-settings', () => ( { ensureCoreSettingsReady: jest.fn( () => Promise.resolve() ), } ) ); - /** * Internal dependencies */ @@ -37,21 +36,15 @@ describe( 'getDefaultQueryParams - preset override', () => { } ); it( 'uses today preset when passed', () => { - expect( getDefaultQueryParams( false, 'today' ).preset ).toBe( - 'today' - ); + expect( getDefaultQueryParams( false, 'today' ).preset ).toBe( 'today' ); } ); it( 'uses last-7-days preset when passed', () => { - expect( getDefaultQueryParams( false, 'last-7-days' ).preset ).toBe( - 'last-7-days' - ); + expect( getDefaultQueryParams( false, 'last-7-days' ).preset ).toBe( 'last-7-days' ); } ); it( 'uses last-30-days preset when passed', () => { - expect( getDefaultQueryParams( false, 'last-30-days' ).preset ).toBe( - 'last-30-days' - ); + expect( getDefaultQueryParams( false, 'last-30-days' ).preset ).toBe( 'last-30-days' ); } ); } ); @@ -78,27 +71,19 @@ describe( 'getDefaultPreset', () => { } ); it( 'returns last-7-days when launched 3 days ago', () => { - expect( getDefaultPreset( '2025-03-12T00:00:00Z' ) ).toBe( - 'last-7-days' - ); + expect( getDefaultPreset( '2025-03-12T00:00:00Z' ) ).toBe( 'last-7-days' ); } ); it( 'returns last-7-days when launched exactly 7 days ago', () => { - expect( getDefaultPreset( '2025-03-08T00:00:00Z' ) ).toBe( - 'last-7-days' - ); + expect( getDefaultPreset( '2025-03-08T00:00:00Z' ) ).toBe( 'last-7-days' ); } ); it( 'returns last-30-days when launched 8 days ago', () => { - expect( getDefaultPreset( '2025-03-07T00:00:00Z' ) ).toBe( - 'last-30-days' - ); + expect( getDefaultPreset( '2025-03-07T00:00:00Z' ) ).toBe( 'last-30-days' ); } ); it( 'returns last-30-days when launched months ago', () => { - expect( getDefaultPreset( '2024-01-01T00:00:00Z' ) ).toBe( - 'last-30-days' - ); + expect( getDefaultPreset( '2024-01-01T00:00:00Z' ) ).toBe( 'last-30-days' ); } ); it( 'returns today when launched in the future', () => { diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts index 8ea911e54ec6..76813df11046 100644 --- a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts +++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts @@ -3,7 +3,6 @@ */ import { getComparisonRangeFromPreset } from '@jetpack-premium-analytics/datetime'; import { differenceInCalendarDays, startOfDay } from 'date-fns'; - /** * Internal dependencies */ @@ -26,6 +25,7 @@ const DEFAULT_PRESET: PresetType = 'last-30-days'; * - Launched today → today * - Launched ≤ 7 days ago → last-7-days * - Launched > 7 days ago → last-30-days + * @param launchedDate */ export function getDefaultPreset( launchedDate?: string ): PresetType { if ( ! launchedDate ) { @@ -53,6 +53,8 @@ export function getDefaultPreset( launchedDate?: string ): PresetType { * * Callers that need a dynamic default (e.g. based on store * age) should resolve the preset externally and pass it in. + * @param withComparison + * @param preset */ export const getDefaultQueryParams = ( /** @@ -73,11 +75,7 @@ export const getDefaultQueryParams = ( const { from: fromString, to: toString } = range; - const interval = getDefaultIntervalForPeriod( - undefined, - fromString, - toString - ); + const interval = getDefaultIntervalForPeriod( undefined, fromString, toString ); if ( ! withComparison ) { return { diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts index 5c5b1649f419..4c76b44e3610 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts @@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ @@ -53,7 +52,7 @@ async function fetchProductImages( path: addQueryArgs( '/wc/v3/products', queryArgs ), } ); - return response.map( ( product ) => ( { + return response.map( product => ( { productId: product.id, imageUrl: product.images?.[ 0 ]?.src || '', imageAlt: product.images?.[ 0 ]?.alt || product.name, @@ -76,10 +75,7 @@ export function useProductImages( params: UseProductImagesParams ) { queryFn: async () => { const images = await fetchProductImages( params.productIds ); return images.reduce( - ( - acc: Record< number, ProductImage >, - image: ProductImage & { productId: number } - ) => { + ( acc: Record< number, ProductImage >, image: ProductImage & { productId: number } ) => { acc[ image.productId ] = { imageUrl: image.imageUrl, imageAlt: image.imageAlt, diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts index fc91af38a3fb..7c50d401510d 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts @@ -5,14 +5,12 @@ import { reportBookingsQuery } from '../queries'; import { type ReportParams } from '../utils/search'; import { useReport } from './use-report'; +/** + * + * @param params + */ export function useReportBookings( params: ReportParams ) { - return useReport( ( p ) => reportBookingsQuery( p ), params, { - disabledComparisonKey: [ - 'reports', - 'bookings', - 'by-date', - '__comparison__', - 'disabled', - ], + return useReport( p => reportBookingsQuery( p ), params, { + disabledComparisonKey: [ 'reports', 'bookings', 'by-date', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts index 9bc2d545d60c..b27ec3f9360f 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts @@ -9,17 +9,17 @@ type UseReportConversionRateOptions = { enabled?: boolean; }; +/** + * + * @param params + * @param options + */ export function useReportConversionRate( params: ReportParams, options?: UseReportConversionRateOptions ) { - return useReport( ( p ) => reportConversionRateQuery( p ), params, { + return useReport( p => reportConversionRateQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'conversion-rate', - '__comparison__', - 'disabled', - ], + disabledComparisonKey: [ 'reports', 'conversion-rate', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts index 6ba9daa58d92..a88b7a7b282b 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts @@ -5,13 +5,12 @@ import { reportCouponsByDateQuery } from '../queries'; import { type ReportParams } from '../utils/search'; import { useReport } from './use-report'; +/** + * + * @param params + */ export function useReportCouponsByDate( params: ReportParams ) { - return useReport( ( p ) => reportCouponsByDateQuery( p ), params, { - disabledComparisonKey: [ - 'reports', - 'couponsByDate', - '__comparison__', - 'disabled', - ], + return useReport( p => reportCouponsByDateQuery( p ), params, { + disabledComparisonKey: [ 'reports', 'couponsByDate', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts index 2b2fb11a3617..205e0d4780af 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts @@ -5,13 +5,12 @@ import { reportCouponsQuery } from '../queries'; import { type ReportParams } from '../utils/search'; import { useReport } from './use-report'; +/** + * + * @param params + */ export function useReportCoupons( params: ReportParams ) { - return useReport( ( p ) => reportCouponsQuery( p ), params, { - disabledComparisonKey: [ - 'reports', - 'coupons', - '__comparison__', - 'disabled', - ], + return useReport( p => reportCouponsQuery( p ), params, { + disabledComparisonKey: [ 'reports', 'coupons', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts index 1ddd8c538af0..08217edd8423 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts @@ -9,18 +9,17 @@ type UseReportCustomersByDateOptions = { enabled?: boolean; }; +/** + * + * @param params + * @param options + */ export function useReportCustomersByDate( params: ReportParams, options?: UseReportCustomersByDateOptions ) { - return useReport( ( p ) => reportCustomersByDateQuery( p ), params, { + return useReport( p => reportCustomersByDateQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'customers', - 'by-date', - '__comparison__', - 'disabled', - ], + disabledComparisonKey: [ 'reports', 'customers', 'by-date', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts index 3f4a7a2aee6e..2fa65e6724ec 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts @@ -5,8 +5,12 @@ import { reportCustomersQuery } from '../queries'; import { type ReportParams } from '../utils/search'; import { useReport } from './use-report'; +/** + * + * @param params + */ export function useReportCustomers( params: ReportParams ) { - return useReport( ( p ) => reportCustomersQuery( p ), params, { + return useReport( p => reportCustomersQuery( p ), params, { disabledComparisonKey: [ 'reports', 'customers', diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts index 7bf18f3e341e..6377f6e68735 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts @@ -16,6 +16,11 @@ const DISABLED_COMPARISON_KEY = [ 'included-in-primary', ]; +/** + * + * @param params + * @param options + */ export function useReportOrderAttribution( params: ReportParams, options?: UseReportOrderAttributionOptions @@ -32,12 +37,7 @@ export function useReportOrderAttribution( // Order attribution requires the view parameter if ( ! params.view ) { return { - queryKey: [ - 'reports', - 'order-attribution', - '__disabled__', - 'no-view-param', - ], + queryKey: [ 'reports', 'order-attribution', '__disabled__', 'no-view-param' ], enabled: false, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts index 336de035aede..89cf74039adb 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts @@ -9,18 +9,14 @@ type UseReportOrdersOptions = { enabled?: boolean; }; -export function useReportOrders( - params: ReportParams, - options?: UseReportOrdersOptions -) { - return useReport( ( p ) => reportOrdersQuery( p ), params, { +/** + * + * @param params + * @param options + */ +export function useReportOrders( params: ReportParams, options?: UseReportOrdersOptions ) { + return useReport( p => reportOrdersQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'orders', - 'by-date', - '__comparison__', - 'disabled', - ], + disabledComparisonKey: [ 'reports', 'orders', 'by-date', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts index bdd30704ffe8..905477389c79 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts @@ -5,13 +5,13 @@ import { reportProductsQuery } from '../queries/report-products-query'; import { type ReportParams } from '../utils/search'; import { useReport } from './use-report'; +/** + * + * @param params + * @param limit + */ export function useReportProducts( params: ReportParams, limit = 5 ) { - return useReport( ( p ) => reportProductsQuery( { ...p, limit } ), params, { - disabledComparisonKey: [ - 'reports', - 'products', - '__comparison__', - 'disabled', - ], + return useReport( p => reportProductsQuery( { ...p, limit } ), params, { + disabledComparisonKey: [ 'reports', 'products', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts index 535980817370..7ac25ef8f69b 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts @@ -32,14 +32,8 @@ export function useReportSessionsByDevice( params: ReportParams, options?: UseReportSessionsByDeviceOptions ) { - return useReport( ( p ) => reportSessionsByDeviceQuery( p ), params, { + return useReport( p => reportSessionsByDeviceQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'sessions', - 'by-device', - '__comparison__', - 'disabled', - ], + disabledComparisonKey: [ 'reports', 'sessions', 'by-device', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts index b4252eb16676..ac9b8146508d 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts @@ -12,12 +12,17 @@ type UseReportVisitorsByLocationOptions = { limit?: number; }; +/** + * + * @param params + * @param options + */ export function useReportVisitorsByLocation( params: ReportParams, options?: UseReportVisitorsByLocationOptions ) { return useReport( - ( p ) => + p => reportVisitorsByLocationQuery( { ...p, group_by: options?.groupBy ?? 'country', @@ -27,13 +32,7 @@ export function useReportVisitorsByLocation( params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'visitors', - 'by-location', - '__comparison__', - 'disabled', - ], + disabledComparisonKey: [ 'reports', 'visitors', 'by-location', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts index 7fd1805f2744..b6eafd7a3f3f 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts @@ -9,18 +9,14 @@ type UseReportVisitorsOptions = { enabled?: boolean; }; -export function useReportVisitors( - params: ReportParams, - options?: UseReportVisitorsOptions -) { - return useReport( ( p ) => reportVisitorsQuery( p ), params, { +/** + * + * @param params + * @param options + */ +export function useReportVisitors( params: ReportParams, options?: UseReportVisitorsOptions ) { + return useReport( p => reportVisitorsQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'visitors', - 'by-date', - '__comparison__', - 'disabled', - ], + disabledComparisonKey: [ 'reports', 'visitors', 'by-date', '__comparison__', 'disabled' ], } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts index 9d90ab78683c..e72a203f2e80 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts @@ -3,7 +3,6 @@ */ import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; import { useCallback } from 'react'; - /** * Internal dependencies */ @@ -81,11 +80,7 @@ export function useReport< TData >( 'comparison' ) : { - queryKey: options?.disabledComparisonKey ?? [ - 'reports', - '__comparison__', - 'disabled', - ], + queryKey: options?.disabledComparisonKey ?? [ 'reports', '__comparison__', 'disabled' ], }; const primary = useQuery( { @@ -95,10 +90,7 @@ export function useReport< TData >( const comparison = useQuery( { ...comparisonQueryOptions, - enabled: - queryEnabled && - comparisonEnabled && - ( comparisonQueryOptions.enabled ?? true ), + enabled: queryEnabled && comparisonEnabled && ( comparisonQueryOptions.enabled ?? true ), } ); // Compute common derived states diff --git a/projects/packages/premium-analytics/packages/data/src/index.ts b/projects/packages/premium-analytics/packages/data/src/index.ts index 0d685aeb5de7..6f02580c031e 100644 --- a/projects/packages/premium-analytics/packages/data/src/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/index.ts @@ -1,15 +1,6 @@ -export { - AnalyticsQueryClientProvider, - queryClient, -} from './providers/query-client-provider'; -export { - GlobalErrorProvider, - useGlobalError, -} from './providers/global-error-context'; -export { - globalErrorManager, - type GlobalErrorType, -} from './providers/global-error-manager'; +export { AnalyticsQueryClientProvider, queryClient } from './providers/query-client-provider'; +export { GlobalErrorProvider, useGlobalError } from './providers/global-error-context'; +export { globalErrorManager, type GlobalErrorType } from './providers/global-error-manager'; export { useReportOrders } from './hooks/use-report-orders'; export { useReportOrderAttribution } from './hooks/use-report-order-attribution'; export { useReportCoupons } from './hooks/use-report-coupons'; @@ -44,10 +35,7 @@ export type { ReportQueryParams } from './api'; export type { FilterCondition } from './types/filter-condition'; export type { ProductType } from './types/product-type'; export { ORDER_ATTRIBUTION_VIEWS } from './api/report-order-attribution-summary-fetch'; -export { - getDefaultIntervalForPeriod, - getDateFormatFromInterval, -} from './utils/interval'; +export { getDefaultIntervalForPeriod, getDateFormatFromInterval } from './utils/interval'; export { getDefaultPreset, getDefaultQueryParams } from './defaults'; export { exportReport } from './api'; export type { ExportReportParams, ExportReportResponse } from './api'; diff --git a/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts index b5f494b174f7..b73aac40dca0 100644 --- a/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts +++ b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts @@ -18,22 +18,23 @@ import { type RequestReportParamsMap = { orders: Parameters< typeof reportOrdersQuery >[ 0 ]; - 'order-attribution': Parameters< - typeof reportOrderAttributionSummaryQuery - >[ 0 ]; + 'order-attribution': Parameters< typeof reportOrderAttributionSummaryQuery >[ 0 ]; coupons: Parameters< typeof reportCouponsQuery >[ 0 ]; 'coupons-by-date': Parameters< typeof reportCouponsByDateQuery >[ 0 ]; customers: Parameters< typeof reportCustomersQuery >[ 0 ]; 'customers-by-date': Parameters< typeof reportCustomersByDateQuery >[ 0 ]; visitors: Parameters< typeof reportVisitorsQuery >[ 0 ]; - 'visitors-by-location': Parameters< - typeof reportVisitorsByLocationQuery - >[ 0 ]; + 'visitors-by-location': Parameters< typeof reportVisitorsByLocationQuery >[ 0 ]; 'sessions-by-device': Parameters< typeof reportSessionsByDeviceQuery >[ 0 ]; products: Parameters< typeof reportProductsQuery >[ 0 ]; 'conversion-rate': Parameters< typeof reportConversionRateQuery >[ 0 ]; }; +/** + * + * @param reportType + * @param params + */ export async function prefetchReport< T extends keyof RequestReportParamsMap >( reportType: T = 'orders' as T, params: RequestReportParamsMap[ T ] @@ -41,9 +42,7 @@ export async function prefetchReport< T extends keyof RequestReportParamsMap >( switch ( reportType ) { case 'orders': return queryClient.ensureQueryData( - reportOrdersQuery( - params as RequestReportParamsMap[ 'orders' ] - ) + reportOrdersQuery( params as RequestReportParamsMap[ 'orders' ] ) ); case 'order-attribution': @@ -55,65 +54,47 @@ export async function prefetchReport< T extends keyof RequestReportParamsMap >( case 'coupons': return queryClient.ensureQueryData( - reportCouponsQuery( - params as RequestReportParamsMap[ 'coupons' ] - ) + reportCouponsQuery( params as RequestReportParamsMap[ 'coupons' ] ) ); case 'coupons-by-date': return queryClient.ensureQueryData( - reportCouponsByDateQuery( - params as RequestReportParamsMap[ 'coupons-by-date' ] - ) + reportCouponsByDateQuery( params as RequestReportParamsMap[ 'coupons-by-date' ] ) ); case 'customers': return queryClient.ensureQueryData( - reportCustomersQuery( - params as RequestReportParamsMap[ 'customers' ] - ) + reportCustomersQuery( params as RequestReportParamsMap[ 'customers' ] ) ); case 'customers-by-date': return queryClient.ensureQueryData( - reportCustomersByDateQuery( - params as RequestReportParamsMap[ 'customers-by-date' ] - ) + reportCustomersByDateQuery( params as RequestReportParamsMap[ 'customers-by-date' ] ) ); case 'visitors': return queryClient.ensureQueryData( - reportVisitorsQuery( - params as RequestReportParamsMap[ 'visitors' ] - ) + reportVisitorsQuery( params as RequestReportParamsMap[ 'visitors' ] ) ); case 'visitors-by-location': return queryClient.ensureQueryData( - reportVisitorsByLocationQuery( - params as RequestReportParamsMap[ 'visitors-by-location' ] - ) + reportVisitorsByLocationQuery( params as RequestReportParamsMap[ 'visitors-by-location' ] ) ); case 'sessions-by-device': return queryClient.ensureQueryData( - reportSessionsByDeviceQuery( - params as RequestReportParamsMap[ 'sessions-by-device' ] - ) + reportSessionsByDeviceQuery( params as RequestReportParamsMap[ 'sessions-by-device' ] ) ); case 'products': return queryClient.ensureQueryData( - reportProductsQuery( - params as RequestReportParamsMap[ 'products' ] - ) + reportProductsQuery( params as RequestReportParamsMap[ 'products' ] ) ); case 'conversion-rate': return queryClient.ensureQueryData( - reportConversionRateQuery( - params as RequestReportParamsMap[ 'conversion-rate' ] - ) + reportConversionRateQuery( params as RequestReportParamsMap[ 'conversion-rate' ] ) ); default: diff --git a/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts index 0d656d471de8..7073cb9e7d32 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts @@ -2,14 +2,11 @@ * Internal dependencies */ import { fetchReportBookings } from '../../api/report-bookings-fetch'; -import type { Override } from '../../utils/types'; import { safeParseInt } from '../../utils/parsing'; +import type { Override } from '../../utils/types'; -type ReportsBookingsByDateResponse = Awaited< - ReturnType< typeof fetchReportBookings > ->; -type RawBookingsReportDataItem = - ReportsBookingsByDateResponse[ 'data' ][ number ]; +type ReportsBookingsByDateResponse = Awaited< ReturnType< typeof fetchReportBookings > >; +type RawBookingsReportDataItem = ReportsBookingsByDateResponse[ 'data' ][ number ]; type RawBookingsReportSummaryItem = ReportsBookingsByDateResponse[ 'summary' ]; type SanitizedBookingsByDateItem = Override< @@ -44,32 +41,26 @@ type SanitizedBookingsSummaryItem = Override< /** * Sanitize/process a single booking item by converting strings to numbers + * @param item */ -function sanitizeBookingItem( - item: RawBookingsReportDataItem -): SanitizedBookingsByDateItem { +function sanitizeBookingItem( item: RawBookingsReportDataItem ): SanitizedBookingsByDateItem { return { ...item, status_unpaid: safeParseInt( item.status_unpaid ), - status_pending_confirmation: safeParseInt( - item.status_pending_confirmation - ), + status_pending_confirmation: safeParseInt( item.status_pending_confirmation ), status_confirmed: safeParseInt( item.status_confirmed ), status_paid: safeParseInt( item.status_paid ), status_cancelled: safeParseInt( item.status_cancelled ), status_complete: safeParseInt( item.status_complete ), attendance_status_booked: safeParseInt( item.attendance_status_booked ), - attendance_status_no_show: safeParseInt( - item.attendance_status_no_show - ), - attendance_status_checked_in: safeParseInt( - item.attendance_status_checked_in - ), + attendance_status_no_show: safeParseInt( item.attendance_status_no_show ), + attendance_status_checked_in: safeParseInt( item.attendance_status_checked_in ), }; } /** * Sanitize/process a single booking summary item by converting strings to numbers + * @param item */ function sanitizeBookingSummaryItem( item: RawBookingsReportSummaryItem @@ -77,20 +68,14 @@ function sanitizeBookingSummaryItem( return { ...item, status_unpaid: safeParseInt( item.status_unpaid ), - status_pending_confirmation: safeParseInt( - item.status_pending_confirmation - ), + status_pending_confirmation: safeParseInt( item.status_pending_confirmation ), status_confirmed: safeParseInt( item.status_confirmed ), status_paid: safeParseInt( item.status_paid ), status_cancelled: safeParseInt( item.status_cancelled ), status_complete: safeParseInt( item.status_complete ), attendance_status_booked: safeParseInt( item.attendance_status_booked ), - attendance_status_no_show: safeParseInt( - item.attendance_status_no_show - ), - attendance_status_checked_in: safeParseInt( - item.attendance_status_checked_in - ), + attendance_status_no_show: safeParseInt( item.attendance_status_no_show ), + attendance_status_checked_in: safeParseInt( item.attendance_status_checked_in ), }; } @@ -108,6 +93,7 @@ type SanitizedBookingsByDateResponse = { * * The `summary` and `data` items have different structures (summary lacks time_interval), * so we use different sanitizer functions for each. + * @param response */ export const sanitizeReportBookingsResponse = ( response: ReportsBookingsByDateResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts index 3e33c27c69af..1135cf3a43f0 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts @@ -2,19 +2,17 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ import { fetchReportConversionRate } from '../../api/report-conversion-rate-fetch'; -import type { Override } from '../../utils/types'; import { safeParseInt } from '../../utils/parsing'; +import type { Override } from '../../utils/types'; type ReportsConversionRateByDateResponse = Awaited< ReturnType< typeof fetchReportConversionRate > >; -type RawConversionRateReportDataItem = - ReportsConversionRateByDateResponse[ 'data' ][ number ]; +type RawConversionRateReportDataItem = ReportsConversionRateByDateResponse[ 'data' ][ number ]; type SanitizedConversionRateByDateItem = Override< RawConversionRateReportDataItem, { @@ -30,6 +28,7 @@ type SanitizedConversionRateByDateItem = Override< /** * Sanitize/process a single conversion rate item by converting strings to numbers * and calculating the conversion rate + * @param item */ function sanitizeConversionRateItem( item: RawConversionRateReportDataItem @@ -42,8 +41,7 @@ function sanitizeConversionRateItem( // Calculate conversion rate as decimal (e.g., 0.035 for 3.5%) // This format works with formatMetricValue 'percentage' type - const conversionRate = - activeSessionsNum > 0 ? completedCheckoutNum / activeSessionsNum : 0; + const conversionRate = activeSessionsNum > 0 ? completedCheckoutNum / activeSessionsNum : 0; return { ...item, @@ -82,6 +80,7 @@ type SanitizedConversionRateByDateResponse = { * * The `summary` single item has basically the same structure * as the `data` array items, so we can use the same mapper function for both. + * @param response */ export const sanitizeReportConversionRateResponse = ( response: ReportsConversionRateByDateResponse @@ -97,9 +96,7 @@ export const sanitizeReportConversionRateResponse = ( date_end: '', }; - const sanitizedSummary = sanitizeConversionRateItem( - response?.summary || defaultSummary - ); + const sanitizedSummary = sanitizeConversionRateItem( response?.summary || defaultSummary ); // Create funnel steps from the summary data const steps: FunnelStep[] = [ @@ -115,9 +112,7 @@ export const sanitizeReportConversionRateResponse = ( count: sanitizedSummary.with_cart_addition, rate: sanitizedSummary.active_sessions > 0 - ? ( sanitizedSummary.with_cart_addition / - sanitizedSummary.active_sessions ) * - 100 + ? ( sanitizedSummary.with_cart_addition / sanitizedSummary.active_sessions ) * 100 : 0, }, { @@ -126,9 +121,7 @@ export const sanitizeReportConversionRateResponse = ( count: sanitizedSummary.reached_checkout, rate: sanitizedSummary.active_sessions > 0 - ? ( sanitizedSummary.reached_checkout / - sanitizedSummary.active_sessions ) * - 100 + ? ( sanitizedSummary.reached_checkout / sanitizedSummary.active_sessions ) * 100 : 0, }, { @@ -137,18 +130,14 @@ export const sanitizeReportConversionRateResponse = ( count: sanitizedSummary.completed_checkout, rate: sanitizedSummary.active_sessions > 0 - ? ( sanitizedSummary.completed_checkout / - sanitizedSummary.active_sessions ) * - 100 + ? ( sanitizedSummary.completed_checkout / sanitizedSummary.active_sessions ) * 100 : 0, }, ]; return { summary: sanitizedSummary, - data: response?.data - ? response.data.map( sanitizeConversionRateItem ) - : [], + data: response?.data ? response.data.map( sanitizeConversionRateItem ) : [], steps, overallRate: sanitizedSummary.conversion_rate, }; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts index 53b5720d37a0..c8af73181fce 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts @@ -4,9 +4,7 @@ import { fetchReportCouponsByDate } from '../../api/report-coupons-by-date-fetch'; import type { Override } from '../../utils/types'; -type ReportsCouponsByDateResponse = Awaited< - ReturnType< typeof fetchReportCouponsByDate > ->; +type ReportsCouponsByDateResponse = Awaited< ReturnType< typeof fetchReportCouponsByDate > >; type RawSummary = ReportsCouponsByDateResponse[ 'summary' ]; type RawDataItem = ReportsCouponsByDateResponse[ 'data' ][ number ]; @@ -54,6 +52,10 @@ type SanitizedCouponsByDateResponse = { data: SanitizedCouponsByDateDataItem[]; }; +/** + * + * @param item + */ function sanitizeItem( item: RawDataItem ): SanitizedCouponsByDateDataItem { return { ...item, @@ -69,6 +71,10 @@ function sanitizeItem( item: RawDataItem ): SanitizedCouponsByDateDataItem { }; } +/** + * + * @param summary + */ function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { return { ...summary, @@ -79,9 +85,7 @@ function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { sales_with_coupon: parseFloat( summary.sales_with_coupon ), sales_without_coupon: parseFloat( summary.sales_without_coupon ), total_discount_amount: parseFloat( summary.total_discount_amount ), - net_sales_after_discount: parseFloat( - summary.net_sales_after_discount - ), + net_sales_after_discount: parseFloat( summary.net_sales_after_discount ), coupon_usage_percentage: parseFloat( summary.coupon_usage_percentage ), }; } @@ -89,6 +93,7 @@ function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { /** * Sanitize the response from the reports/coupons/by-date endpoint. * Converts string values to numbers for calculations and charting. + * @param response */ export const sanitizeReportCouponsByDateResponse = ( response: ReportsCouponsByDateResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts index 04c76205f2c5..1f40db489cd3 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts @@ -4,9 +4,7 @@ import { fetchReportCoupons } from '../../api/report-coupons-fetch'; import type { Override } from '../../utils/types'; -type ReportsCouponsResponse = Awaited< - ReturnType< typeof fetchReportCoupons > ->; +type ReportsCouponsResponse = Awaited< ReturnType< typeof fetchReportCoupons > >; type RawCouponsDataItem = ReportsCouponsResponse[ 'data' ][ number ]; type RawCouponsDataSummary = ReportsCouponsResponse[ 'summary' ]; @@ -44,10 +42,9 @@ type SanitizedCouponsResponse = { /** * Sanitize/process a single coupon item by converting strings to numbers + * @param item */ -function sanitizeCouponItem( - item: RawCouponsDataItem -): SanitizedCouponsDataItem { +function sanitizeCouponItem( item: RawCouponsDataItem ): SanitizedCouponsDataItem { return { ...item, discount_amount: parseFloat( item.discount_amount ), @@ -58,10 +55,9 @@ function sanitizeCouponItem( /** * Sanitize/process summary by converting strings to numbers + * @param summary */ -function sanitizeCouponSummary( - summary: RawCouponsDataSummary -): SanitizedCouponsDataSummary { +function sanitizeCouponSummary( summary: RawCouponsDataSummary ): SanitizedCouponsDataSummary { return { ...summary, total_sales: parseFloat( summary.total_sales ), @@ -73,6 +69,7 @@ function sanitizeCouponSummary( /** * Sanitize the response from the reports/coupons endpoint * Converts string values to numbers for easier calculations and charting. + * @param response */ export const sanitizeReportCouponsResponse = ( response: ReportsCouponsResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts index 1c61302ae3c5..8b098d7adde2 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts @@ -4,12 +4,9 @@ import { fetchReportCustomersByDate } from '../../api/report-customers-by-date-fetch'; import type { Override } from '../../utils/types'; -type ReportsCustomersByDateResponse = Awaited< - ReturnType< typeof fetchReportCustomersByDate > ->; +type ReportsCustomersByDateResponse = Awaited< ReturnType< typeof fetchReportCustomersByDate > >; type RawCustomersByDateSummary = ReportsCustomersByDateResponse[ 'summary' ]; -type RawCustomersByDateItem = - ReportsCustomersByDateResponse[ 'data' ][ number ]; +type RawCustomersByDateItem = ReportsCustomersByDateResponse[ 'data' ][ number ]; /** * Processed summary (numbers for calculations) @@ -76,10 +73,9 @@ export type SanitizedCustomersByDateResponse = { /** * Sanitize/process a single customer item by converting strings to numbers + * @param item */ -function sanitizeCustomerByDateItem( - item: RawCustomersByDateItem -): SanitizedCustomersByDateItem { +function sanitizeCustomerByDateItem( item: RawCustomersByDateItem ): SanitizedCustomersByDateItem { const totalCustomers = parseInt( item.total_customers, 10 ); return { ...item, @@ -88,15 +84,10 @@ function sanitizeCustomerByDateItem( returning_customers: parseInt( item.returning_customers, 10 ), orders_count: parseInt( item.orders_count, 10 ), new_customer_orders: parseInt( item.new_customer_orders, 10 ), - returning_customer_orders: parseInt( - item.returning_customer_orders, - 10 - ), + returning_customer_orders: parseInt( item.returning_customer_orders, 10 ), net_sales: parseFloat( item.net_sales ), new_customer_net_sales: parseFloat( item.new_customer_net_sales ), - returning_customer_net_sales: parseFloat( - item.returning_customer_net_sales - ), + returning_customer_net_sales: parseFloat( item.returning_customer_net_sales ), // Add alias for compatibility with chart builder customers: totalCustomers, }; @@ -104,6 +95,7 @@ function sanitizeCustomerByDateItem( /** * Sanitize/process the summary by converting strings to numbers + * @param summary */ function sanitizeCustomerByDateSummary( summary: RawCustomersByDateSummary @@ -116,47 +108,24 @@ function sanitizeCustomerByDateSummary( total_discounts: parseFloat( summary.total_discounts ), total_refunds: parseFloat( summary.total_refunds ), total_orders: parseInt( summary.total_orders, 10 ), - total_average_order_value: parseFloat( - summary.total_average_order_value - ), - total_avg_items_per_order: parseFloat( - summary.total_avg_items_per_order - ), + total_average_order_value: parseFloat( summary.total_average_order_value ), + total_avg_items_per_order: parseFloat( summary.total_avg_items_per_order ), total_customers: totalCustomers, new_customers: parseInt( summary.new_customers, 10 ), returning_customers: parseInt( summary.returning_customers, 10 ), new_customer_sales: parseFloat( summary.new_customer_sales ), - new_customer_gross_sales: parseFloat( - summary.new_customer_gross_sales - ), + new_customer_gross_sales: parseFloat( summary.new_customer_gross_sales ), new_customer_discounts: parseFloat( summary.new_customer_discounts ), new_customer_refunds: parseFloat( summary.new_customer_refunds ), new_customer_orders: parseInt( summary.new_customer_orders, 10 ), - new_customer_avg_order_value: parseFloat( - summary.new_customer_avg_order_value - ), - new_customer_avg_items_per_order: parseFloat( - summary.new_customer_avg_items_per_order - ), - returning_customer_sales: parseFloat( - summary.returning_customer_sales - ), - returning_customer_gross_sales: parseFloat( - summary.returning_customer_gross_sales - ), - returning_customer_discounts: parseFloat( - summary.returning_customer_discounts - ), - returning_customer_refunds: parseFloat( - summary.returning_customer_refunds - ), - returning_customer_orders: parseInt( - summary.returning_customer_orders, - 10 - ), - returning_customer_avg_order_value: parseFloat( - summary.returning_customer_avg_order_value - ), + new_customer_avg_order_value: parseFloat( summary.new_customer_avg_order_value ), + new_customer_avg_items_per_order: parseFloat( summary.new_customer_avg_items_per_order ), + returning_customer_sales: parseFloat( summary.returning_customer_sales ), + returning_customer_gross_sales: parseFloat( summary.returning_customer_gross_sales ), + returning_customer_discounts: parseFloat( summary.returning_customer_discounts ), + returning_customer_refunds: parseFloat( summary.returning_customer_refunds ), + returning_customer_orders: parseInt( summary.returning_customer_orders, 10 ), + returning_customer_avg_order_value: parseFloat( summary.returning_customer_avg_order_value ), returning_customer_avg_items_per_order: parseFloat( summary.returning_customer_avg_items_per_order ), @@ -168,6 +137,7 @@ function sanitizeCustomerByDateSummary( /** * Sanitize the response from the reports/customers/by-date endpoint * Converts string values to numbers for easier calculations and charting. + * @param response */ export const sanitizeReportCustomersByDateResponse = ( response: ReportsCustomersByDateResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts index be0fffcf7844..780eca7c831f 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts @@ -4,13 +4,9 @@ import { fetchReportCustomers } from '../../api/report-customers-fetch'; import type { Override } from '../../utils/types'; -type ReportsCustomersNewReturningResponse = Awaited< - ReturnType< typeof fetchReportCustomers > ->; -type RawCustomersNewReturningSummary = - ReportsCustomersNewReturningResponse[ 'summary' ]; -type RawCustomersNewReturningItem = - ReportsCustomersNewReturningResponse[ 'data' ][ number ]; +type ReportsCustomersNewReturningResponse = Awaited< ReturnType< typeof fetchReportCustomers > >; +type RawCustomersNewReturningSummary = ReportsCustomersNewReturningResponse[ 'summary' ]; +type RawCustomersNewReturningItem = ReportsCustomersNewReturningResponse[ 'data' ][ number ]; /** * Processed summary (numbers for calculations) @@ -46,6 +42,7 @@ type SanitizedCustomersNewReturningResponse = { /** * Sanitize/process a single customer item by converting strings to numbers + * @param item */ function sanitizeCustomerItem( item: RawCustomersNewReturningItem @@ -59,6 +56,7 @@ function sanitizeCustomerItem( /** * Sanitize/process the summary by converting strings to numbers + * @param summary */ function sanitizeCustomerSummary( summary: RawCustomersNewReturningSummary @@ -68,15 +66,14 @@ function sanitizeCustomerSummary( total_net_sales: parseFloat( summary.total_net_sales ), total_orders: parseInt( summary.total_orders, 10 ), new_customer_sales: parseFloat( summary.new_customer_sales ), - returning_customer_sales: parseFloat( - summary.returning_customer_sales - ), + returning_customer_sales: parseFloat( summary.returning_customer_sales ), }; } /** * Sanitize the response from the reports/customers/new-returning endpoint * Converts string values to numbers for easier calculations and charting. + * @param response */ export const sanitizeReportCustomersResponse = ( response: ReportsCustomersNewReturningResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts index 129d281b23a4..5ed83bf8404e 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts @@ -20,25 +20,21 @@ export function normalizeOrderAttributionByProductResponse( previousResponse?: OrderAttributionByProductResponse ): OrderAttributionSummaryResponse { // Create a map for quick lookup of previous period data by item - const previousDataMap = new Map< - string, - ( typeof currentResponse.data )[ 0 ] - >(); + const previousDataMap = new Map< string, ( typeof currentResponse.data )[ 0 ] >(); if ( previousResponse ) { - previousResponse.data.forEach( ( item ) => { + previousResponse.data.forEach( item => { previousDataMap.set( item.item, item ); } ); } // Transform the flat structure to nested structure - const normalizedData = currentResponse.data.map( ( currentItem ) => { + const normalizedData = currentResponse.data.map( currentItem => { const previousItem = previousDataMap.get( currentItem.item ); // If no previous response provided (no comparison), use current data for both periods // This matches the behavior of the existing API when compare_from/to equal from/to const previousValue = previousItem?.value || currentItem.value; - const previousIntervals = - previousItem?.intervals || currentItem.intervals; + const previousIntervals = previousItem?.intervals || currentItem.intervals; return { item: currentItem.item, @@ -56,22 +52,18 @@ export function normalizeOrderAttributionByProductResponse( // Handle items that exist in previous period but not in current // This ensures we don't lose data when an item had sales in the previous period but not current if ( previousResponse ) { - previousResponse.data.forEach( ( previousItem ) => { - const existsInCurrent = currentResponse.data.some( - ( item ) => item.item === previousItem.item - ); + previousResponse.data.forEach( previousItem => { + const existsInCurrent = currentResponse.data.some( item => item.item === previousItem.item ); if ( ! existsInCurrent ) { normalizedData.push( { item: previousItem.item, current_period: { value: '0', - intervals: previousItem.intervals.map( - ( interval ) => ( { - ...interval, - net_sales: '0', - } ) - ), + intervals: previousItem.intervals.map( interval => ( { + ...interval, + net_sales: '0', + } ) ), }, previous_period: { value: previousItem.value, diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts index 7634d328fae4..09572282b098 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import { sanitizeStringNumber } from '../utils'; import { fetchReportOrderAttributionSummary } from '../../api/report-order-attribution-summary-fetch'; +import { sanitizeStringNumber } from '../utils'; type OrderAttributionSummaryResponse = Awaited< ReturnType< typeof fetchReportOrderAttributionSummary > @@ -60,6 +60,7 @@ export type SanitizedOrderAttributionSummaryResponse = { /** * Sanitizes a single interval by converting string net_sales to number + * @param interval */ function sanitizeOrderAttributionInterval( interval: OrderAttributionInterval @@ -74,6 +75,7 @@ function sanitizeOrderAttributionInterval( /** * Sanitizes a period by converting value to number and intervals + * @param period */ function sanitizeOrderAttributionPeriod( period: OrderAttributionPeriod @@ -86,6 +88,7 @@ function sanitizeOrderAttributionPeriod( /** * Sanitizes a single order attribution summary item + * @param item */ function sanitizeOrderAttributionSummaryItem( item: OrderAttributionSummaryItem diff --git a/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts index faf1f71af07e..75be705212bb 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts @@ -1,12 +1,12 @@ /** * Internal dependencies */ +import { safeParseFloat, safeParseInt } from '../../utils/parsing'; import type { ReportsOrdersByDateResponse, RequestReportOrdersParams, } from '../../api/report-orders-fetch'; import type { Override } from '../../utils/types'; -import { safeParseFloat, safeParseInt } from '../../utils/parsing'; /** * Re-export the request params type for backwards compatibility. @@ -37,6 +37,7 @@ type SanitizedOrdersByProductTypeByDateItem = Override< /** * Sanitize/process a single orders by product type item by converting strings to numbers + * @param item */ function sanitizeOrdersByProductTypeItem( item: RawOrdersByProductTypeReportDataItem @@ -71,6 +72,7 @@ type SanitizedOrdersByProductTypeByDateResponse = { * * The `summary` single item has basically the same structure * as the `data` array items, so we can use the same mapper function for both. + * @param response */ export const sanitizeReportOrdersByProductTypeResponse = ( response: ReportsOrdersByProductTypeByDateResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts index 32dc9661797b..00b7a8acf6d4 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts @@ -2,12 +2,10 @@ * Internal dependencies */ import { fetchReportOrders } from '../../api/report-orders-fetch'; -import type { Override } from '../../utils/types'; import { safeParseFloat, safeParseInt } from '../../utils/parsing'; +import type { Override } from '../../utils/types'; -type ReportsOrdersByDateResponse = Awaited< - ReturnType< typeof fetchReportOrders > ->; +type ReportsOrdersByDateResponse = Awaited< ReturnType< typeof fetchReportOrders > >; type RawOrdersReportDataItem = ReportsOrdersByDateResponse[ 'data' ][ number ]; type SanitizedOrdersByDateItem = Override< RawOrdersReportDataItem, @@ -32,10 +30,9 @@ type SanitizedOrdersByDateItem = Override< /** * Sanitize/process a single order item by converting strings to numbers + * @param item */ -function sanitizeOrderItem( - item: RawOrdersReportDataItem -): SanitizedOrdersByDateItem { +function sanitizeOrderItem( item: RawOrdersReportDataItem ): SanitizedOrdersByDateItem { return { ...item, average_order_value: safeParseFloat( item.average_order_value ), @@ -70,6 +67,7 @@ type SanitizedOrdersByDateResponse = { * * The `summary` single item has basically the same structure * as the `data` array items, so we can use the same mapper function for both. + * @param response */ export const sanitizeReportOrdersResponse = ( response: ReportsOrdersByDateResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts index a3937da22a13..6d2ba237964f 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts @@ -4,9 +4,7 @@ import { fetchReportProducts } from '../../api/report-products-fetch'; import type { Override } from '../../utils/types'; -type ReportProductsResponse = Awaited< - ReturnType< typeof fetchReportProducts > ->; +type ReportProductsResponse = Awaited< ReturnType< typeof fetchReportProducts > >; type RawProductsReportDataItem = ReportProductsResponse[ 'data' ][ number ]; type RawProductsReportSummary = ReportProductsResponse[ 'summary' ]; @@ -33,10 +31,9 @@ type SanitizedProductsSummary = Override< /** * Sanitize/process a single product item by converting strings to numbers + * @param item */ -function sanitizeProductItem( - item: RawProductsReportDataItem -): SanitizedProductsItem { +function sanitizeProductItem( item: RawProductsReportDataItem ): SanitizedProductsItem { return { ...item, product_id: parseInt( item.product_id, 10 ), @@ -46,9 +43,11 @@ function sanitizeProductItem( }; } -function sanitizeProductSummary( - summary: RawProductsReportSummary -): SanitizedProductsSummary { +/** + * + * @param summary + */ +function sanitizeProductSummary( summary: RawProductsReportSummary ): SanitizedProductsSummary { return { ...summary, total_orders: parseInt( summary.total_orders, 10 ), @@ -72,6 +71,7 @@ type SanitizedProductsResponse = { * * The `summary` single item has basically the same structure * as the `data` array items, so we can use the same mapper function for both. + * @param response */ export const sanitizeReportProductsResponse = ( response: ReportProductsResponse diff --git a/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts index f88fc25fc0c4..127c39843e31 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts @@ -6,15 +6,12 @@ import { fetchReportSessionsByDevice } from '../../api/report-sessions-by-device /** * Inferred types from fetch response */ -type ReportsSessionsByDeviceResponse = Awaited< - ReturnType< typeof fetchReportSessionsByDevice > ->; +type ReportsSessionsByDeviceResponse = Awaited< ReturnType< typeof fetchReportSessionsByDevice > >; /** * Raw item type from API response */ -type SessionsByDeviceItem = - ReportsSessionsByDeviceResponse[ 'data' ][ number ]; +type SessionsByDeviceItem = ReportsSessionsByDeviceResponse[ 'data' ][ number ]; /** * Sanitized item with numeric values @@ -44,9 +41,7 @@ type SanitizedSessionsByDeviceResponse = { * * @param item - Raw item from API response */ -function sanitizeSessionsByDeviceItem( - item: SessionsByDeviceItem -): SanitizedSessionsByDeviceItem { +function sanitizeSessionsByDeviceItem( item: SessionsByDeviceItem ): SanitizedSessionsByDeviceItem { return { device_type: item.device_type || '', active_sessions: parseInt( item.active_sessions, 10 ) || 0, @@ -66,13 +61,10 @@ export const sanitizeReportSessionsByDeviceResponse = ( ): SanitizedSessionsByDeviceResponse => { const items = response?.data ?? []; const data = items - .filter( ( item ) => item.device_type ) // Filter out empty device types + .filter( item => item.device_type ) // Filter out empty device types .map( sanitizeSessionsByDeviceItem ); - const totalSessions = data.reduce( - ( acc, item ) => acc + item.active_sessions, - 0 - ); + const totalSessions = data.reduce( ( acc, item ) => acc + item.active_sessions, 0 ); return { summary: { diff --git a/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts index 509856885bc9..b9dac4936c16 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts @@ -10,11 +10,8 @@ import type { Override } from '../../utils/types'; type ReportsVisitorsByLocationResponse = Awaited< ReturnType< typeof fetchReportVisitorsByLocation > >; -type RawVisitorsByLocationItem = - ReportsVisitorsByLocationResponse[ 'data' ][ number ]; -type RawVisitorsByLocationSummary = NonNullable< - ReportsVisitorsByLocationResponse[ 'summary' ] ->; +type RawVisitorsByLocationItem = ReportsVisitorsByLocationResponse[ 'data' ][ number ]; +type RawVisitorsByLocationSummary = NonNullable< ReportsVisitorsByLocationResponse[ 'summary' ] >; type SanitizedVisitorsByLocationItem = Override< RawVisitorsByLocationItem, @@ -30,6 +27,10 @@ type SanitizedVisitorsByLocationSummary = Override< } >; +/** + * + * @param item + */ function sanitizeVisitorsByLocationItem( item: RawVisitorsByLocationItem ): SanitizedVisitorsByLocationItem { @@ -41,6 +42,10 @@ function sanitizeVisitorsByLocationItem( }; } +/** + * + * @param summary + */ function sanitizeVisitorsByLocationSummary( summary: RawVisitorsByLocationSummary ): SanitizedVisitorsByLocationSummary { @@ -67,11 +72,7 @@ export const sanitizeReportVisitorsByLocationResponse = ( }; return { - summary: sanitizeVisitorsByLocationSummary( - response?.summary ?? defaultSummary - ), - data: response?.data - ? response.data.map( sanitizeVisitorsByLocationItem ) - : [], + summary: sanitizeVisitorsByLocationSummary( response?.summary ?? defaultSummary ), + data: response?.data ? response.data.map( sanitizeVisitorsByLocationItem ) : [], }; }; diff --git a/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts index 3a787c37cba6..7d14cf71ebc1 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts @@ -7,11 +7,8 @@ import type { Override } from '../../utils/types'; /** * Inferred types */ -type ReportsVisitorsByDateResponse = Awaited< - ReturnType< typeof fetchReportVisitors > ->; -type RawVisitorsReportDataItem = - ReportsVisitorsByDateResponse[ 'data' ][ number ]; +type ReportsVisitorsByDateResponse = Awaited< ReturnType< typeof fetchReportVisitors > >; +type RawVisitorsReportDataItem = ReportsVisitorsByDateResponse[ 'data' ][ number ]; type RawVisitorsReportDataSummary = ReportsVisitorsByDateResponse[ 'summary' ]; type SanitizedVisitorsByDateItem = Override< @@ -40,10 +37,9 @@ type SanitizeVisitorsItemArg = Override< /** * Sanitize/process a single visitors item by converting strings to numbers + * @param item */ -function sanitizeVisitorsItem( - item: SanitizeVisitorsItemArg -): SanitizedVisitorsByDateItem { +function sanitizeVisitorsItem( item: SanitizeVisitorsItemArg ): SanitizedVisitorsByDateItem { return { ...item, active_sessions: parseInt( item.active_sessions, 10 ), @@ -65,6 +61,7 @@ type SanitizedVisitorsByDateResponse = { * * The `summary` single item has basically the same structure * as the `data` array items, so we can use the same mapper function for both. + * @param response */ export const sanitizeReportVisitorsResponse = ( response: ReportsVisitorsByDateResponse diff --git a/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx index 1a03b0e0dd64..cf1058f6255e 100644 --- a/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx +++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import { onlineManager } from '@tanstack/react-query'; import { createContext, useContext, @@ -9,15 +10,10 @@ import { useSyncExternalStore, type ReactNode, } from 'react'; -import { onlineManager } from '@tanstack/react-query'; - /** * Internal dependencies */ -import { - globalErrorManager, - type GlobalErrorType, -} from './global-error-manager'; +import { globalErrorManager, type GlobalErrorType } from './global-error-manager'; interface GlobalErrorContextValue { globalError: GlobalErrorType; @@ -26,13 +22,13 @@ interface GlobalErrorContextValue { isGlobalError: boolean; } -const GlobalErrorContext = createContext< GlobalErrorContextValue | null >( - null -); +const GlobalErrorContext = createContext< GlobalErrorContextValue | null >( null ); /** * Connects React to the global error manager via useSyncExternalStore. * Also subscribes to network status changes via onlineManager. + * @param root0 + * @param root0.children */ export function GlobalErrorProvider( { children }: { children: ReactNode } ) { const globalError = useSyncExternalStore( @@ -54,7 +50,7 @@ export function GlobalErrorProvider( { children }: { children: ReactNode } ) { globalErrorManager.setError( 'network' ); } - const unsubscribe = onlineManager.subscribe( ( isOnline ) => { + const unsubscribe = onlineManager.subscribe( isOnline => { if ( ! isOnline ) { globalErrorManager.setError( 'network' ); } else if ( globalErrorManager.getError() === 'network' ) { @@ -76,9 +72,7 @@ export function GlobalErrorProvider( { children }: { children: ReactNode } ) { ); return ( - - { children } - + { children } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts b/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts index 5cc52b9b003a..9686d09e595b 100644 --- a/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts +++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts @@ -21,7 +21,7 @@ class GlobalErrorManager { return; } this.error = error; - this.listeners.forEach( ( listener ) => listener() ); + this.listeners.forEach( listener => listener() ); }; clearError = (): void => this.setError( null ); diff --git a/projects/packages/premium-analytics/packages/data/src/providers/index.ts b/projects/packages/premium-analytics/packages/data/src/providers/index.ts index 641d073e3b48..9b6bc769e9bf 100644 --- a/projects/packages/premium-analytics/packages/data/src/providers/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/providers/index.ts @@ -1,11 +1,5 @@ -export { - queryClient, - AnalyticsQueryClientProvider, -} from './query-client-provider'; +export { queryClient, AnalyticsQueryClientProvider } from './query-client-provider'; export { GlobalErrorProvider, useGlobalError } from './global-error-context'; -export { - globalErrorManager, - type GlobalErrorType, -} from './global-error-manager'; +export { globalErrorManager, type GlobalErrorType } from './global-error-manager'; diff --git a/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx index c6ddf592efbc..1baf4f0ea5be 100644 --- a/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx +++ b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx @@ -1,13 +1,8 @@ /** * External dependencies */ -import { - QueryClient, - QueryClientProvider, - QueryCache, -} from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'; import { ReactNode, lazy, Suspense } from 'react'; - /** * Internal dependencies */ @@ -39,8 +34,7 @@ function areQueryDevtoolsEnabled(): boolean { } const ReactQueryDevtoolsProduction = lazy( () => - // eslint-disable-next-line import/no-extraneous-dependencies -- DevTools is intentionally in devDependencies, only loaded when enabled - import( '@tanstack/react-query-devtools/production' ).then( ( d ) => ( { + import( '@tanstack/react-query-devtools/production' ).then( d => ( { default: d.ReactQueryDevtools, } ) ) ); @@ -48,6 +42,7 @@ const ReactQueryDevtoolsProduction = lazy( () => /** * Extract HTTP status code from various error formats. * WordPress REST API errors may have different shapes. + * @param error */ function getErrorStatus( error: unknown ): number | null { if ( ! error || typeof error !== 'object' ) { @@ -97,7 +92,7 @@ function getErrorStatus( error: unknown ): number | null { * Network errors are handled separately in GlobalErrorProvider via onlineManager. */ const queryCache = new QueryCache( { - onError: ( error ) => { + onError: error => { const currentError = globalErrorManager.getError(); // Don't override network error (highest priority) @@ -153,11 +148,7 @@ export const queryClient = new QueryClient( { }, } ); -export const AnalyticsQueryClientProvider = ( { - children, -}: { - children: ReactNode; -} ) => { +export const AnalyticsQueryClientProvider = ( { children }: { children: ReactNode } ) => { return ( <>{ children } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts index f1a4cdf921c4..8a223b3f8ce9 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,23 +8,17 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportBookings } from '../api'; import { sanitizeReportBookingsResponse } from '../processing/bookings'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportBookingsParams = Parameters< - typeof fetchReportBookings ->[ 0 ]; +type RequestReportBookingsParams = Parameters< typeof fetchReportBookings >[ 0 ]; const getReportBookingsQueryKey = ( p: RequestReportBookingsParams ) => - [ - 'reports', - 'bookings', - 'by-date', - p.from, - p.to, - p.interval, - p.date_type, - p.filters, - ] as const; + [ 'reports', 'bookings', 'by-date', p.from, p.to, p.interval, p.date_type, p.filters ] as const; +/** + * + * @param params + */ export function reportBookingsQuery( params: RequestReportBookingsParams ): UseQueryOptions< ReportDataMap[ 'bookings' ] > { @@ -43,7 +36,8 @@ export function reportBookingsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts index 5133245335d3..7e60af58f333 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,25 +8,18 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportConversionRate } from '../api/report-conversion-rate-fetch'; import { sanitizeReportConversionRateResponse } from '../processing/conversion-rate'; import type { RequestReportConversionRateParams } from '../api/report-conversion-rate-fetch'; +import type { UseQueryOptions } from '@tanstack/react-query'; -const getReportConversionRateQueryKey = ( - p: RequestReportConversionRateParams -) => - [ - 'reports', - 'conversion-rate', - p.from, - p.to, - p.interval, - p.date_type, - p.filters, - ] as const; +const getReportConversionRateQueryKey = ( p: RequestReportConversionRateParams ) => + [ 'reports', 'conversion-rate', p.from, p.to, p.interval, p.date_type, p.filters ] as const; +/** + * + * @param params + */ export function reportConversionRateQuery( params: RequestReportConversionRateParams -): UseQueryOptions< - ReturnType< typeof sanitizeReportConversionRateResponse > -> { +): UseQueryOptions< ReturnType< typeof sanitizeReportConversionRateResponse > > { return { queryKey: getReportConversionRateQueryKey( params ), queryFn: async () => { @@ -42,7 +34,8 @@ export function reportConversionRateQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts index fe9e4d09b18e..a02418ab87e1 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts @@ -1,33 +1,27 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ import { fetchReportCouponsByDate } from '../api'; import { sanitizeReportCouponsByDateResponse } from '../processing/coupons-by-date'; -import type { ReportDataMap } from '../types'; import { FilterCondition } from '../types/filter-condition'; +import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportCouponsByDateParams = Parameters< - typeof fetchReportCouponsByDate ->[ 0 ] & { +type RequestReportCouponsByDateParams = Parameters< typeof fetchReportCouponsByDate >[ 0 ] & { filters?: FilterCondition[]; }; const getQueryKey = ( p: RequestReportCouponsByDateParams ) => - [ - 'reports', - 'couponsByDate', - p.from, - p.to, - p.interval, - p.date_type, - p.filters, - ] as const; + [ 'reports', 'couponsByDate', p.from, p.to, p.interval, p.date_type, p.filters ] as const; +/** + * + * @param params + */ export function reportCouponsByDateQuery( params: RequestReportCouponsByDateParams ): UseQueryOptions< ReportDataMap[ 'couponsByDate' ] > { @@ -45,7 +39,8 @@ export function reportCouponsByDateQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts index 05244b7017ba..c37f0d78ec0d 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts @@ -1,33 +1,27 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ import { fetchReportCoupons } from '../api'; import { sanitizeReportCouponsResponse } from '../processing/coupons'; -import type { ReportDataMap } from '../types'; import { FilterCondition } from '../types/filter-condition'; +import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportCouponsParams = Parameters< - typeof fetchReportCoupons ->[ 0 ] & { +type RequestReportCouponsParams = Parameters< typeof fetchReportCoupons >[ 0 ] & { filters?: FilterCondition[]; }; const getReportCouponsQueryKey = ( p: RequestReportCouponsParams ) => - [ - 'reports', - 'coupons', - p.from, - p.to, - p.interval, - p.date_type, - p.filters, - ] as const; + [ 'reports', 'coupons', p.from, p.to, p.interval, p.date_type, p.filters ] as const; +/** + * + * @param params + */ export function reportCouponsQuery( params: RequestReportCouponsParams ): UseQueryOptions< ReportDataMap[ 'coupons' ] > { @@ -45,7 +39,8 @@ export function reportCouponsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts index b030320e91e4..1537d84c7353 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,24 +8,17 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportCustomersByDate } from '../api/report-customers-by-date-fetch'; import { sanitizeReportCustomersByDateResponse } from '../processing/customers-by-date'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportCustomersByDateParams = Parameters< - typeof fetchReportCustomersByDate ->[ 0 ]; +type RequestReportCustomersByDateParams = Parameters< typeof fetchReportCustomersByDate >[ 0 ]; -const getReportCustomersByDateQueryKey = ( - p: RequestReportCustomersByDateParams -) => - [ - 'reports', - 'customers', - 'by-date', - p.from, - p.to, - p.interval, - p.date_type, - ] as const; +const getReportCustomersByDateQueryKey = ( p: RequestReportCustomersByDateParams ) => + [ 'reports', 'customers', 'by-date', p.from, p.to, p.interval, p.date_type ] as const; +/** + * + * @param params + */ export function reportCustomersByDateQuery( params: RequestReportCustomersByDateParams ): UseQueryOptions< ReportDataMap[ 'customersByDate' ] > { @@ -44,7 +36,8 @@ export function reportCustomersByDateQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts index c214fd28c6ed..0cfdb1a30cfa 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,10 +8,9 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportCustomers } from '../api'; import { sanitizeReportCustomersResponse } from '../processing/customers'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportCustomersParams = Parameters< - typeof fetchReportCustomers ->[ 0 ]; +type RequestReportCustomersParams = Parameters< typeof fetchReportCustomers >[ 0 ]; const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [ 'reports', @@ -24,6 +22,10 @@ const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [ p.filters, ]; +/** + * + * @param params + */ export function reportCustomersQuery( params: RequestReportCustomersParams ): UseQueryOptions< ReportDataMap[ 'customers' ] > { @@ -42,7 +44,8 @@ export function reportCustomersQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts index deda11889eaf..55be6f0e756d 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts @@ -1,22 +1,19 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ -import { - fetchReportOrderAttributionSummary, - fetchReportOrderAttributionByProduct, -} from '../api'; +import { fetchReportOrderAttributionSummary, fetchReportOrderAttributionByProduct } from '../api'; import { sanitizeReportOrderAttributionSummaryResponse, normalizeOrderAttributionByProductResponse, type SanitizedOrderAttributionSummaryResponse, } from '../processing/order-attribution'; -import type { FilterCondition } from '../types/filter-condition'; import { hasProductFilters } from '../utils/product-filters'; +import type { FilterCondition } from '../types/filter-condition'; +import type { UseQueryOptions } from '@tanstack/react-query'; type ReportOrderAttributionSummaryParams = Parameters< typeof fetchReportOrderAttributionSummary @@ -29,10 +26,9 @@ type ReportOrderAttributionSummaryParams = Parameters< * * Note: All comparison parameters are included in the query key because * order attribution returns both primary and comparison data in a single response. + * @param params */ -const getReportOrderAttributionQueryKey = ( - params: ReportOrderAttributionSummaryParams -) => +const getReportOrderAttributionQueryKey = ( params: ReportOrderAttributionSummaryParams ) => [ 'reports', 'order-attribution', @@ -80,43 +76,37 @@ export function reportOrderAttributionSummaryQuery( const shouldFetchComparison = compare_from && compare_to && - ( compare_from !== params.from || - compare_to !== params.to ); + ( compare_from !== params.from || compare_to !== params.to ); // Fetch both periods in parallel for better performance - const [ currentResponse, previousResponse ] = await Promise.all( - [ - fetchReportOrderAttributionByProduct( { - from: params.from, - to: params.to, - interval: params.interval, - view: params.view, - filters: params.filters, - date_type: params.date_type, - } ), - shouldFetchComparison - ? fetchReportOrderAttributionByProduct( { - from: compare_from, - to: compare_to, - interval: params.interval, - view: params.view, - filters: params.filters, - date_type: params.date_type, - } ) - : Promise.resolve( undefined ), - ] - ); + const [ currentResponse, previousResponse ] = await Promise.all( [ + fetchReportOrderAttributionByProduct( { + from: params.from, + to: params.to, + interval: params.interval, + view: params.view, + filters: params.filters, + date_type: params.date_type, + } ), + shouldFetchComparison + ? fetchReportOrderAttributionByProduct( { + from: compare_from, + to: compare_to, + interval: params.interval, + view: params.view, + filters: params.filters, + date_type: params.date_type, + } ) + : Promise.resolve( undefined ), + ] ); // Normalize to match the regular API structure (includes both periods) - const normalizedResponse = - normalizeOrderAttributionByProductResponse( - currentResponse, - previousResponse - ); - - return sanitizeReportOrderAttributionSummaryResponse( - normalizedResponse + const normalizedResponse = normalizeOrderAttributionByProductResponse( + currentResponse, + previousResponse ); + + return sanitizeReportOrderAttributionSummaryResponse( normalizedResponse ); } // Regular API path: Returns both primary and comparison in one response @@ -128,17 +118,13 @@ export function reportOrderAttributionSummaryQuery( * Enable the query only when all required parameters are present. * The 'view' parameter is required for order attribution queries. */ - enabled: !! ( - params.from && - params.to && - params.interval && - params.view - ), + enabled: !! ( params.from && params.to && params.interval && params.view ), /** * Keep previous data while fetching to prevent flash of empty state. * This provides a smoother user experience during data refetching. + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts index 50703f37728c..29deb2e9a552 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,6 +8,7 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportOrders } from '../api'; import { sanitizeReportOrdersResponse } from '../processing/orders'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; type RequestReportOrdersParams = Parameters< typeof fetchReportOrders >[ 0 ]; @@ -22,6 +22,10 @@ const getReportOrdersQueryKey = ( p: RequestReportOrdersParams ) => [ p.filters || [], ]; +/** + * + * @param params + */ export function reportOrdersQuery( params: RequestReportOrdersParams ): UseQueryOptions< ReportDataMap[ 'orders' ] > { @@ -39,7 +43,8 @@ export function reportOrdersQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts index 4696117be2e2..a46bb686d4ed 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts @@ -1,21 +1,17 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ import { fetchReportProducts } from '../api/report-products-fetch'; import { sanitizeReportProductsResponse } from '../processing/products'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportProductsParams = Parameters< - typeof fetchReportProducts ->[ 0 ]; +type RequestReportProductsParams = Parameters< typeof fetchReportProducts >[ 0 ]; -type SanitizedProductsResponse = ReturnType< - typeof sanitizeReportProductsResponse ->; +type SanitizedProductsResponse = ReturnType< typeof sanitizeReportProductsResponse >; const getReportProductsQueryKey = ( p: RequestReportProductsParams ) => [ @@ -30,6 +26,10 @@ const getReportProductsQueryKey = ( p: RequestReportProductsParams ) => p.filters, ] as const; +/** + * + * @param params + */ export function reportProductsQuery( params: RequestReportProductsParams ): UseQueryOptions< SanitizedProductsResponse > { @@ -47,7 +47,8 @@ export function reportProductsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts index 8e425b7e7127..b00ce00bf33c 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,14 +8,12 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportSessionsByDevice } from '../api/report-sessions-by-device-fetch'; import { sanitizeReportSessionsByDeviceResponse } from '../processing/sessions-by-device'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportSessionsByDeviceParams = Parameters< - typeof fetchReportSessionsByDevice ->[ 0 ]; +type RequestReportSessionsByDeviceParams = Parameters< typeof fetchReportSessionsByDevice >[ 0 ]; -const getReportSessionsByDeviceQueryKey = ( - p: RequestReportSessionsByDeviceParams -) => [ 'reports', 'sessions', 'by-device', p.from, p.to ] as const; +const getReportSessionsByDeviceQueryKey = ( p: RequestReportSessionsByDeviceParams ) => + [ 'reports', 'sessions', 'by-device', p.from, p.to ] as const; /** * Creates query options for fetching sessions by device report data. @@ -41,7 +38,8 @@ export function reportSessionsByDeviceQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts index a75988200065..32663a38b75e 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,14 +8,13 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportVisitorsByLocation } from '../api'; import { sanitizeReportVisitorsByLocationResponse } from '../processing/visitors-by-location'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; type RequestReportVisitorsByLocationParams = Parameters< typeof fetchReportVisitorsByLocation >[ 0 ]; -const getReportVisitorsByLocationQueryKey = ( - p: RequestReportVisitorsByLocationParams -) => +const getReportVisitorsByLocationQueryKey = ( p: RequestReportVisitorsByLocationParams ) => [ 'reports', 'visitors', @@ -29,6 +27,10 @@ const getReportVisitorsByLocationQueryKey = ( p.limit ?? null, ] as const; +/** + * + * @param params + */ export function reportVisitorsByLocationQuery( params: RequestReportVisitorsByLocationParams ): UseQueryOptions< ReportDataMap[ 'visitorsByLocation' ] > { @@ -41,6 +43,6 @@ export function reportVisitorsByLocationQuery( enabled: !! ( params.from && params.to && params.interval ), - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts index d3749c184256..78d2f138d554 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,22 +8,17 @@ import type { UseQueryOptions } from '@tanstack/react-query'; import { fetchReportVisitors } from '../api'; import { sanitizeReportVisitorsResponse } from '../processing/visitors'; import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportVisitorsParams = Parameters< - typeof fetchReportVisitors ->[ 0 ]; +type RequestReportVisitorsParams = Parameters< typeof fetchReportVisitors >[ 0 ]; const getReportVisitorsQueryKey = ( p: RequestReportVisitorsParams ) => - [ - 'reports', - 'visitors', - 'by-date', - p.from, - p.to, - p.interval, - p.date_type, - ] as const; + [ 'reports', 'visitors', 'by-date', p.from, p.to, p.interval, p.date_type ] as const; +/** + * + * @param params + */ export function reportVisitorsQuery( params: RequestReportVisitorsParams ): UseQueryOptions< ReportDataMap[ 'visitors' ] > { @@ -42,7 +36,8 @@ export function reportVisitorsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => previousData, + placeholderData: previousData => previousData, }; } diff --git a/projects/packages/premium-analytics/packages/data/src/types.ts b/projects/packages/premium-analytics/packages/data/src/types.ts index 8ad61e0cbbd0..76b5d9f1a945 100644 --- a/projects/packages/premium-analytics/packages/data/src/types.ts +++ b/projects/packages/premium-analytics/packages/data/src/types.ts @@ -1,21 +1,18 @@ /** * Internal dependencies */ -import { - sanitizeReportOrdersResponse, - sanitizeReportProductsResponse, -} from './processing'; +import { sanitizeReportOrdersResponse, sanitizeReportProductsResponse } from './processing'; +import { sanitizeReportBookingsResponse } from './processing/bookings'; +import { sanitizeReportConversionRateResponse } from './processing/conversion-rate'; +import { sanitizeReportCouponsResponse } from './processing/coupons'; +import { sanitizeReportCouponsByDateResponse } from './processing/coupons-by-date'; import { sanitizeReportCustomersResponse } from './processing/customers'; import { sanitizeReportCustomersByDateResponse } from './processing/customers-by-date'; import { sanitizeReportOrderAttributionSummaryResponse } from './processing/order-attribution'; -import { sanitizeReportCouponsResponse } from './processing/coupons'; -import { sanitizeReportCouponsByDateResponse } from './processing/coupons-by-date'; -import { sanitizeReportVisitorsResponse } from './processing/visitors'; -import { sanitizeReportVisitorsByLocationResponse } from './processing/visitors-by-location'; -import { sanitizeReportConversionRateResponse } from './processing/conversion-rate'; import { sanitizeReportOrdersByProductTypeResponse } from './processing/orders-by-product-type'; -import { sanitizeReportBookingsResponse } from './processing/bookings'; import { sanitizeReportSessionsByDeviceResponse } from './processing/sessions-by-device'; +import { sanitizeReportVisitorsResponse } from './processing/visitors'; +import { sanitizeReportVisitorsByLocationResponse } from './processing/visitors-by-location'; import type { ReportParams } from './utils/search'; export type ReportType = @@ -38,9 +35,7 @@ export type QueryParams = ReportParams & { }; // Inferred from processing/orders.ts -type SanitizedOrdersByDateResponse = ReturnType< - typeof sanitizeReportOrdersResponse ->; +type SanitizedOrdersByDateResponse = ReturnType< typeof sanitizeReportOrdersResponse >; // Inferred from processing/order-attribution.ts type SanitizedOrderAttributionSummaryResponse = ReturnType< @@ -48,34 +43,22 @@ type SanitizedOrderAttributionSummaryResponse = ReturnType< >; // Inferred from processing/coupons.ts -type SanitizedCouponsResponse = ReturnType< - typeof sanitizeReportCouponsResponse ->; +type SanitizedCouponsResponse = ReturnType< typeof sanitizeReportCouponsResponse >; // Inferred from processing/coupons-by-date/index.ts -type SanitizedCouponsByDateResponse = ReturnType< - typeof sanitizeReportCouponsByDateResponse ->; +type SanitizedCouponsByDateResponse = ReturnType< typeof sanitizeReportCouponsByDateResponse >; // Inferred from processing/customers.ts -type SanitizedCustomersResponse = ReturnType< - typeof sanitizeReportCustomersResponse ->; +type SanitizedCustomersResponse = ReturnType< typeof sanitizeReportCustomersResponse >; // Inferred from processing/customers-by-date/index.ts -type SanitizedCustomersByDateResponse = ReturnType< - typeof sanitizeReportCustomersByDateResponse ->; +type SanitizedCustomersByDateResponse = ReturnType< typeof sanitizeReportCustomersByDateResponse >; // Inferred from processing/products.ts -type SanitizedProductsResponse = ReturnType< - typeof sanitizeReportProductsResponse ->; +type SanitizedProductsResponse = ReturnType< typeof sanitizeReportProductsResponse >; // Inferred from processing/visitors.ts -type SanitizedVisitorsResponse = ReturnType< - typeof sanitizeReportVisitorsResponse ->; +type SanitizedVisitorsResponse = ReturnType< typeof sanitizeReportVisitorsResponse >; // Inferred from processing/visitors-by-location.ts type SanitizedVisitorsByLocationResponse = ReturnType< @@ -83,9 +66,7 @@ type SanitizedVisitorsByLocationResponse = ReturnType< >; // Inferred from processing/conversion-rate.ts -type SanitizedConversionRateResponse = ReturnType< - typeof sanitizeReportConversionRateResponse ->; +type SanitizedConversionRateResponse = ReturnType< typeof sanitizeReportConversionRateResponse >; // Inferred from processing/orders-by-product-type.ts type SanitizedOrdersByProductTypeResponse = ReturnType< @@ -93,9 +74,7 @@ type SanitizedOrdersByProductTypeResponse = ReturnType< >; // Inferred from processing/bookings.ts -type SanitizedBookingsResponse = ReturnType< - typeof sanitizeReportBookingsResponse ->; +type SanitizedBookingsResponse = ReturnType< typeof sanitizeReportBookingsResponse >; // Inferred from processing/sessions-by-device.ts type SanitizedSessionsByDeviceResponse = ReturnType< diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts index 2ad7b3fb685d..e6c2ea4e4a58 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import { tz } from '@date-fns/tz'; import { startOfDay, endOfDay, @@ -12,8 +13,6 @@ import { startOfYear, endOfYear, } from 'date-fns'; -import { tz } from '@date-fns/tz'; - /** * Mocks – getSiteTimezone and dateToISOStringWithLocalTZ * depend on WordPress core store. @@ -25,11 +24,8 @@ import { tz } from '@date-fns/tz'; */ jest.mock( '../date', () => ( { getSiteTimezone: jest.fn( () => '+00:00' ), - dateToISOStringWithLocalTZ: jest.fn( ( date: Date ) => - new Date( date.getTime() ).toISOString() - ), + dateToISOStringWithLocalTZ: jest.fn( ( date: Date ) => new Date( date.getTime() ).toISOString() ), } ) ); - /** * Internal dependencies */ @@ -49,6 +45,10 @@ const UTC = tz( '+00:00' ); * Normalize a TZDate or Date to Z-format ISO string, * ensuring the expected values match the mock's output format. */ +/** + * + * @param date + */ function toZ( date: Date ): string { return new Date( date.getTime() ).toISOString(); } @@ -120,24 +120,16 @@ describe( 'computeDateRangeFromPreset', () => { const range = computeDateRangeFromPreset( 'last-month' ); expect( range ).toBeDefined(); - expect( range!.from ).toBe( - toZ( startOfMonth( LAST_MONTH, { in: UTC } ) ) - ); - expect( range!.to ).toBe( - toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) - ); + expect( range!.from ).toBe( toZ( startOfMonth( LAST_MONTH, { in: UTC } ) ) ); + expect( range!.to ).toBe( toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) ); } ); it( 'returns last 12 calendar months for "last-12-months"', () => { const range = computeDateRangeFromPreset( 'last-12-months' ); expect( range ).toBeDefined(); - expect( range!.from ).toBe( - toZ( startOfMonth( subMonths( TODAY_START, 12 ), { in: UTC } ) ) - ); - expect( range!.to ).toBe( - toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) - ); + expect( range!.from ).toBe( toZ( startOfMonth( subMonths( TODAY_START, 12 ), { in: UTC } ) ) ); + expect( range!.to ).toBe( toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) ); } ); it( 'returns last calendar year for "last-year"', () => { @@ -145,9 +137,7 @@ describe( 'computeDateRangeFromPreset', () => { const lastYear = subYears( TODAY_START, 1 ); expect( range ).toBeDefined(); - expect( range!.from ).toBe( - toZ( startOfYear( lastYear, { in: UTC } ) ) - ); + expect( range!.from ).toBe( toZ( startOfYear( lastYear, { in: UTC } ) ) ); expect( range!.to ).toBe( toZ( endOfYear( lastYear, { in: UTC } ) ) ); } ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts index 88ca70758db1..1dc771865b48 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts @@ -1,5 +1,5 @@ /** - * Mocks – break the dependency chain to @wordpress/core-data. + * Mocks – break the dependency chain to `@wordpress/core-data`. */ jest.mock( '../../defaults', () => ( { getDefaultQueryParams: jest.fn(), @@ -12,7 +12,6 @@ jest.mock( '../preset-date-range', () => ( { jest.mock( '../interval', () => ( { getDefaultIntervalForPeriod: jest.fn(), } ) ); - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts index 25850845d3c8..00644be04800 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts @@ -12,14 +12,13 @@ jest.mock( '../preset-date-range', () => ( { jest.mock( '../interval', () => ( { getDefaultIntervalForPeriod: jest.fn(), } ) ); - /** * Internal dependencies */ -import { normalizeReportParams } from '../search'; import { getDefaultQueryParams } from '../../defaults'; -import { computeDateRangeFromPreset } from '../preset-date-range'; import { getDefaultIntervalForPeriod } from '../interval'; +import { computeDateRangeFromPreset } from '../preset-date-range'; +import { normalizeReportParams } from '../search'; import type { ReportParams } from '../search'; const mockGetDefaults = getDefaultQueryParams as jest.MockedFunction< @@ -86,9 +85,7 @@ describe( 'normalizeReportParams', () => { // Default comparison should be applied (search is undefined // → !search?.from → true → default branch). expect( result.comp ).toBe( '1' ); - expect( result.compare_from ).toBe( - DEFAULTS_WITH_COMPARISON.compare_from - ); + expect( result.compare_from ).toBe( DEFAULTS_WITH_COMPARISON.compare_from ); expect( result.compare_to ).toBe( DEFAULTS_WITH_COMPARISON.compare_to ); } ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/date.ts b/projects/packages/premium-analytics/packages/data/src/utils/date.ts index 011e8ae2b768..81f1950d8d4e 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/date.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts @@ -1,14 +1,14 @@ /** * External dependencies */ -import { select } from '@wordpress/data'; -import { store as coreStore, type Settings } from '@wordpress/core-data'; +import { type TZDate } from '@date-fns/tz'; import { toLocalTZ, formatToTimezoneNaiveString as _formatNaive, dateToISOStringWithTZ as _toISOWithTZ, } from '@jetpack-premium-analytics/datetime'; -import { type TZDate } from '@date-fns/tz'; +import { store as coreStore, type Settings } from '@wordpress/core-data'; +import { select } from '@wordpress/data'; type FullSettings = Settings & { gmt_offset: number; @@ -16,8 +16,7 @@ type FullSettings = Settings & { let DEFAULT_TIME_ZONE: string; try { - DEFAULT_TIME_ZONE = - Intl.DateTimeFormat().resolvedOptions().timeZone ?? '+00:00'; + DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone ?? '+00:00'; } catch { DEFAULT_TIME_ZONE = '+00:00'; } @@ -37,9 +36,10 @@ function formatGmtOffset( offset: number | undefined ): string { const abs = Math.abs( offset ); const hours = Math.floor( abs ); const minutes = Math.floor( ( abs - hours ) * 60 + 1e-6 ); - return `${ sign }${ String( hours ).padStart( 2, '0' ) }:${ String( - minutes - ).padStart( 2, '0' ) }`; + return `${ sign }${ String( hours ).padStart( 2, '0' ) }:${ String( minutes ).padStart( + 2, + '0' + ) }`; } /* @@ -50,11 +50,11 @@ function formatGmtOffset( offset: number | undefined ): string { * @param {string} timezone - The timezone to use. * @return {string} The timezone. */ +/** + * + */ export function getSiteTimezone() { - const siteSettings = select( coreStore ).getEntityRecord( - 'root', - 'site' - ) as FullSettings; + const siteSettings = select( coreStore ).getEntityRecord( 'root', 'site' ) as FullSettings; if ( ! siteSettings ) { return DEFAULT_TIME_ZONE; @@ -71,14 +71,9 @@ export function getSiteTimezone() { * @return {string} The site's GMT offset. */ export function getSiteGmtOffset(): string { - const siteSettings = select( coreStore ).getEntityRecord( - 'root', - 'site' - ) as FullSettings; + const siteSettings = select( coreStore ).getEntityRecord( 'root', 'site' ) as FullSettings; if ( ! siteSettings ) { - throw new Error( - 'getSiteGmtOffset() called before core settings are ready' - ); + throw new Error( 'getSiteGmtOffset() called before core settings are ready' ); } return formatGmtOffset( siteSettings?.gmt_offset ) || DEFAULT_TIME_ZONE; } @@ -88,11 +83,10 @@ export function getSiteGmtOffset(): string { * - Accepts number | string | Date (or undefined -> now) * - Uses site timezone by default * - Returns TZDate (timezone-aware) + * @param value + * @param timezone */ -export function localTZDate( - value?: number | string | Date, - timezone?: string -): TZDate { +export function localTZDate( value?: number | string | Date, timezone?: string ): TZDate { const tz = timezone ?? getSiteTimezone(); return toLocalTZ( value, tz ); } @@ -100,11 +94,10 @@ export function localTZDate( /** * Same semantics as your current helper: * TZ-aware -> timezone-naive "YYYY-MM-DDTHH:mm:ss.SSS" + * @param date + * @param timezone */ -export function formatToTimezoneNaiveString( - date: Date, - timezone?: string -): string { +export function formatToTimezoneNaiveString( date: Date, timezone?: string ): string { const tz = timezone ?? getSiteTimezone(); return _formatNaive( date, tz ); } @@ -112,11 +105,10 @@ export function formatToTimezoneNaiveString( /** * Same semantics as your current helper: * TZ-aware -> ISO with offset "YYYY-MM-DDTHH:mm:ss.SSSxxx" + * @param date + * @param timezone */ -export function dateToISOStringWithLocalTZ( - date: Date, - timezone?: string -): string { +export function dateToISOStringWithLocalTZ( date: Date, timezone?: string ): string { const tz = timezone ?? getSiteTimezone(); return _toISOWithTZ( date, tz ); } diff --git a/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts index 697202c07c86..cd00c8a6e0e5 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts @@ -1,8 +1,8 @@ /** * External dependencies */ -import { resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import { resolveSelect } from '@wordpress/data'; let readyPromise: Promise< void > | null = null; @@ -14,11 +14,7 @@ export function ensureCoreSettingsReady(): Promise< void > { if ( ! readyPromise ) { readyPromise = Promise.all( [ resolveSelect( coreStore ).getEntityRecord( 'root', 'site' ), - resolveSelect( coreStore ).getEntityRecord( - 'root', - 'settings', - 'general' - ), + resolveSelect( coreStore ).getEntityRecord( 'root', 'settings', 'general' ), ] ).then( () => void 0 ); } return readyPromise; diff --git a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts index e73a9a4e04b2..30c962cb435d 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts @@ -2,24 +2,23 @@ * External dependencies */ import { differenceInHours } from 'date-fns'; - /** * Internal dependencies */ -import type { IntervalType } from './search'; import { localTZDate } from './date'; +import type { IntervalType } from './search'; -function getAllowedIntervalsByRange( - from: string, - to: string -): IntervalType[] { +/** + * + * @param from + * @param to + */ +function getAllowedIntervalsByRange( from: string, to: string ): IntervalType[] { // Use hours instead of days to handle ranges that are 1 second short of a full day. // E.g., '2024-11-01 00:00:00' to '2025-10-31 23:59:59' is 8759 hours (364.958 days), // which rounds to 365 days, correctly categorizing it as a yearly interval. const daysDiff = Math.round( - Math.abs( - differenceInHours( localTZDate( to ), localTZDate( from ) ) / 24 - ) + Math.abs( differenceInHours( localTZDate( to ), localTZDate( from ) ) / 24 ) ); if ( daysDiff >= 1095 ) { @@ -42,6 +41,9 @@ function getAllowedIntervalsByRange( /** * Returns the allowed selectable intervals for a specific period. * + * @param period + * @param from + * @param to * @return {Array} Array containing allowed intervals. */ function getAllowedIntervalsForPeriod( @@ -69,6 +71,12 @@ function getAllowedIntervalsForPeriod( } } +/** + * + * @param period + * @param from + * @param to + */ export function getDefaultIntervalForPeriod( period: string | undefined, from: string, @@ -77,6 +85,12 @@ export function getDefaultIntervalForPeriod( return getAllowedIntervalsForPeriod( period, from, to )?.[ 0 ] ?? 'day'; } +/** + * + * @param period + * @param from + * @param to + */ export function getDateFormatFromInterval( period: string | undefined, // Pass in undefined to use the default interval. from: string, diff --git a/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts index 3ff816683937..8a4535a7231d 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts @@ -1,5 +1,7 @@ /** * Safe integer parsing with fallback value + * @param value + * @param fallback */ export function safeParseInt( value: unknown, fallback = 0 ): number { const num = parseInt( String( value ), 10 ); @@ -8,6 +10,8 @@ export function safeParseInt( value: unknown, fallback = 0 ): number { /** * Safe float parsing with fallback value + * @param value + * @param fallback */ export function safeParseFloat( value: unknown, fallback = 0 ): number { const num = parseFloat( String( value ) ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts index d3695405408c..cf0345d1003d 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts @@ -2,12 +2,11 @@ * External dependencies */ import { computePrimaryRange } from '@jetpack-premium-analytics/datetime'; +import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date'; import type { SelectablePresetId } from '@jetpack-premium-analytics/datetime'; - /** * Internal dependencies */ -import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date'; /** * Compute the absolute date range for a given preset ID diff --git a/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts index 7c000b4ea339..063b924a50b2 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts @@ -19,7 +19,5 @@ export function hasProductFilters( filters?: FilterCondition[] ): boolean { return false; } - return filters.some( ( filter ) => - PRODUCT_FILTER_KEYS.includes( filter.key ) - ); + return filters.some( filter => PRODUCT_FILTER_KEYS.includes( filter.key ) ); } diff --git a/projects/packages/premium-analytics/packages/data/src/utils/search.ts b/projects/packages/premium-analytics/packages/data/src/utils/search.ts index 4d01bd951f09..0aa97aacb501 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/search.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts @@ -7,34 +7,27 @@ import { type ComparisonPresetId, type PrimaryPresetId, } from '@jetpack-premium-analytics/datetime'; - /** * Internal dependencies */ -import { getDefaultQueryParams } from '../defaults'; import { ORDER_ATTRIBUTION_VIEWS } from '../api/report-order-attribution-summary-fetch'; +import { getDefaultQueryParams } from '../defaults'; import { getDefaultIntervalForPeriod } from './interval'; import { computeDateRangeFromPreset } from './preset-date-range'; -import type { FilterCondition } from '../types/filter-condition'; import type { DateType } from './types'; +import type { FilterCondition } from '../types/filter-condition'; export type { FilterCondition }; /** * Re-export SelectablePresetId as PresetType for backward compatibility. - * The canonical type now lives in @jetpack-premium-analytics/datetime. + * The canonical type now lives in `@jetpack-premium-analytics/datetime`. */ export type PresetType = SelectablePresetId; type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ]; -export type IntervalType = - | 'hour' - | 'day' - | 'week' - | 'month' - | 'quarter' - | 'year'; +export type IntervalType = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; /* * ReportParams are the expected params present in the client URL. @@ -64,18 +57,15 @@ type PartialComparisonFields = Partial< /* * Checks if the comparison is present in the search params. */ -export function hasComparisonEnabled< T extends PartialComparisonFields >( - p: T -) { - return ( - p.comp === '1' && !! p.compare_from?.trim() && !! p.compare_to?.trim() - ); +/** + * + * @param p + */ +export function hasComparisonEnabled< T extends PartialComparisonFields >( p: T ) { + return p.comp === '1' && !! p.compare_from?.trim() && !! p.compare_to?.trim(); } -type NormalizeReportParamsArgType = Omit< - ReportParams, - 'from' | 'to' | 'interval' | 'preset' -> & { +type NormalizeReportParamsArgType = Omit< ReportParams, 'from' | 'to' | 'interval' | 'preset' > & { from?: string; to?: string; interval?: string; @@ -86,8 +76,8 @@ type NormalizeReportParamsArgType = Omit< * Returns normalized params for the report request query. * When no defined, it will use the defaults. * - * @param {NormalizeReportParamsArgType} [search] URL search params. - * @param {PresetType} [defaultPreset] Override the fallback preset. + * @param {NormalizeReportParamsArgType} [search] - URL search params. + * @param {PresetType} [defaultPreset] - Override the fallback preset. */ export function normalizeReportParams( search?: NormalizeReportParamsArgType, From 876685389cf69085acedcd535eac03897a2f6740 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:17:19 +0800 Subject: [PATCH 05/32] chore(premium-analytics): wire data package deps and test setup --- pnpm-lock.yaml | 38 +++++++++++++++++++ .../premium-analytics/babel.config.cjs | 8 ++++ .../premium-analytics/eslint.config.mjs | 23 ++++++++--- .../packages/premium-analytics/package.json | 8 ++++ .../premium-analytics/tests/jest.config.cjs | 13 +++++++ 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 projects/packages/premium-analytics/babel.config.cjs create mode 100644 projects/packages/premium-analytics/tests/jest.config.cjs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54d70fd092e0..049d02c7b189 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3872,9 +3872,18 @@ importers: '@date-fns/tz': specifier: 1.4.1 version: 1.4.1 + '@tanstack/react-query': + specifier: 5.90.8 + version: 5.90.8(react@18.3.1) + '@wordpress/api-fetch': + specifier: 7.46.0 + version: 7.46.0 '@wordpress/boot': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/core-data': + specifier: 7.46.0 + version: 7.46.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/data': specifier: 10.46.0 version: 10.46.0(react@18.3.1) @@ -3890,6 +3899,9 @@ importers: '@wordpress/route': specifier: 0.12.0 version: 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/url': + specifier: 4.46.0 + version: 4.46.0 date-fns: specifier: 4.1.0 version: 4.1.0 @@ -3900,12 +3912,18 @@ importers: specifier: 18.3.1 version: 18.3.1(react@18.3.1) devDependencies: + '@automattic/jetpack-webpack-config': + specifier: workspace:* + version: link:../../js-packages/webpack-config '@babel/core': specifier: 7.29.0 version: 7.29.0 '@storybook/react': specifier: 10.3.6 version: 10.3.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.3.6(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.9.3) + '@tanstack/react-query-devtools': + specifier: 5.90.2 + version: 5.90.2(@tanstack/react-query@5.90.8(react@18.3.1))(react@18.3.1) '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 @@ -3924,6 +3942,9 @@ importers: browserslist: specifier: 4.28.2 version: 4.28.2 + jest: + specifier: 30.4.2 + version: 30.4.2 storybook: specifier: 10.3.6 version: 10.3.6(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9989,6 +10010,15 @@ packages: '@tanstack/query-core@5.90.8': resolution: {integrity: sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ==} + '@tanstack/query-devtools@5.90.1': + resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} + + '@tanstack/react-query-devtools@5.90.2': + resolution: {integrity: sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==} + peerDependencies: + '@tanstack/react-query': ^5.90.2 + react: ^18 || ^19 + '@tanstack/react-query@5.90.8': resolution: {integrity: sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ==} peerDependencies: @@ -22444,6 +22474,14 @@ snapshots: '@tanstack/query-core@5.90.8': {} + '@tanstack/query-devtools@5.90.1': {} + + '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.8(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-devtools': 5.90.1 + '@tanstack/react-query': 5.90.8(react@18.3.1) + react: 18.3.1 + '@tanstack/react-query@5.90.8(react@18.3.1)': dependencies: '@tanstack/query-core': 5.90.8 diff --git a/projects/packages/premium-analytics/babel.config.cjs b/projects/packages/premium-analytics/babel.config.cjs new file mode 100644 index 000000000000..ed6bf0680c44 --- /dev/null +++ b/projects/packages/premium-analytics/babel.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + presets: [ + [ + '@automattic/jetpack-webpack-config/babel/preset', + { pluginReplaceTextdomain: { textdomain: 'jetpack-premium-analytics' } }, + ], + ], +}; diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index 38a1a71417d7..46536db040d8 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -1,16 +1,16 @@ import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs'; /** - * Soften JSDoc rules for `packages/datetime/**` and `packages/formatters/**` - * so the initial ports can land with the upstream JSDoc style (descriptions - * on the function body, not on per-param tags). Temporary — backfill proper - * JSDoc on the helpers and remove these overrides (at which point this whole - * file can go away). + * Soften JSDoc rules for the internal `packages/*` ports so the initial + * ports can land with the upstream JSDoc style (descriptions on the + * function body, not on per-param tags). Temporary — backfill proper + * descriptions on the helpers and remove these overrides (at which point + * this whole file can go away). */ export default defineConfig( makeBaseConfig( import.meta.url ), { - files: [ 'packages/datetime/**' ], + files: [ 'packages/datetime/**', 'packages/data/**' ], rules: { 'jsdoc/require-description': 'off', 'jsdoc/require-param-description': 'off', @@ -27,5 +27,16 @@ export default defineConfig( 'jsdoc/require-returns': 'off', 'jsdoc/check-indentation': 'off', }, + }, + { + // The data port carries a couple of upstream patterns this temporary + // override keeps as-is: intentional `any` escapes for the generic report + // `TData` (see use-report.ts), and `react` flagged as extraneous because + // the internal package's deps are declared on the parent manifest. + files: [ 'packages/data/**' ], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'import/no-extraneous-dependencies': 'off', + }, } ); diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index fd3c20aab0a9..7060f8452bb1 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -7,6 +7,7 @@ "build": "wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", "build-production": "NODE_ENV=production wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php", "storybook": "cd ../../js-packages/storybook && pnpm run storybook:dev", + "test": "jest --config=tests/jest.config.cjs", "typecheck": "tsgo --noEmit", "watch": "wp-build --watch" }, @@ -32,25 +33,32 @@ "dependencies": { "@automattic/number-formatters": "workspace:*", "@date-fns/tz": "1.4.1", + "@tanstack/react-query": "5.90.8", + "@wordpress/api-fetch": "7.46.0", "@wordpress/boot": "0.13.0", + "@wordpress/core-data": "7.46.0", "@wordpress/data": "10.46.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^13.0.0", "@wordpress/primitives": "4.46.0", "@wordpress/route": "0.12.0", + "@wordpress/url": "4.46.0", "date-fns": "4.1.0", "react": "18.3.1", "react-dom": "18.3.1" }, "devDependencies": { + "@automattic/jetpack-webpack-config": "workspace:*", "@babel/core": "7.29.0", "@storybook/react": "10.3.6", + "@tanstack/react-query-devtools": "5.90.2", "@testing-library/dom": "10.4.1", "@types/jest": "30.0.0", "@typescript/native-preview": "7.0.0-dev.20260225.1", "@wordpress/build": "0.14.0", "@wordpress/ui": "0.13.0", "browserslist": "4.28.2", + "jest": "30.4.2", "storybook": "10.3.6", "typescript": "5.9.3" } diff --git a/projects/packages/premium-analytics/tests/jest.config.cjs b/projects/packages/premium-analytics/tests/jest.config.cjs new file mode 100644 index 000000000000..621d39dcc87a --- /dev/null +++ b/projects/packages/premium-analytics/tests/jest.config.cjs @@ -0,0 +1,13 @@ +const path = require( 'path' ); +const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); + +module.exports = { + ...baseConfig, + rootDir: path.join( __dirname, '..' ), + moduleNameMapper: { + ...baseConfig.moduleNameMapper, + // Resolve internal `packages/*` imports to their TypeScript source, + // mirroring the tsconfig `paths` alias (see README → "Internal packages"). + '^@jetpack-premium-analytics/(.*)$': path.join( __dirname, '..', 'packages', '$1', 'src' ), + }, +}; From 9b7a74c755bf15b7448651bcc3f7a2dbc801d425 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:17:43 +0800 Subject: [PATCH 06/32] changelog: add entry for premium-analytics data port --- .../wooa7s-1316-integrate-data-package-into-analytics | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics new file mode 100644 index 000000000000..d2029922c0f4 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Port data package (React Query report hooks, fetchers, and processing) as an internal package from next-woocommerce-analytics. From 2c8048b8fd24c2618e4f53f06ef1aa6d3b8bf7a3 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 11:23:34 +0800 Subject: [PATCH 07/32] docs(premium-analytics): use canonical package name in data README --- .../premium-analytics/packages/data/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/README.md b/projects/packages/premium-analytics/packages/data/README.md index 7db93ce258ff..c8492a081340 100644 --- a/projects/packages/premium-analytics/packages/data/README.md +++ b/projects/packages/premium-analytics/packages/data/README.md @@ -1,4 +1,4 @@ -# @jetpack-premium-analytics/data +# @automattic/jetpack-premium-analytics-data Data management for Jetpack Premium Analytics with React Query integration. @@ -15,7 +15,7 @@ import { useReport, prefetchReport, // ... other exports -} from '@jetpack-premium-analytics/data'; +} from '@automattic/jetpack-premium-analytics-data'; ``` ## Features @@ -36,7 +36,7 @@ import { ### Setup ```tsx -import { AnalyticsQueryClientProvider } from '@jetpack-premium-analytics/data'; +import { AnalyticsQueryClientProvider } from '@automattic/jetpack-premium-analytics-data'; function App() { return ( @@ -55,7 +55,7 @@ import { useReportOrdersByProductType, useReportOrderAttribution, useReportCoupons -} from '@jetpack-premium-analytics/data'; +} from '@automattic/jetpack-premium-analytics-data'; function OrdersReport() { // Orders endpoint separates primary and comparison periods @@ -105,7 +105,7 @@ function CouponsReport() { ### Prefetching ```tsx -import { prefetchReport, ensureCoreSettingsReady } from '@jetpack-premium-analytics/data'; +import { prefetchReport, ensureCoreSettingsReady } from '@automattic/jetpack-premium-analytics-data'; export const route = { beforeLoad: async () => { @@ -241,7 +241,7 @@ Returns the optimal default interval for a given time period. **Example:** ```tsx -import { getDefaultIntervalForPeriod } from '@jetpack-premium-analytics/data'; +import { getDefaultIntervalForPeriod } from '@automattic/jetpack-premium-analytics-data'; const interval = getDefaultIntervalForPeriod( 'last-7-days', from, to ); // Returns 'day' ``` @@ -254,7 +254,7 @@ Constant array of available order attribution views. **Example:** ```tsx -import { ORDER_ATTRIBUTION_VIEWS } from '@jetpack-premium-analytics/data'; +import { ORDER_ATTRIBUTION_VIEWS } from '@automattic/jetpack-premium-analytics-data'; // Use in components for view selection const views = ORDER_ATTRIBUTION_VIEWS; // ['channel', 'source', ...] @@ -333,7 +333,7 @@ Creates a timezone-aware date using the site's configured timezone by default. ```typescript -import { localTZDate } from '@jetpack-premium-analytics/data'; +import { localTZDate } from '@automattic/jetpack-premium-analytics-data'; const now = localTZDate(); // Current time in site timezone const custom = localTZDate( '2024-01-15', 'America/New_York' ); From 3dc2054cf7217a401c0bdb92e730cde501d701c0 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:31:20 +0800 Subject: [PATCH 08/32] feat(premium-analytics): copy routing package from next-woocommerce-analytics --- .../packages/routing/README.md | 133 ++++++++ .../packages/routing/package.json | 17 + .../packages/routing/src/hooks/index.ts | 1 + .../src/hooks/use-date-range-search/index.ts | 0 .../use-date-range-search.ts | 0 .../src/hooks/use-staged-search/README.md | 149 +++++++++ .../src/hooks/use-staged-search/index.ts | 1 + .../use-staged-search/use-staged-search.tsx | 293 ++++++++++++++++++ .../packages/routing/src/index.ts | 8 + .../comparison/derive-comparison-range.ts | 99 ++++++ .../routing/src/search/comparison/index.ts | 1 + .../src/search/date-range/date-range.ts | 110 +++++++ .../routing/src/search/date-range/index.ts | 5 + .../packages/routing/tsconfig.json | 9 + 14 files changed, 826 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/routing/README.md create mode 100644 projects/packages/premium-analytics/packages/routing/package.json create mode 100644 projects/packages/premium-analytics/packages/routing/src/hooks/index.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/hooks/use-date-range-search/index.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/hooks/use-date-range-search/use-date-range-search.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md create mode 100644 projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/index.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx create mode 100644 projects/packages/premium-analytics/packages/routing/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/search/comparison/index.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts create mode 100644 projects/packages/premium-analytics/packages/routing/src/search/date-range/index.ts create mode 100644 projects/packages/premium-analytics/packages/routing/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/routing/README.md b/projects/packages/premium-analytics/packages/routing/README.md new file mode 100644 index 000000000000..5032d8fa4884 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/README.md @@ -0,0 +1,133 @@ +# @next-woo-analytics/routing + +Utilities for handling **routing and URL search parameters** in +WooCommerce Analytics with TypeScript integration. + +This package centralizes logic for encoding and decoding route params so +that date ranges, filters, comparison parameters, and other query +parameters are handled consistently across the application. + +## Features + +- **Date range encoding** – Convert `DateRange` objects into ISO strings + with timezone support +- **Comparison parameters** – Handle `compare_from`, `compare_to`, + `compare_preset`, and `comp` flags +- **@wordpress/route integration** – Type-safe navigation with search + parameter management +- **Timezone handling** – Automatic timezone conversion for consistent + date handling +- **URL state persistence** – Maintains filter and comparison state + across page refreshes +- **Navigation utilities** – Write encoded parameters directly to the URL + using router navigation + +## Usage Examples + +### Date Range Navigation + +```typescript +import { writeDateRangeToSearch } from '@next-woo-analytics/routing'; +import { useNavigate } from '@wordpress/route'; + +function DateRangeSelector() { + const navigate = useNavigate(); + + const handleRangeChange = ( nextRange ) => { + writeDateRangeToSearch( { + navigate, + to: '/wc-analytics/dashboard', + range: nextRange, + search: { interval: 'day' } // Preserve other params + } ); + }; +} +``` + +### Comparison Parameter Management + +```typescript +import { writeComparisonToSearch } from '@next-woo-analytics/routing'; + +function ComparisonSelector() { + const navigate = useNavigate(); + + const handleComparisonChange = ( range, presetId ) => { + writeComparisonToSearch( { + navigate, + to: '/wc-analytics/dashboard', + range, + presetId, + enabled: !!range + } ); + }; +} +``` + +## API Reference + +### `writeDateRangeToSearch( options )` + +Writes a `DateRange` to the URL using the provided `navigate` function. + +**Parameters:** +- **`navigate`** – Navigation function from `useNavigate()` (`@wordpress/route`) +- **`to`** – Destination path (e.g., `'/wc-analytics/dashboard'`) +- **`range`** – `{ from: Date | undefined; to?: Date | undefined }` +- **`timezone?`** *(optional)* – Override timezone for date conversion +- **`search?`** *(optional)* – Additional search params to preserve/set + +**URL Parameters Generated:** +- `from` – ISO string with timezone offset +- `to` – ISO string with timezone offset + +### `writeComparisonToSearch( options )` + +Writes comparison parameters to the URL for period-over-period analysis. + +**Parameters:** +- **`navigate`** – Navigation function from `@wordpress/route` +- **`to`** – Destination path +- **`range?`** – Comparison date range +- **`presetId?`** – Preset identifier (e.g., 'previous_period') +- **`enabled?`** – Whether comparison is active +- **`timezone?`** – Override timezone +- **`search?`** – Additional search params + +**URL Parameters Generated:** +- `compare_from` – Comparison start date (ISO string) +- `compare_to` – Comparison end date (ISO string) +- `compare_preset` – Preset identifier +- `comp` – '1' when comparison enabled, undefined when disabled + +### `encodeDateToSearchParam( date?, timezone? )` + +Low-level function to convert a Date to an ISO string with timezone. + +**Parameters:** +- **`date?`** – Date to encode (returns undefined if not provided) +- **`timezone?`** – Timezone override + +**Returns:** ISO string with timezone offset or undefined + +## Architecture + +### URL Parameter Structure + +``` +/wc-analytics/dashboard? + from=2025-01-01T00:00:00-08:00& # Primary date range + to=2025-01-31T23:59:59-08:00& + interval=day& # Data granularity + compare_from=2024-12-01T00:00:00-08:00& # Comparison range + compare_to=2024-12-31T23:59:59-08:00& + compare_preset=previous_period& # Comparison preset + comp=1 # Comparison enabled flag +``` + +### Timezone Handling + +1. **Local Timezone Detection**: Uses site timezone from WordPress settings +2. **ISO String Generation**: Converts dates to ISO strings with timezone offset +3. **Consistent API Calls**: Ensures all API requests use properly formatted dates +4. **Cross-browser Support**: Handles timezone differences across different environments diff --git a/projects/packages/premium-analytics/packages/routing/package.json b/projects/packages/premium-analytics/packages/routing/package.json new file mode 100644 index 000000000000..0108098239e2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/package.json @@ -0,0 +1,17 @@ +{ + "name": "@next-woo-analytics/routing", + "description": "WooCommerce Analytics Routing", + "version": "1.0.0", + "type": "module", + "wpModule": true, + "main": "src/index.ts", + "exports": { + ".": "./build/src/index.js" + }, + "dependencies": { + "date-fns": "*", + "@tanstack/react-router": "*", + "@next-woo-analytics/data": "workspace:*", + "@next-woo-analytics/datetime": "workspace:*" + } +} diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/index.ts b/projects/packages/premium-analytics/packages/routing/src/hooks/index.ts new file mode 100644 index 000000000000..a29505a5a269 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/index.ts @@ -0,0 +1 @@ +export { useStagedSearch } from './use-staged-search'; diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-date-range-search/index.ts b/projects/packages/premium-analytics/packages/routing/src/hooks/use-date-range-search/index.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-date-range-search/use-date-range-search.ts b/projects/packages/premium-analytics/packages/routing/src/hooks/use-date-range-search/use-date-range-search.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md new file mode 100644 index 000000000000..973e6d53bf2b --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md @@ -0,0 +1,149 @@ +# `useStagedSearch` — staged UI + atomic URL commits + +Make the UI react instantly while the URL stays the source of truth. Edits are +staged locally and then committed atomically to the URL (one navigation). Back/ +Forward stays smooth. + +--- + +## Concepts + +* **committed**: current URL state (`useSearch`). +* **staged**: optimistic local edits (what the user is changing now). +* **effective**: `staged` merged over `committed` (ignoring `undefined`). + +Atomic commit: + +* `commit()` writes all staged changes in one `navigate({ search })`. +* Optional debounced auto-commit uses `replace: true` to avoid dirty history. +* On confirm, call `commit({ replace: false })` to push a history entry. + +--- + +## API + +```ts +type UseStagedSearchOptions< TFrom extends string > = { + from: TFrom; // TanStack route id/path + autoCommitDebounceMs?: number; // optional debounce in ms +}; + +type UseStagedSearchReturn< TSearch > = { + committed: TSearch; // current URL state + staged: TSearch; // optimistic local snapshot + effective: TSearch; // staged over committed per key + isSyncing: boolean; // true while committing + isDirty: boolean; // staged differs from committed + stage( patch: Partial< TSearch > ): void; + commit( opts?: { replace?: boolean } ): void; + revert(): void; + cancelAutoCommit(): void; +}; +``` + +Notes: + +* Internally uses `useSearch( { from } )` and `useNavigate( { from } )`. +* No `to` is passed on commit, so the current route is preserved. + +--- + +## Minimal usage + +```tsx +import { useMemo, useCallback } from 'react'; +import { + useStagedSearch, + encodeDateToSearchParam, +} from '@next-woo-analytics/routing'; +import { localTZDate } from '@next-woo-analytics/data'; +import type { DateRange } from '@next-woo-analytics/datetime'; + +type Search = { + from?: string; + to?: string; + compare_preset?: string; + comp?: string; +}; + +export function DashboardHeader() { + const { effective, stage, commit } = useStagedSearch< + Search, + '/wc-analytics/dashboard' + >( { + from: '/wc-analytics/dashboard', + // autoCommitDebounceMs: 250, + } ); + + const range = useMemo( () => ( { + from: effective.from ? localTZDate( effective.from ) : undefined, + to: effective.to ? localTZDate( effective.to ) : undefined, + } ), [ effective.from, effective.to ] ); + + const onRangeChange = useCallback( + ( next: DateRange | undefined ) => { + if ( ! next ) { + return; + } + stage( { + from: encodeDateToSearchParam( next.from ), + to: encodeDateToSearchParam( next.to ), + } ); + commit( { replace: false } ); + }, + [ stage, commit ] + ); + + // ... +} +``` + +--- + +## Best practices + +**What to use when** + +* Render and fetch: **`effective`** +* Inputs being edited: **`staged`** +* URL-driven side effects / analytics / share links: **`committed`** + +**Navigation and history** + +* Do not pass `to` on commit; update only `search` for SPA smoothness. +* Explicit commit: `commit( { replace: false } )` pushes history. +* Auto-commit (debounce): `replace: true` during continuous edits. +* The URL→UI mirror keeps Back/Forward fluid and flicker-free. + +**Data fetching** + +```ts +const { effective, isSyncing } = useStagedSearch< Search >( { + from: '/wc-analytics/dashboard', +} ); + +const query = useQuery( { + queryKey: [ 'orders', effective ], + enabled: ! isSyncing, + queryFn: () => fetchOrders( effective ), +} ); +``` + +**Debounce guidance** + +* `autoCommitDebounceMs`: 200–300 ms works well for date pickers. +* During edits → debounced replace-commits. +* On confirm (Apply/close) → `commit( { replace: false } )`. + +**Removing params** + +* `effective` ignores `undefined` in `staged`. To remove a key, stage it as + `undefined` and commit; the updater omits the key in the URL. + +**Avoid** + +* Writing the URL from multiple components (breaks atomicity). +* Mixing `useSearch()` reads in children that also depend on staging. +* Always using `replace: true` on explicit commits. + +--- diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/index.ts b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/index.ts new file mode 100644 index 000000000000..a29505a5a269 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/index.ts @@ -0,0 +1 @@ +export { useStagedSearch } from './use-staged-search'; diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx new file mode 100644 index 000000000000..419a403f9216 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx @@ -0,0 +1,293 @@ +/** + * External dependencies + */ +import { useNavigate, useSearch } from '@wordpress/route'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +type AnyObject = Record< string, unknown >; + +export type UseStagedSearchOptions< TFrom extends string > = { + from: TFrom; // e.g., '/wc-analytics/dashboard', + + /** + * If provided, stage() will schedule an automatic debounced commit + * after the given milliseconds. Those auto-commits use replace: true + * to avoid polluting the browser history during continuous interaction. + */ + autoCommitDebounceMs?: number; +}; + +export type UseStagedSearchReturn< TSearch extends AnyObject > = { + /** + * The current URL state. + */ + committed: TSearch; + + /** + * The optimistic snapshot for immediate UI. + */ + staged: TSearch; + + /** + * The effective state for rendering and data fetching. + */ + effective: TSearch; + + /** + * Whether the process is syncing. + */ + isSyncing: boolean; + + /** + * Whether the staged state differs from the committed state. + */ + isDirty: boolean; + + /** + * Stage a local patch without touching the URL. + */ + stage: ( patch: Partial< TSearch > ) => void; + + /** + * Commit all staged changes in a single atomic navigate(). + */ + commit: ( opts?: { replace?: boolean } ) => void; + + /** + * Discard local changes and return to committed snapshot. + */ + revert: () => void; + + /** + * Cancel pending debounced commit. + */ + cancelAutoCommit: () => void; +}; + +function shallowEqual( a: AnyObject, b: AnyObject ) { + if ( a === b ) { + return true; + } + + const ak = Object.keys( a ); + const bk = Object.keys( b ); + if ( ak.length !== bk.length ) { + return false; + } + + for ( const k of ak ) { + if ( a[ k ] !== b[ k ] ) { + return false; + } + } + + return true; +} + +function mergeDefined< T extends AnyObject >( + base: T, + patch: Partial< T > +): T { + const out: AnyObject = { ...base }; + for ( const key in patch ) { + const val = patch[ key as keyof T ]; + if ( val !== undefined ) { + out[ key ] = val as unknown; + } + } + return out as T; +} + +export function useStagedSearch< + TSearch extends AnyObject, + TFrom extends string, +>( opts: UseStagedSearchOptions< TFrom > ): UseStagedSearchReturn< TSearch > { + const navigate = useNavigate( { from: opts.from } ); + const committed = useSearch( { from: opts.from } ) as TSearch; + + /* + * Stage the search params. + */ + const [ staged, setStaged ] = useState< TSearch >( committed ); + + /* + * Track if the process is syncing. + */ + const [ isSyncing, setIsSyncing ] = useState( false ); // not used yet + + // Buffer for not-yet-committed changes. + const bufferRef = useRef< Partial< TSearch > >( {} ); + + // Debounce timer for auto-commit. + const timerRef = useRef< ReturnType< typeof setTimeout > | null >( null ); + + /** + * Mirror URL -> staged. + * If URL changes (back/forward or external writes), align staged snapshot. + * Also clear syncing flag after the router applies the new committed state. + */ + useEffect( () => { + setStaged( committed ); + bufferRef.current = {}; + if ( isSyncing ) { + setIsSyncing( false ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ committed ] ); + + /** + * Cancel pending debounced auto-commit. + */ + const cancelAutoCommit = useCallback( () => { + if ( timerRef.current ) { + clearTimeout( timerRef.current ); + timerRef.current = null; + } + }, [] ); + + /** + * Cleanup on unmount. + */ + useEffect( () => { + return () => { + cancelAutoCommit(); + }; + }, [ cancelAutoCommit ] ); + + /** + * Stage a local patch without touching the URL immediately. + * If autoCommitDebounceMs is set, schedule a debounced replace-commit. + */ + const stage = useCallback( + ( patch: Partial< TSearch > ) => { + setStaged( ( prev ) => ( { ...prev, ...patch } ) ); + bufferRef.current = { ...bufferRef.current, ...patch }; + + if ( typeof opts.autoCommitDebounceMs === 'number' ) { + cancelAutoCommit(); + timerRef.current = setTimeout( () => { + navigate( { + replace: true, // do not pollute history while interacting + search: ( prev ) => ( { + ...prev, + ...( bufferRef.current as Partial< TSearch > ), + } ), + } ); + timerRef.current = null; + }, opts.autoCommitDebounceMs ); + } + }, + [ navigate, opts.autoCommitDebounceMs, cancelAutoCommit ] + ); + + /** + * Commit all staged changes in a single atomic navigate(). + * - No `to`: keep the current route (prevents heavy remounts). + * - Default `replace` to false so history is preserved on explicit commits. + * - Cancels any pending debounced commit. + */ + const commit = useCallback( + ( commitOpts?: { replace?: boolean } ) => { + const patch = bufferRef.current; + const hasPatch = patch && Object.keys( patch ).length > 0; + + // Cancel any pending debounced replace-commit + cancelAutoCommit(); + + // If buffer is empty but staged differs from committed, compute a minimal diff + let diff: Partial< TSearch > | null = null; + if ( ! hasPatch ) { + const merged = { + ...( committed as AnyObject ), + ...( staged as AnyObject ), + } as TSearch; + + if ( + ! shallowEqual( + merged as AnyObject, + committed as AnyObject + ) + ) { + diff = {}; + for ( const key in merged ) { + // eslint-disable-next-line no-prototype-builtins + if ( + ( committed as AnyObject ).hasOwnProperty( key ) + ) { + if ( + ( committed as AnyObject )[ key ] !== + ( staged as AnyObject )[ key ] + ) { + ( diff as AnyObject )[ key ] = ( + staged as AnyObject + )[ key ]; + } + } else { + ( diff as AnyObject )[ key ] = ( + staged as AnyObject + )[ key ]; + } + } + } + } + + const finalPatch = hasPatch + ? ( patch as Partial< TSearch > ) + : diff; + + if ( ! finalPatch || Object.keys( finalPatch ).length === 0 ) { + return; + } + + setIsSyncing( true ); + + navigate( { + replace: commitOpts?.replace ?? false, // explicit commits push into history + search: ( prev ) => ( { + ...prev, + ...( finalPatch as Partial< TSearch > ), + } ), + } ); + + // isSyncing is flipped off by the committed->staged mirror effect. + }, + [ navigate, committed, staged, cancelAutoCommit ] + ); + + /** + * Discard local changes and return to committed snapshot. + */ + const revert = useCallback( () => { + cancelAutoCommit(); + bufferRef.current = {}; + setStaged( committed ); + }, [ committed, cancelAutoCommit ] ); + + /** + * Effective = committed merged with defined staged keys. + * Use this as the single source for rendering and data fetching. + */ + const effective = useMemo( + () => mergeDefined( committed, staged ), + [ committed, staged ] + ) as TSearch; + + /* + * Dirty if there is a buffer or staged differs from committed. + */ + const isDirty = + Object.keys( bufferRef.current ).length > 0 && + ! shallowEqual( staged as AnyObject, committed as AnyObject ); + + return { + committed, + staged, + effective, + isSyncing, + isDirty, + stage, + commit, + revert, + cancelAutoCommit, + }; +} diff --git a/projects/packages/premium-analytics/packages/routing/src/index.ts b/projects/packages/premium-analytics/packages/routing/src/index.ts new file mode 100644 index 000000000000..b6518f4c17a1 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/index.ts @@ -0,0 +1,8 @@ +export { + encodeDateToSearchParam, + writeDateRangeToSearch, + writeComparisonToSearch, +} from './search/date-range'; + +export { deriveComparisonRange } from './search/comparison'; +export { useStagedSearch } from './hooks'; diff --git a/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts b/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts new file mode 100644 index 000000000000..02f700827a06 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import { + normalizeReportParams, + dateToISOStringWithLocalTZ, + getSiteTimezone, +} from '@next-woo-analytics/data'; +import { + getComparisonRangeFromPreset, + type ComparisonPresetId, + startOfDayTZ, + endOfDayTZ, +} from '@next-woo-analytics/datetime'; + +type ReportParams = NonNullable< + Parameters< typeof normalizeReportParams >[ 0 ] +>; + +/** + * Normalize URL/UI comparison preset IDs to canonical ComparisonPresetId. + * Accepts variants with hyphen or underscore for robustness. + * + * @param value - Raw preset ID from URL or UI (e.g., 'previous_period' or 'previous-period') + * @return Canonical ComparisonPresetId or undefined if invalid + */ +const toComparisonPresetId = ( + value?: string +): ComparisonPresetId | undefined => { + switch ( value ) { + case 'previous-period': + case 'previous_period': + return 'previous-period'; + case 'previous-week': + case 'previous_week': + return 'previous-week'; + case 'previous-month': + case 'previous_month': + return 'previous-month'; + case 'previous-year': + case 'previous_year': + return 'previous-year'; + default: + return undefined; + } +}; + +/** + * Derive compare_from/compare_to from the main range + preset, + * honoring the site's timezone via existing data utils. + * + * Rules: + * - Only derive when comparison is enabled (comp === "1") AND a preset is present. + * - Normalize main range to site-local day bounds before computing presets. + * - Return ISO strings WITH site offset (same format you write to the URL). + */ +export function deriveComparisonRange( opts: ReportParams ): + | { + compare_from: string; + compare_to: string; + } + | undefined { + // Require comparison enabled + preset + const presetId = toComparisonPresetId( opts.compare_preset ); + if ( opts.comp !== '1' || ! presetId ) { + return undefined; + } + + // Need valid main range + if ( ! opts.from || ! opts.to ) { + return undefined; + } + + // Parse URL params (ISO+offset) to instants + const fromInstant = new Date( opts.from ); + const toInstant = new Date( opts.to ); + if ( isNaN( fromInstant.getTime() ) || isNaN( toInstant.getTime() ) ) { + return undefined; + } + + // Normalize to site-local day bounds + const timezone = getSiteTimezone(); + const reference = { + from: startOfDayTZ( fromInstant, timezone ), + to: endOfDayTZ( toInstant, timezone ), + }; + + // Compute comparison range (Dates) + const cmp = getComparisonRangeFromPreset( reference, presetId ); + if ( ! cmp?.from || ! cmp?.to ) { + return undefined; + } + + // Serialize back to ISO with site offset (string-to-string stable) + return { + compare_from: dateToISOStringWithLocalTZ( cmp.from ), + compare_to: dateToISOStringWithLocalTZ( cmp.to ), + }; +} diff --git a/projects/packages/premium-analytics/packages/routing/src/search/comparison/index.ts b/projects/packages/premium-analytics/packages/routing/src/search/comparison/index.ts new file mode 100644 index 000000000000..33bfe8d0427a --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/search/comparison/index.ts @@ -0,0 +1 @@ +export { deriveComparisonRange } from './derive-comparison-range'; diff --git a/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts b/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts new file mode 100644 index 000000000000..bb7d9be916cb --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import type { DateRange } from '@next-woo-analytics/datetime'; +import { + localTZDate, + dateToISOStringWithLocalTZ, +} from '@next-woo-analytics/data'; + +/** + * Serializes a Date into an ISO string with the site's timezone + * (or returns an empty string if no date is provided). + * Useful for writing dates to the URL and for API requests. + */ +export function encodeDateToSearchParam( + date?: Date, + timezone?: string +): string | undefined { + return date + ? dateToISOStringWithLocalTZ( localTZDate( date, timezone ) ) + : undefined; +} + +type WriteDateRangeToSearchProps = { + navigate: ( opts: { + to: string; + search: + | Record< string, string | undefined > + | ( ( + prev: Record< string, string | undefined > + ) => Record< string, string | undefined > ); + } ) => void; + to: string; + range: DateRange; + timezone?: string; + search?: Record< string, string | undefined | null >; +}; + +/** + * Writes a DateRange to the URL using navigate(). + * + * - Centralizes the conversion from Date -> ISO(+offset) according to + * the site's timezone. + * - If you need to preserve/propagate `interval` or other params, + * pass them in `search`. + * - Note: whether other existing params are preserved depends on the + * router's navigate implementation. This helper sets an explicit object. + */ +export function writeDateRangeToSearch( { + navigate, + to: toPath, + range, + timezone, + search, +}: WriteDateRangeToSearchProps ) { + const fromParam = encodeDateToSearchParam( range?.from, timezone ); + const toParam = encodeDateToSearchParam( range?.to, timezone ); + + navigate( { + to: toPath, + search: ( prev: Record< string, string | undefined > ) => ( { + ...prev, + from: fromParam, + to: toParam, + ...search, + } ), + } ); +} + +type WriteComparisonToSearchProps = { + navigate: ( opts: { + to: string; + search: + | Record< string, string | undefined > + | ( ( + prev: Record< string, string | undefined > + ) => Record< string, string | undefined > ); + } ) => void; + to: string; + range?: DateRange; + presetId?: string; + enabled?: boolean; + timezone?: string; + search?: Record< string, string | undefined | null >; +}; + +export function writeComparisonToSearch( { + navigate, + to: toPath, + range, + presetId, + enabled, + timezone, + search, +}: WriteComparisonToSearchProps ) { + const fromParam = encodeDateToSearchParam( range?.from, timezone ); + const toParam = encodeDateToSearchParam( range?.to, timezone ); + + navigate( { + to: toPath, + search: ( prev: Record< string, string | undefined > ) => ( { + ...prev, + compare_from: fromParam, + compare_to: toParam, + compare_preset: presetId ?? undefined, + comp: enabled ? '1' : undefined, + ...search, + } ), + } ); +} diff --git a/projects/packages/premium-analytics/packages/routing/src/search/date-range/index.ts b/projects/packages/premium-analytics/packages/routing/src/search/date-range/index.ts new file mode 100644 index 000000000000..4d8482211148 --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/src/search/date-range/index.ts @@ -0,0 +1,5 @@ +export { + encodeDateToSearchParam, + writeDateRangeToSearch, + writeComparisonToSearch, +} from './date-range'; diff --git a/projects/packages/premium-analytics/packages/routing/tsconfig.json b/projects/packages/premium-analytics/packages/routing/tsconfig.json new file mode 100644 index 000000000000..e66ececee7fa --- /dev/null +++ b/projects/packages/premium-analytics/packages/routing/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "sourceMap": true, + }, + "include": [ "src/**/*" ], + "exclude": [ "build", "node_modules" ] +} \ No newline at end of file From 0b8a086272d10ac20938fa4fa25cc8a26555a63c Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:32:09 +0800 Subject: [PATCH 09/32] refactor(premium-analytics): adapt routing package imports and manifest for monorepo --- .../packages/routing/README.md | 6 +++--- .../packages/routing/package.json | 19 ++++++++----------- .../src/hooks/use-staged-search/README.md | 6 +++--- .../comparison/derive-comparison-range.ts | 4 ++-- .../src/search/date-range/date-range.ts | 4 ++-- .../packages/routing/tsconfig.json | 9 --------- 6 files changed, 18 insertions(+), 30 deletions(-) delete mode 100644 projects/packages/premium-analytics/packages/routing/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/routing/README.md b/projects/packages/premium-analytics/packages/routing/README.md index 5032d8fa4884..c23e3e12815e 100644 --- a/projects/packages/premium-analytics/packages/routing/README.md +++ b/projects/packages/premium-analytics/packages/routing/README.md @@ -1,4 +1,4 @@ -# @next-woo-analytics/routing +# @jetpack-premium-analytics/routing Utilities for handling **routing and URL search parameters** in WooCommerce Analytics with TypeScript integration. @@ -27,7 +27,7 @@ parameters are handled consistently across the application. ### Date Range Navigation ```typescript -import { writeDateRangeToSearch } from '@next-woo-analytics/routing'; +import { writeDateRangeToSearch } from '@jetpack-premium-analytics/routing'; import { useNavigate } from '@wordpress/route'; function DateRangeSelector() { @@ -47,7 +47,7 @@ function DateRangeSelector() { ### Comparison Parameter Management ```typescript -import { writeComparisonToSearch } from '@next-woo-analytics/routing'; +import { writeComparisonToSearch } from '@jetpack-premium-analytics/routing'; function ComparisonSelector() { const navigate = useNavigate(); diff --git a/projects/packages/premium-analytics/packages/routing/package.json b/projects/packages/premium-analytics/packages/routing/package.json index 0108098239e2..8533c37fae25 100644 --- a/projects/packages/premium-analytics/packages/routing/package.json +++ b/projects/packages/premium-analytics/packages/routing/package.json @@ -1,17 +1,14 @@ { - "name": "@next-woo-analytics/routing", - "description": "WooCommerce Analytics Routing", - "version": "1.0.0", + "name": "@automattic/jetpack-premium-analytics-routing", + "version": "0.1.0", + "private": true, "type": "module", - "wpModule": true, "main": "src/index.ts", - "exports": { - ".": "./build/src/index.js" - }, + "types": "src/index.ts", + "sideEffects": false, "dependencies": { - "date-fns": "*", - "@tanstack/react-router": "*", - "@next-woo-analytics/data": "workspace:*", - "@next-woo-analytics/datetime": "workspace:*" + "@jetpack-premium-analytics/data": "workspace:*", + "@jetpack-premium-analytics/datetime": "workspace:*", + "@wordpress/route": "0.12.0" } } diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md index 973e6d53bf2b..543a91695132 100644 --- a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md @@ -55,9 +55,9 @@ import { useMemo, useCallback } from 'react'; import { useStagedSearch, encodeDateToSearchParam, -} from '@next-woo-analytics/routing'; -import { localTZDate } from '@next-woo-analytics/data'; -import type { DateRange } from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/routing'; +import { localTZDate } from '@jetpack-premium-analytics/data'; +import type { DateRange } from '@jetpack-premium-analytics/datetime'; type Search = { from?: string; diff --git a/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts b/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts index 02f700827a06..b63cbf3ad127 100644 --- a/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts +++ b/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts @@ -5,13 +5,13 @@ import { normalizeReportParams, dateToISOStringWithLocalTZ, getSiteTimezone, -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; import { getComparisonRangeFromPreset, type ComparisonPresetId, startOfDayTZ, endOfDayTZ, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; type ReportParams = NonNullable< Parameters< typeof normalizeReportParams >[ 0 ] diff --git a/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts b/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts index bb7d9be916cb..ebb4a144cb61 100644 --- a/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts +++ b/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts @@ -1,11 +1,11 @@ /** * External dependencies */ -import type { DateRange } from '@next-woo-analytics/datetime'; +import type { DateRange } from '@jetpack-premium-analytics/datetime'; import { localTZDate, dateToISOStringWithLocalTZ, -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; /** * Serializes a Date into an ISO string with the site's timezone diff --git a/projects/packages/premium-analytics/packages/routing/tsconfig.json b/projects/packages/premium-analytics/packages/routing/tsconfig.json deleted file mode 100644 index e66ececee7fa..000000000000 --- a/projects/packages/premium-analytics/packages/routing/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "build", - "sourceMap": true, - }, - "include": [ "src/**/*" ], - "exclude": [ "build", "node_modules" ] -} \ No newline at end of file From 8f9cc38b65856376013cda16e2b32b072610e8b2 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:36:36 +0800 Subject: [PATCH 10/32] style(premium-analytics): align ported routing package with jetpack lint and prettier --- .../use-staged-search/use-staged-search.tsx | 60 +++++++++---------- .../comparison/derive-comparison-range.ts | 9 +-- .../src/search/date-range/date-range.ts | 41 +++++++------ 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx index 419a403f9216..4b8ab233e360 100644 --- a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx @@ -64,6 +64,11 @@ export type UseStagedSearchReturn< TSearch extends AnyObject > = { cancelAutoCommit: () => void; }; +/** + * + * @param a + * @param b + */ function shallowEqual( a: AnyObject, b: AnyObject ) { if ( a === b ) { return true; @@ -84,10 +89,12 @@ function shallowEqual( a: AnyObject, b: AnyObject ) { return true; } -function mergeDefined< T extends AnyObject >( - base: T, - patch: Partial< T > -): T { +/** + * + * @param base + * @param patch + */ +function mergeDefined< T extends AnyObject >( base: T, patch: Partial< T > ): T { const out: AnyObject = { ...base }; for ( const key in patch ) { const val = patch[ key as keyof T ]; @@ -98,10 +105,13 @@ function mergeDefined< T extends AnyObject >( return out as T; } -export function useStagedSearch< - TSearch extends AnyObject, - TFrom extends string, ->( opts: UseStagedSearchOptions< TFrom > ): UseStagedSearchReturn< TSearch > { +/** + * + * @param opts + */ +export function useStagedSearch< TSearch extends AnyObject, TFrom extends string >( + opts: UseStagedSearchOptions< TFrom > +): UseStagedSearchReturn< TSearch > { const navigate = useNavigate( { from: opts.from } ); const committed = useSearch( { from: opts.from } ) as TSearch; @@ -160,7 +170,7 @@ export function useStagedSearch< */ const stage = useCallback( ( patch: Partial< TSearch > ) => { - setStaged( ( prev ) => ( { ...prev, ...patch } ) ); + setStaged( prev => ( { ...prev, ...patch } ) ); bufferRef.current = { ...bufferRef.current, ...patch }; if ( typeof opts.autoCommitDebounceMs === 'number' ) { @@ -168,7 +178,7 @@ export function useStagedSearch< timerRef.current = setTimeout( () => { navigate( { replace: true, // do not pollute history while interacting - search: ( prev ) => ( { + search: prev => ( { ...prev, ...( bufferRef.current as Partial< TSearch > ), } ), @@ -202,38 +212,22 @@ export function useStagedSearch< ...( staged as AnyObject ), } as TSearch; - if ( - ! shallowEqual( - merged as AnyObject, - committed as AnyObject - ) - ) { + if ( ! shallowEqual( merged as AnyObject, committed as AnyObject ) ) { diff = {}; for ( const key in merged ) { // eslint-disable-next-line no-prototype-builtins - if ( - ( committed as AnyObject ).hasOwnProperty( key ) - ) { - if ( - ( committed as AnyObject )[ key ] !== - ( staged as AnyObject )[ key ] - ) { - ( diff as AnyObject )[ key ] = ( - staged as AnyObject - )[ key ]; + if ( ( committed as AnyObject ).hasOwnProperty( key ) ) { + if ( ( committed as AnyObject )[ key ] !== ( staged as AnyObject )[ key ] ) { + ( diff as AnyObject )[ key ] = ( staged as AnyObject )[ key ]; } } else { - ( diff as AnyObject )[ key ] = ( - staged as AnyObject - )[ key ]; + ( diff as AnyObject )[ key ] = ( staged as AnyObject )[ key ]; } } } } - const finalPatch = hasPatch - ? ( patch as Partial< TSearch > ) - : diff; + const finalPatch = hasPatch ? ( patch as Partial< TSearch > ) : diff; if ( ! finalPatch || Object.keys( finalPatch ).length === 0 ) { return; @@ -243,7 +237,7 @@ export function useStagedSearch< navigate( { replace: commitOpts?.replace ?? false, // explicit commits push into history - search: ( prev ) => ( { + search: prev => ( { ...prev, ...( finalPatch as Partial< TSearch > ), } ), diff --git a/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts b/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts index b63cbf3ad127..f680fc4b5d4e 100644 --- a/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts +++ b/projects/packages/premium-analytics/packages/routing/src/search/comparison/derive-comparison-range.ts @@ -13,9 +13,7 @@ import { endOfDayTZ, } from '@jetpack-premium-analytics/datetime'; -type ReportParams = NonNullable< - Parameters< typeof normalizeReportParams >[ 0 ] ->; +type ReportParams = NonNullable< Parameters< typeof normalizeReportParams >[ 0 ] >; /** * Normalize URL/UI comparison preset IDs to canonical ComparisonPresetId. @@ -24,9 +22,7 @@ type ReportParams = NonNullable< * @param value - Raw preset ID from URL or UI (e.g., 'previous_period' or 'previous-period') * @return Canonical ComparisonPresetId or undefined if invalid */ -const toComparisonPresetId = ( - value?: string -): ComparisonPresetId | undefined => { +const toComparisonPresetId = ( value?: string ): ComparisonPresetId | undefined => { switch ( value ) { case 'previous-period': case 'previous_period': @@ -53,6 +49,7 @@ const toComparisonPresetId = ( * - Only derive when comparison is enabled (comp === "1") AND a preset is present. * - Normalize main range to site-local day bounds before computing presets. * - Return ISO strings WITH site offset (same format you write to the URL). + * @param opts */ export function deriveComparisonRange( opts: ReportParams ): | { diff --git a/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts b/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts index ebb4a144cb61..b615430bedfd 100644 --- a/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts +++ b/projects/packages/premium-analytics/packages/routing/src/search/date-range/date-range.ts @@ -1,24 +1,18 @@ /** * External dependencies */ +import { localTZDate, dateToISOStringWithLocalTZ } from '@jetpack-premium-analytics/data'; import type { DateRange } from '@jetpack-premium-analytics/datetime'; -import { - localTZDate, - dateToISOStringWithLocalTZ, -} from '@jetpack-premium-analytics/data'; /** * Serializes a Date into an ISO string with the site's timezone * (or returns an empty string if no date is provided). * Useful for writing dates to the URL and for API requests. + * @param date + * @param timezone */ -export function encodeDateToSearchParam( - date?: Date, - timezone?: string -): string | undefined { - return date - ? dateToISOStringWithLocalTZ( localTZDate( date, timezone ) ) - : undefined; +export function encodeDateToSearchParam( date?: Date, timezone?: string ): string | undefined { + return date ? dateToISOStringWithLocalTZ( localTZDate( date, timezone ) ) : undefined; } type WriteDateRangeToSearchProps = { @@ -26,9 +20,7 @@ type WriteDateRangeToSearchProps = { to: string; search: | Record< string, string | undefined > - | ( ( - prev: Record< string, string | undefined > - ) => Record< string, string | undefined > ); + | ( ( prev: Record< string, string | undefined > ) => Record< string, string | undefined > ); } ) => void; to: string; range: DateRange; @@ -45,6 +37,12 @@ type WriteDateRangeToSearchProps = { * pass them in `search`. * - Note: whether other existing params are preserved depends on the * router's navigate implementation. This helper sets an explicit object. + * @param root0 + * @param root0.navigate + * @param root0.to + * @param root0.range + * @param root0.timezone + * @param root0.search */ export function writeDateRangeToSearch( { navigate, @@ -72,9 +70,7 @@ type WriteComparisonToSearchProps = { to: string; search: | Record< string, string | undefined > - | ( ( - prev: Record< string, string | undefined > - ) => Record< string, string | undefined > ); + | ( ( prev: Record< string, string | undefined > ) => Record< string, string | undefined > ); } ) => void; to: string; range?: DateRange; @@ -84,6 +80,17 @@ type WriteComparisonToSearchProps = { search?: Record< string, string | undefined | null >; }; +/** + * + * @param root0 + * @param root0.navigate + * @param root0.to + * @param root0.range + * @param root0.presetId + * @param root0.enabled + * @param root0.timezone + * @param root0.search + */ export function writeComparisonToSearch( { navigate, to: toPath, From e349a33b7eba6f8e47a0d3613ca8c65a9147ceb0 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:44:19 +0800 Subject: [PATCH 11/32] chore(premium-analytics): extend eslint config for ported routing package --- projects/packages/premium-analytics/eslint.config.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index 46536db040d8..1fae06be0834 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -10,7 +10,7 @@ import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs export default defineConfig( makeBaseConfig( import.meta.url ), { - files: [ 'packages/datetime/**', 'packages/data/**' ], + files: [ 'packages/datetime/**', 'packages/data/**', 'packages/routing/**' ], rules: { 'jsdoc/require-description': 'off', 'jsdoc/require-param-description': 'off', @@ -38,5 +38,14 @@ export default defineConfig( '@typescript-eslint/no-explicit-any': 'off', 'import/no-extraneous-dependencies': 'off', }, + }, + { + // The routing port imports `react` directly (the staged-search hook), + // flagged as extraneous because the internal package's deps are declared + // on the parent manifest. + files: [ 'packages/routing/**' ], + rules: { + 'import/no-extraneous-dependencies': 'off', + }, } ); From 26063eb558934c6410919452770c84ae35939750 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:44:24 +0800 Subject: [PATCH 12/32] changelog: add entry for premium-analytics routing port --- .../wooa7s-1317-integrate-routing-package-into-analytics | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1317-integrate-routing-package-into-analytics diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1317-integrate-routing-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1317-integrate-routing-package-into-analytics new file mode 100644 index 000000000000..904cd0aaf523 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1317-integrate-routing-package-into-analytics @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Port routing package (date-range/comparison search-param helpers and the staged-search hook) as an internal package from next-woocommerce-analytics. From efd9362527000e29827fda2a50bedc5d410e6f77 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 11:29:13 +0800 Subject: [PATCH 13/32] docs(premium-analytics): use canonical package name in routing READMEs --- .../packages/premium-analytics/packages/routing/README.md | 6 +++--- .../packages/routing/src/hooks/use-staged-search/README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/packages/premium-analytics/packages/routing/README.md b/projects/packages/premium-analytics/packages/routing/README.md index c23e3e12815e..bfeefb8ff1fc 100644 --- a/projects/packages/premium-analytics/packages/routing/README.md +++ b/projects/packages/premium-analytics/packages/routing/README.md @@ -1,4 +1,4 @@ -# @jetpack-premium-analytics/routing +# @automattic/jetpack-premium-analytics-routing Utilities for handling **routing and URL search parameters** in WooCommerce Analytics with TypeScript integration. @@ -27,7 +27,7 @@ parameters are handled consistently across the application. ### Date Range Navigation ```typescript -import { writeDateRangeToSearch } from '@jetpack-premium-analytics/routing'; +import { writeDateRangeToSearch } from '@automattic/jetpack-premium-analytics-routing'; import { useNavigate } from '@wordpress/route'; function DateRangeSelector() { @@ -47,7 +47,7 @@ function DateRangeSelector() { ### Comparison Parameter Management ```typescript -import { writeComparisonToSearch } from '@jetpack-premium-analytics/routing'; +import { writeComparisonToSearch } from '@automattic/jetpack-premium-analytics-routing'; function ComparisonSelector() { const navigate = useNavigate(); diff --git a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md index 543a91695132..43f6bd7b19c0 100644 --- a/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md +++ b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/README.md @@ -55,9 +55,9 @@ import { useMemo, useCallback } from 'react'; import { useStagedSearch, encodeDateToSearchParam, -} from '@jetpack-premium-analytics/routing'; -import { localTZDate } from '@jetpack-premium-analytics/data'; -import type { DateRange } from '@jetpack-premium-analytics/datetime'; +} from '@automattic/jetpack-premium-analytics-routing'; +import { localTZDate } from '@automattic/jetpack-premium-analytics-data'; +import type { DateRange } from '@automattic/jetpack-premium-analytics-datetime'; type Search = { from?: string; From b92a3b6979ff1c08701edd295b523578de2d3c9c Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:52:42 +0800 Subject: [PATCH 14/32] feat(premium-analytics): add ui package from next-woocommerce-analytics components --- .../packages/ui/package.json | 25 ++ .../date-comparison-dropdown.scss | 34 ++ .../date-comparison-dropdown.tsx | 170 ++++++++ .../ui/src/date-comparison-dropdown/index.ts | 1 + .../date-filters-panel/date-filters-panel.tsx | 246 +++++++++++ .../ui/src/date-filters-panel/index.ts | 1 + .../date-range-input/date-range-input.scss | 36 ++ .../src/date-range-input/date-range-input.tsx | 119 +++++ .../packages/ui/src/date-range-input/index.ts | 1 + .../date-range-popover/date-range-filter.scss | 105 +++++ .../date-range-popover/date-range-filter.tsx | 411 ++++++++++++++++++ .../ui/src/date-range-popover/index.ts | 2 + .../date-range-presets.scss | 27 ++ .../date-range-presets/date-range-presets.tsx | 148 +++++++ .../ui/src/date-range-presets/index.ts | 29 ++ .../packages/ui/src/index.ts | 1 + .../src/use-comparison-date-presets/index.ts | 2 + .../use-comparison-date-presets.ts | 54 +++ .../packages/ui/tsconfig.json | 9 + 19 files changed, 1421 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/ui/package.json create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts create mode 100644 projects/packages/premium-analytics/packages/ui/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/ui/package.json b/projects/packages/premium-analytics/packages/ui/package.json new file mode 100644 index 000000000000..746a1f67760b --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@next-woo-analytics/components", + "description": "WooCommerce Analytics components", + "version": "1.0.0", + "type": "module", + "wpModule": true, + "main": "src/index.ts", + "exports": { + ".": "./build/src/index.js" + }, + "dependencies": { + "@automattic/ui": "*", + "date-fns": "*", + "@next-woo-analytics/data": "workspace:*", + "@next-woo-analytics/datetime": "workspace:*", + "@wc-analytics/formatters": "workspace:*", + "@wordpress/components": "*", + "@automattic/admin-toolkit": "*", + "@automattic/design-system": "*", + "@wordpress/icons": "*", + "@wordpress/compose": "*", + "@wordpress/ui": "*", + "clsx": "*" + } +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss new file mode 100644 index 000000000000..93626fe01f0b --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss @@ -0,0 +1,34 @@ +.date-comparison-dropdown { + &__button { + background-color: var( --wpds-color-bg-surface-neutral-strong ); + } +} + +.date-filters-panel-button { + background-color: var( --wpds-color-bg-surface-neutral-strong ); +} + +.date-comparison-dropdown__popover { + width: 235px; +} + +/* disable animation for the date range popover */ +/* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ +@media not (prefers-reduced-motion: reduce) { + .date-comparison-dropdown__popover { + view-transition-name: next-admin--date-comparison-dropdown; + transition: none !important; + } +} + +/* ensure it's above the canvas/stage during the transition */ +::view-transition-group(next-admin--date-comparison-dropdown) { + z-index: 3000; +} + +/* no animation for the snapshot (avoid "flashing") */ +::view-transition-new(next-admin--date-comparison-dropdown), +::view-transition-old(next-admin--date-comparison-dropdown) { + animation: none; +} +/* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx new file mode 100644 index 000000000000..5334eabbca17 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx @@ -0,0 +1,170 @@ +/** + * External dependencies + */ +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import { Button } from '@wordpress/ui'; +import { formatDateRange } from '@wc-analytics/formatters'; +import { sprintf, __ } from '@wordpress/i18n'; +import { useMemo } from 'react'; +import type { ComparisonPresetId } from '@next-woo-analytics/datetime'; + +const { Menu } = unlock( componentsPrivateApis ); + +/** + * Internal dependencies + */ +import { DateRangePresets } from '../date-range-presets'; +import type { ComparisonDateRangePreset } from '../use-comparison-date-presets'; +import './date-comparison-dropdown.scss'; + +type DateComparisonDropdownProps = { + /** + * Available comparison presets (e.g., previous-period, previous-month) + */ + presets: ComparisonDateRangePreset[]; + /** + * Whether comparison is enabled + */ + enabled: boolean; + /** + * Currently selected comparison preset ID + */ + presetId?: ComparisonPresetId; + /** + * Whether to remove "Compare to:" prefix from button label + */ + removeCompareToPrefix?: boolean; + /** + * Callback when comparison is enabled + */ + onEnable: () => void; + /** + * Callback when a comparison preset is selected + */ + onPresetChange: ( id: ComparisonPresetId ) => void; + /** + * Callback when comparison is cleared + */ + onClear: () => void; +}; + +export function DateComparisonDropdown( { + presets, + enabled, + presetId, + removeCompareToPrefix = false, + onEnable, + onPresetChange, + onClear, +}: DateComparisonDropdownProps ) { + const selectedPreset = useMemo( + () => + presetId ? presets.find( ( p ) => p.id === presetId ) : undefined, + [ presets, presetId ] + ); + + const comparisonRange = selectedPreset?.range; + const hasValidPreset = !! comparisonRange; + const hasPresets = presets.length > 0; + + if ( ! enabled ) { + return ( + + + { __( 'No comparison', 'woocommerce-analytics' ) } + + } + /> + + + + + { __( + 'No comparison', + 'woocommerce-analytics' + ) } + + + + + + { __( + 'Comparison to past', + 'woocommerce-analytics' + ) } + + + + + + ); + } + + let label: string = __( 'Select comparison', 'woocommerce-analytics' ); + if ( hasValidPreset ) { + if ( removeCompareToPrefix ) { + label = formatDateRange( comparisonRange ); + } else { + label = sprintf( + // translators: %s is the comparison range label + __( 'Compare to: %s', 'woocommerce-analytics' ), + formatDateRange( comparisonRange ) + ); + } + } + + return ( + + + { label } + + } + /> + + { hasPresets && ( + { + /* + * Type assertion is safe here because: + * 1. presets is ComparisonDateRangePreset[] (strongly typed) + * 2. DateRangePresets picks id from our presets array + * 3. Therefore id must be ComparisonPresetId + */ + onPresetChange( id as ComparisonPresetId ); + } } + onClear={ onClear } + /> + ) } + + + ); +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts new file mode 100644 index 000000000000..89ca07f36f91 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts @@ -0,0 +1 @@ +export { DateComparisonDropdown } from './date-comparison-dropdown'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx new file mode 100644 index 000000000000..ff4b7498adfd --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx @@ -0,0 +1,246 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import { BaseControl } from '@wordpress/components'; +import { useMemo, useCallback } from 'react'; +import { + isComparisonPresetId, + isPrimaryPreset, + type ComparisonPresetId, + type PrimaryPresetId, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePopover } from '../date-range-popover'; +import { DateComparisonDropdown } from '../date-comparison-dropdown'; +import { useComparisonDatePresets } from '../use-comparison-date-presets'; + +type DateRangePopoverProps = Parameters< typeof DateRangePopover >[ 0 ]; + +export type DateRange = DateRangePopoverProps[ 'range' ]; + +export type DateFiltersPanelProps = { + /** + * The current date range preset ID (e.g., 'last-7-days', 'last-30-days'). + */ + presetId?: PrimaryPresetId; + + /** + * The current primary date range. + */ + range: DateRange; + + /** + * The current comparison preset ID (e.g., 'previous-period', 'previous-month'). + */ + comparisonPresetId?: ComparisonPresetId; + + /** + * Callback when the primary date range changes. + */ + onChange: DateRangePopoverProps[ 'onChange' ]; + + /** + * Callback when the comparison date range changes. + * Receives the calculated comparison range and the preset ID used. + */ + onComparisonChange: ( + range: DateRange | undefined, + presetId?: ComparisonPresetId + ) => void; + + /** + * Props for the date range popover. + */ + rangeControlProps?: Omit< + Parameters< typeof BaseControl >[ 0 ], + 'children' + >; + + /** + * Props for the date comparison dropdown. + */ + comparisonControlProps?: Omit< + Parameters< typeof BaseControl >[ 0 ], + 'children' + >; + + /** + * Callback when the primary date range is applied. + */ + onApply: DateRangePopoverProps[ 'onApply' ]; + + /** + * Callback when the primary date range is canceled. + */ + onCancel: DateRangePopoverProps[ 'onCancel' ]; + + /** + * Whether the primary date range can be applied. + */ + canApply?: boolean; + + /** + * IANA timezone string (e.g., 'America/New_York', 'Europe/London'). + * Required for proper date/time handling. + */ + timeZone: string; + + /** + * Optional external container element for responsive calculations. + * When provided, the DateRangePopover will measure this container's width + * instead of its own wrapper to determine mobile/wide layouts. + */ + containerElement?: HTMLElement | null; +}; + +/** + * DateFiltersPanel - Manages date range selection and comparison controls + * + * This component serves as the container for date filtering functionality, + * managing both the primary date range selection and the comparison date range. + * It owns the comparison state and delegates to child components for UI. + */ +export function DateFiltersPanel( { + presetId, + range, + comparisonPresetId, + onChange, + onComparisonChange, + rangeControlProps = { + label: null, + help: null, + }, + comparisonControlProps = { + label: null, + help: null, + }, + onApply, + onCancel, + canApply = true, + timeZone, + containerElement, +}: DateFiltersPanelProps ) { + /** + * Validate and normalize the primary preset ID. + * Only accepts built-in preset IDs (including 'custom'). + * Invalid/unknown values are treated as undefined, which allows + * DateRangePopover to handle them gracefully (falls back to custom). + */ + const validatedPresetId = useMemo( () => { + if ( ! presetId ) { + return undefined; + } + // Only accept known built-in presets + // Unknown/garbage values from URL are rejected to prevent UI inconsistency + return isPrimaryPreset( presetId ) ? presetId : undefined; + }, [ presetId ] ); + + // Validate and normalize the comparison preset ID + const validatedComparisonPresetId = useMemo( () => { + return isComparisonPresetId( comparisonPresetId ) + ? comparisonPresetId + : undefined; + }, [ comparisonPresetId ] ); + + // Derive comparison enabled state directly from validated prop + const comparisonEnabled = !! validatedComparisonPresetId; + + // Get available presets for the current range + const presets = useComparisonDatePresets( range ); + + /** + * Determines the default preset ID to use when comparison is enabled. + * Priority order: + * 1. 'previous-period' + * 2. 'previous-month' + * 3. First available preset + */ + const defaultPresetId = useMemo( () => { + return ( + presets.find( ( p ) => p.id === 'previous-period' )?.id ?? + presets.find( ( p ) => p.id === 'previous-month' )?.id ?? + presets[ 0 ]?.id + ); + }, [ presets ] ); + + /** + * Currently selected comparison preset, + * based on the validated stored preset ID, or the default preset. + * Returns undefined if no preset is selected + * or if the ID doesn't match any available preset. + */ + const preset = useMemo( () => { + const id = validatedComparisonPresetId ?? defaultPresetId; + return id ? presets.find( ( p ) => p.id === id ) : undefined; + }, [ presets, validatedComparisonPresetId, defaultPresetId ] ); + + const presetChange = useCallback( + ( id: ComparisonPresetId ) => { + const nextPreset = presets.find( ( p ) => p.id === id ); + onComparisonChange( nextPreset?.range, id ); + }, + [ onComparisonChange, presets ] + ); + + /** + * Handles clearing the comparison completely. + * Clears the selected preset and notifies parent. + */ + const clearComparison = useCallback( () => { + onComparisonChange( undefined, undefined ); + }, [ onComparisonChange ] ); + + const handleEnable = useCallback( () => { + // Use validated ID with fallback to default + const presetIdToUse = validatedComparisonPresetId ?? defaultPresetId; + if ( preset?.range && presetIdToUse ) { + onComparisonChange( preset.range, presetIdToUse ); + } + }, [ + onComparisonChange, + preset, + validatedComparisonPresetId, + defaultPresetId, + ] ); + + return ( + + + + + + + + + + ); +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts new file mode 100644 index 000000000000..9a1d3322329e --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts @@ -0,0 +1 @@ +export { DateFiltersPanel } from './date-filters-panel'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss new file mode 100644 index 000000000000..754e7105acaf --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss @@ -0,0 +1,36 @@ +.input-date-control { + flex: 1; + font-size: var( --wpds-font-size-sm ); + + @supports selector(&::-webkit-calendar-picker-indicator) { + input[type="date"] { + // Removes extra spaces for the calendar icon. + width: fit-content; + padding-right: 0; + + appearance: none; + -webkit-appearance: none; + background: none; + + font-size: var( --wpds-font-size-md ); + + // Removes the calendar icon. + &::-webkit-calendar-picker-indicator { + display: none; + } + } + } + + @supports not selector(&::-webkit-calendar-picker-indicator) { + input[type="date"] { + padding: 0; // We'll control input's inner spacing manually + + min-width: fit-content; // Prevent extra space on smaller screens + + // Use flex to center the input's content horizontally + display: flex; + width: calc(100% - 20px); // Take almost all the space + margin-inline: auto; // Keep things centered + } + } +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx new file mode 100644 index 000000000000..740951cfd3a4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Field, Input, Stack } from '@wordpress/ui'; +import { useCallback, useEffect, useState } from 'react'; +import { createTZDateFromParts } from '@next-woo-analytics/datetime'; +import { formatDate } from '@wc-analytics/formatters'; + +/** + * Internal dependencies + */ +import { DateRangePopover } from '../date-range-popover/date-range-filter'; +import './date-range-input.scss'; + +type DateRangeInputProps = Pick< + Parameters< typeof DateRangePopover >[ 0 ], + 'range' | 'onChange' +> & { + timeZone: string; +}; + +type DateInputProps = Pick< DateRangeInputProps, 'timeZone' > & { + label: string; + date?: Date; + onChange: ( date?: Date ) => void; +}; + +const formatToString = ( date?: Date ) => + date ? formatDate( date, 'iso' ) : ''; + +function parseFromString( dateString: string, timeZone: string ) { + const [ year, month, day ] = dateString + .split( '-' ) + .map( ( x ) => Number( x ) ); + + const parsedDate = createTZDateFromParts( + [ year, month - 1, day ], + timeZone + ); + + return ! isNaN( parsedDate.getTime() ) ? parsedDate : undefined; +} + +function DateInput( { label, date, onChange, timeZone }: DateInputProps ) { + const [ value, setValue ] = useState( formatToString( date ) ); + + useEffect( () => { + setValue( formatToString( date ) ); + }, [ date ] ); + + const onInputChange = useCallback( + ( event: React.ChangeEvent< HTMLInputElement > ) => { + const newValue = event.target.value; + setValue( newValue ); + + const newDate = parseFromString( newValue, timeZone ); + + // Call onChange only when the date is complete and reasonable, to avoid unwanted updates. + // Also avoids parseFromString auto-filling partial input (e.g. "20" → "1920"). + if ( newDate && newDate.getFullYear() > 2000 ) { + onChange( newDate ); + } + }, + [ onChange, timeZone ] + ); + + const onClick = useCallback( ( e: React.MouseEvent ) => { + // Prevents the date input from opening the browser date picker, + // as we want to use a custom date picker elsewhere. + e.preventDefault(); + }, [] ); + + return ( + + { label } + + + ); +} + +export function DateRangeInput( { + range, + onChange, + timeZone, +}: DateRangeInputProps ) { + const { from, to } = range; + + return ( + + { + if ( nextFrom && to && nextFrom <= to ) { + onChange( { from: nextFrom, to } ); + } + } } + /> + + { + if ( nextTo && from && from <= nextTo ) { + onChange( { from, to: nextTo } ); + } + } } + /> + + ); +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts new file mode 100644 index 000000000000..d42bdcd634fe --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts @@ -0,0 +1 @@ +export { DateRangeInput } from './date-range-input'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss new file mode 100644 index 000000000000..b0bc48158ac7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss @@ -0,0 +1,105 @@ +@use "@wordpress/base-styles/variables" as vars; +@use "@wordpress/base-styles/colors" as colors; + +.date-range-popover-content { + --wca-popover-padding: var( --wpds-dimension-padding-lg ); + --wca-popover-border-color: #{colors.$gray-300}; + --wca-popover-border-width: #{vars.$border-width}; + + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: 1fr auto; + + // Grid lines: background = line color, gap = line width + background-color: var( --wca-popover-border-color ); + column-gap: var( --wca-popover-border-width ); + row-gap: var( --wca-popover-border-width ); + + .date-range-calendar { + display: flex; + justify-content: center; + align-items: center; + } + + // Mobile layout: override grid to use flex column + &--mobile { + display: flex; + flex-direction: column; + gap: var( --wpds-dimension-gap-lg ); + padding: var( --wca-popover-padding ); + background-color: #fff; + + .date-range-calendar { + --a8c-calendar-button-width: 32px; + --a8c-calendar-button-height: 32px; + } + + .date-range-popover-actions { + padding: 0; + padding-block-start: var( --wpds-dimension-gap-sm ); + } + } +} + +.date-range-presets-wrapper { + grid-row: 1; + display: grid; + grid-template-columns: minmax(0, max-content) 1fr; + background-color: #fff; + padding-bottom: var( --wpds-dimension-gap-sm ); + width: calc( 60 * var( --wpds-dimension-base ) ); + padding-right: var( --wpds-dimension-padding-sm ); +} + +.date-filters-panel-button { + background-color: #fff; // ToDo: handle this upstream. +} + +.date-range-calendar-wrapper { + --wca-calendar-button-width: 32px; + --wca-calendar-button-height: 32px; + --wca-calendar-button-gap: 1rem; // consistent with automattic/ui style + --wca-calendar-padding: var( --wca-popover-padding ); + --wca-calendar-width: calc( var( --wca-calendar-button-width ) * 7 + var( --wca-calendar-padding ) * 2 ); + --wca-calendar-width-wide: calc( var( --wca-calendar-button-width ) * 14 + var( --wca-calendar-padding ) * 2 + var( --wca-calendar-button-gap ) ); + + grid-row: 1; + padding: var( --wca-calendar-padding ); + width: var( --wca-calendar-width ); + background-color: #fff; + + .date-range-calendar { + --a8c-calendar-button-width: var( --wca-calendar-button-width ); + --a8c-calendar-button-height: var( --wca-calendar-button-height ); + } + + &__wide { + width: var( --wca-calendar-width-wide ); + } +} + +.date-range-popover-actions { + grid-column: 1 / -1; + padding: calc( var( --wca-popover-padding ) / 2 ) var( --wca-popover-padding ); + background-color: #fff; +} + +/* disable animation for the date range popover */ +/* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ +@media not (prefers-reduced-motion: reduce) { + .date-filters-panel__popover { + view-transition-name: next-admin--date-range-popover; + } +} + +/* ensure it's above the canvas/stage during the transition */ +::view-transition-group(next-admin--date-range-popover) { + z-index: 3000; +} + +/* no animation for the snapshot (avoid "flashing") */ +::view-transition-new(next-admin--date-range-popover), +::view-transition-old(next-admin--date-range-popover) { + animation: none; +} +/* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx new file mode 100644 index 000000000000..d09f8b634c51 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx @@ -0,0 +1,411 @@ +/** + * External dependencies + */ +import { DateRangeCalendar } from '@automattic/ui'; +import { + Dropdown, + SelectControl, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import { calendar } from '@wordpress/icons'; +import { Badge, Button, Stack } from '@wordpress/ui'; +import { useState, useCallback, useMemo, useEffect } from 'react'; +import { useResizeObserver } from '@wordpress/compose'; +import { formatDateRange } from '@wc-analytics/formatters'; +import { __ } from '@wordpress/i18n'; +import clsx from 'clsx'; +import '@automattic/ui/style.css'; +import { + getPresetLabel, + getDefaultDateRangePresets, + PRESET_CUSTOM, + type PrimaryPresetId, + type DateRangePreset, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePresets } from '../date-range-presets'; +import { DateRangeInput } from '../date-range-input'; +import './date-range-filter.scss'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const { Menu } = unlock( componentsPrivateApis ); + +/** + * Threshold width (in pixels) below which we consider the layout "mobile". + * This is based on the container width, not the viewport. + */ +const MOBILE_CONTAINER_WIDTH_THRESHOLD = 480; + +/** + * Date range type from @automattic/ui. + * Represents a range with `from` and `to` Date objects. + */ +export type DateRange = NonNullable< + Parameters< typeof DateRangeCalendar >[ 0 ][ 'selected' ] +>; + +/** + * Props for DateRangePopoverContent component. + */ +type DateRangePopoverContentProps = { + /** + * Currently selected preset identifier + */ + presetId?: PrimaryPresetId; + + /** + * The selected date range + */ + range: DateRange; + + /** + * Callback when range or preset changes + */ + onChange: ( range?: DateRange, preset?: PrimaryPresetId ) => void; + + /** + * Callback when user applies the selection + */ + onApply: () => void; + + /** + * Callback when user cancels the selection + */ + onCancel: () => void; + + /** + * Whether the Apply button should be enabled + */ + canApply: boolean; + + /** + * Whether to show wide screen layout (2 months) + */ + isWideScreen?: boolean; + + /** + * Whether to show mobile layout (dropdown presets instead of sidebar) + */ + isMobile?: boolean; + + /** + * IANA timezone string (e.g., 'America/New_York', 'Europe/London'). + * Required for proper date/time handling. + */ + timeZone: string; +}; + +/** + * Props for DateRangePresetsDropdown component. + */ +type DateRangePresetsDropdownProps = { + value: PrimaryPresetId | null; + onRangeChange: ( range: DateRange, id: PrimaryPresetId ) => void; + presets?: DateRangePreset[]; + timeZone: string; +}; + +function getDisplayedMonth( range: DateRange ): Date { + return range?.from ?? new Date(); +} + +/** + * Action buttons for the date range popover (Cancel/Apply). + */ +function DateRangePopoverActions( { + onCancel, + onApply, + canApply, +}: Pick< DateRangePopoverContentProps, 'onCancel' | 'onApply' | 'canApply' > ) { + return ( + + + + + ); +} + +/** + * Dropdown version of DateRangePresets for mobile layout. + * Displays presets as a SelectControl instead of a menu list. + */ +function DateRangePresetsDropdown( { + value, + onRangeChange, + presets: presetsProp, + timeZone, +}: DateRangePresetsDropdownProps ) { + const defaultPresets = useMemo( + () => ( presetsProp ? [] : getDefaultDateRangePresets( timeZone ) ), + [ presetsProp, timeZone ] + ); + const presets = presetsProp || defaultPresets; + + const options = useMemo( + () => [ + ...presets.map( ( { id, label } ) => ( { + value: id, + label, + } ) ), + { + value: PRESET_CUSTOM, + label: __( 'Custom range', 'woocommerce-analytics' ), + }, + ], + [ presets ] + ); + + const handleChange = useCallback( + ( selectedValue: string ) => { + const preset = presets.find( ( p ) => p.id === selectedValue ); + if ( preset ) { + onRangeChange( preset.range, preset.id ); + } + }, + [ presets, onRangeChange ] + ); + + return ( + + ); +} + +/** + * Content of the DateRangePopover, extracted for Storybook visualization. + * This component is exported for internal use only (stories, testing). + */ +export function DateRangePopoverContent( { + presetId, + range, + onChange, + onApply, + onCancel, + canApply, + isWideScreen = false, + isMobile = false, + timeZone, +}: DateRangePopoverContentProps ) { + const [ displayedMonth, setDisplayedMonth ] = useState( + getDisplayedMonth( range ) + ); + + const handleChange = ( + nextRange?: DateRange, + nextPrimaryPresetId?: PrimaryPresetId + ) => { + if ( nextRange ) { + setDisplayedMonth( getDisplayedMonth( nextRange ) ); + } + + // If nextPrimaryPresetId is undefined, the user manually changed the dates + // (via calendar or input fields), so we switch to PRESET_CUSTOM + const effectivePrimaryPresetId = nextPrimaryPresetId ?? PRESET_CUSTOM; + + onChange( nextRange, effectivePrimaryPresetId ); + }; + + // Mobile layout: single column with dropdown presets + if ( isMobile ) { + return ( +
+ + + + + handleChange( nextRange ) } + numberOfMonths={ 1 } + month={ displayedMonth } + onMonthChange={ setDisplayedMonth } + timeZone={ timeZone } + /> + + +
+ ); + } + + // Desktop layout: grid with sidebar presets + return ( +
+
+ + + +
+ + + + + handleChange( nextRange ) } + numberOfMonths={ isWideScreen ? 2 : 1 } + month={ displayedMonth } + onMonthChange={ setDisplayedMonth } + timeZone={ timeZone } + /> + + + +
+ ); +} + +type DateRangePopoverProps = Omit< + DateRangePopoverContentProps, + 'isWideScreen' | 'isMobile' +> & { + /** + * Optional external container element for responsive calculations. + * When provided, the component will measure this container's width + * instead of its own wrapper to determine mobile/wide layouts. + */ + containerElement?: HTMLElement | null; +}; + +/** + * Threshold width (in pixels) for showing 2 months in calendar. + * Based on CSS: --wca-calendar-width-wide (~500px for 2 months + presets sidebar) + */ +const WIDE_CONTAINER_THRESHOLD = 780; + +export function DateRangePopover( { + presetId, + range, + onChange, + onApply, + onCancel, + canApply, + timeZone, + containerElement, +}: DateRangePopoverProps ) { + const [ containerWidth, setContainerWidth ] = useState< number | null >( + null + ); + + // Callback to update container width + const handleResize = useCallback( ( entries: ResizeObserverEntry[] ) => { + const entry = entries[ 0 ]; + if ( entry ) { + setContainerWidth( entry.contentRect.width ); + } + }, [] ); + + // ResizeObserver for the reference container + const setObserverRef = useResizeObserver< HTMLElement >( handleResize ); + + // Attach observer to containerElement if provided, otherwise use document.body + useEffect( () => { + const element = containerElement ?? document.body; + setObserverRef( element ); + }, [ containerElement, setObserverRef ] ); + + // Determine layout based on container width + const isMobile = + containerWidth !== null && + containerWidth < MOBILE_CONTAINER_WIDTH_THRESHOLD; + + const isWideScreen = + containerWidth !== null && containerWidth >= WIDE_CONTAINER_THRESHOLD; + + const presetLabel = getPresetLabel( presetId ); + + return ( + ( + + ) } + renderContent={ ( { onClose } ) => ( + { + onApply(); + onClose(); + } } + onCancel={ () => { + onCancel(); + onClose(); + } } + canApply={ canApply } + isWideScreen={ isWideScreen } + isMobile={ isMobile } + timeZone={ timeZone } + /> + ) } + /> + ); +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts new file mode 100644 index 000000000000..9d4c1f786baf --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts @@ -0,0 +1,2 @@ +export { DateRangePopover, DateRangePopoverContent } from './date-range-filter'; +export type { DateRange } from './date-range-filter'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss new file mode 100644 index 000000000000..1c1f1306fc48 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss @@ -0,0 +1,27 @@ +@use "@wordpress/base-styles/variables" as vars; +@use "@wordpress/base-styles/colors" as colors; + +.date-range-presets{ + max-width: 240px; +} + +.date-range-presets, +.date-range-presets__custom-group { + .date-range-presets__item { + min-height: var( --wpds-font-line-height-2xl ); + } +} + +.date-range-presets__custom-group { + // Custom button acts as a label, not an interactive element. + // Override disabled styles to show selection state visually. + .date-range-presets__custom { + &[aria-disabled="true"] { + color: var( --wpds-color-fg-content-neutral-weak ); + } + + &[aria-checked="true"] { + color: var( --wpds-color-fg-content-neutral ); + } + } +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx new file mode 100644 index 000000000000..e60fd29057b5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx @@ -0,0 +1,148 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import { useMemo } from 'react'; +import { + PRESET_CUSTOM, + getDefaultDateRangePresets, + type PrimaryPresetId, + type DateRangePreset, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePopover } from '../date-range-popover/date-range-filter'; +import './date-range-presets.scss'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const { Menu } = unlock( componentsPrivateApis ); + +type DateRange = Parameters< typeof DateRangePopover >[ 0 ][ 'range' ]; + +/** + * Props for the DateRangePresets component. + */ +type DateRangePresetsProps = { + /** + * Callback fired when a preset is selected + */ + onRangeChange: ( range: DateRange, id: PrimaryPresetId ) => void; + + /** + * Currently selected preset ID, or null if none + */ + value: PrimaryPresetId | null; + + /** + * IANA timezone string (e.g., 'America/New_York'). + * Required when using default presets. Optional if explicit presets are provided. + */ + timeZone?: string; + + /** + * Custom presets to display instead of defaults + */ + presets?: DateRangePreset[]; + + /** + * Whether to show the custom date option + */ + supportCustom?: boolean; + + /** + * Optional callback to clear/remove comparison. + * When provided, shows a "No comparison" option. + */ + onClear?: () => void; + + /** + * Whether clicking a preset item should close the parent popover. + * Defaults to undefined (Ariakit default: checkbox items stay open). + */ + hideOnClick?: boolean; +}; + +export function DateRangePresets( { + onRangeChange, + value, + timeZone, + presets: presetsProp, + onClear, + hideOnClick, +}: DateRangePresetsProps ) { + const defaultPresets = useMemo( () => { + if ( presetsProp ) { + return []; + } + + if ( ! timeZone ) { + throw new Error( + 'DateRangePresets: `timeZone` is required when `presets` are not provided.' + ); + } + + return getDefaultDateRangePresets( timeZone ); + }, [ presetsProp, timeZone ] ); + + const presets = useMemo( + () => presetsProp || defaultPresets, + [ presetsProp, defaultPresets ] + ); + + return ( + <> + + { presets.map( ( { id, label, range: presetRange } ) => ( + onRangeChange( presetRange, id ) } + hideOnClick={ hideOnClick } + > + { label } + + ) ) } + + + + + + + + { __( 'Custom', 'woocommerce-analytics' ) } + + + + { onClear && ( + + + { __( 'No comparison', 'woocommerce-analytics' ) } + + + ) } + + + ); +} diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts new file mode 100644 index 000000000000..2d34bfc5454d --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts @@ -0,0 +1,29 @@ +export { DateRangePresets } from './date-range-presets'; + +/** + * Re-export types, constants, and guards from datetime + * so existing consumers of this barrel continue to work. + */ +export { + getDefaultDateRangePresets, + getPresetLabel, + isSelectablePreset, + isPrimaryPreset, + // Preset constants + PRESET_TODAY, + PRESET_YESTERDAY, + PRESET_LAST_7_DAYS, + PRESET_LAST_30_DAYS, + PRESET_LAST_90_DAYS, + PRESET_LAST_365_DAYS, + PRESET_LAST_MONTH, + PRESET_LAST_12_MONTHS, + PRESET_LAST_YEAR, + PRESET_CUSTOM, +} from '@next-woo-analytics/datetime'; + +export type { + PrimaryPresetId, + SelectablePresetId, + DateRangePreset, +} from '@next-woo-analytics/datetime'; diff --git a/projects/packages/premium-analytics/packages/ui/src/index.ts b/projects/packages/premium-analytics/packages/ui/src/index.ts new file mode 100644 index 000000000000..9a1d3322329e --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/index.ts @@ -0,0 +1 @@ +export { DateFiltersPanel } from './date-filters-panel'; diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts new file mode 100644 index 000000000000..945c2ce54278 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts @@ -0,0 +1,2 @@ +export { useComparisonDatePresets } from './use-comparison-date-presets'; +export type { ComparisonDateRangePreset } from './use-comparison-date-presets'; diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts new file mode 100644 index 000000000000..a7f9f48e731f --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { + getComparisonRangeFromPreset, + getComparisonPresetConfigs, + type ComparisonPresetId, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import type { DateRange } from '../date-range-popover/date-range-filter'; + +/** + * A comparison-specific date range preset. + * Similar to DateRangePreset but with a strongly-typed ComparisonPresetId. + */ +export type ComparisonDateRangePreset = { + id: ComparisonPresetId; + label: string; + range: DateRange; +}; + +/** + * Custom hook that generates comparison date presets + * based on a reference date range. + * + * @param referenceRange - The primary date range to compare against + * @return Array of comparison presets with strongly-typed IDs + */ +export function useComparisonDatePresets( + referenceRange: DateRange +): ComparisonDateRangePreset[] { + return useMemo( () => { + if ( ! referenceRange.from || ! referenceRange.to ) { + return []; + } + + return getComparisonPresetConfigs() + .map( ( { id, label } ) => { + const range = getComparisonRangeFromPreset( + referenceRange, + id + ); + return range ? { id, label, range } : null; + } ) + .filter( + ( preset ): preset is ComparisonDateRangePreset => + preset !== null + ); + }, [ referenceRange ] ); +} diff --git a/projects/packages/premium-analytics/packages/ui/tsconfig.json b/projects/packages/premium-analytics/packages/ui/tsconfig.json new file mode 100644 index 000000000000..18b7c843caa5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "skipLibCheck": true + }, + "include": [ "src/**/*" ], + "exclude": [ "build", "node_modules" ] +} \ No newline at end of file From 6998770ff46a3328703d938a8518def467f901c0 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:53:43 +0800 Subject: [PATCH 15/32] refactor(premium-analytics): adapt ui package imports and manifest for monorepo --- .../packages/ui/package.json | 37 +++++++++---------- .../date-comparison-dropdown.tsx | 14 +++---- .../date-filters-panel/date-filters-panel.tsx | 2 +- .../src/date-range-input/date-range-input.tsx | 8 ++-- .../date-range-popover/date-range-filter.tsx | 10 ++--- .../date-range-presets/date-range-presets.tsx | 6 +-- .../ui/src/date-range-presets/index.ts | 4 +- .../use-comparison-date-presets.ts | 2 +- .../packages/ui/tsconfig.json | 9 ----- 9 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 projects/packages/premium-analytics/packages/ui/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/ui/package.json b/projects/packages/premium-analytics/packages/ui/package.json index 746a1f67760b..1aae2e2cda00 100644 --- a/projects/packages/premium-analytics/packages/ui/package.json +++ b/projects/packages/premium-analytics/packages/ui/package.json @@ -1,25 +1,24 @@ { - "name": "@next-woo-analytics/components", - "description": "WooCommerce Analytics components", - "version": "1.0.0", + "name": "@automattic/jetpack-premium-analytics-ui", + "version": "0.1.0", + "private": true, "type": "module", - "wpModule": true, "main": "src/index.ts", - "exports": { - ".": "./build/src/index.js" - }, + "types": "src/index.ts", + "sideEffects": [ + "*.scss" + ], "dependencies": { - "@automattic/ui": "*", - "date-fns": "*", - "@next-woo-analytics/data": "workspace:*", - "@next-woo-analytics/datetime": "workspace:*", - "@wc-analytics/formatters": "workspace:*", - "@wordpress/components": "*", - "@automattic/admin-toolkit": "*", - "@automattic/design-system": "*", - "@wordpress/icons": "*", - "@wordpress/compose": "*", - "@wordpress/ui": "*", - "clsx": "*" + "@automattic/ui": "1.0.2", + "@jetpack-premium-analytics/datetime": "workspace:*", + "@jetpack-premium-analytics/formatters": "workspace:*", + "@wordpress/components": "33.1.0", + "@wordpress/compose": "7.46.0", + "@wordpress/i18n": "^6.9.0", + "@wordpress/icons": "^13.0.0", + "@wordpress/private-apis": "1.46.0", + "@wordpress/ui": "0.13.0", + "clsx": "2.1.1", + "react": "18.3.1" } } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx index 5334eabbca17..3088dd941c6c 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx @@ -4,10 +4,10 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { unlock } from '@automattic/admin-toolkit'; import { Button } from '@wordpress/ui'; -import { formatDateRange } from '@wc-analytics/formatters'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { sprintf, __ } from '@wordpress/i18n'; import { useMemo } from 'react'; -import type { ComparisonPresetId } from '@next-woo-analytics/datetime'; +import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; const { Menu } = unlock( componentsPrivateApis ); @@ -80,7 +80,7 @@ export function DateComparisonDropdown( { size="compact" id="date-comparison-dropdown-button" > - { __( 'No comparison', 'woocommerce-analytics' ) } + { __( 'No comparison', 'jetpack-premium-analytics' ) } } /> @@ -94,7 +94,7 @@ export function DateComparisonDropdown( { { __( 'No comparison', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ) } @@ -109,7 +109,7 @@ export function DateComparisonDropdown( { { __( 'Comparison to past', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ) } @@ -119,14 +119,14 @@ export function DateComparisonDropdown( { ); } - let label: string = __( 'Select comparison', 'woocommerce-analytics' ); + let label: string = __( 'Select comparison', 'jetpack-premium-analytics' ); if ( hasValidPreset ) { if ( removeCompareToPrefix ) { label = formatDateRange( comparisonRange ); } else { label = sprintf( // translators: %s is the comparison range label - __( 'Compare to: %s', 'woocommerce-analytics' ), + __( 'Compare to: %s', 'jetpack-premium-analytics' ), formatDateRange( comparisonRange ) ); } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx index ff4b7498adfd..d2a3202800ad 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx @@ -9,7 +9,7 @@ import { isPrimaryPreset, type ComparisonPresetId, type PrimaryPresetId, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx index 740951cfd3a4..18e5d3d3e955 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx @@ -4,8 +4,8 @@ import { __ } from '@wordpress/i18n'; import { Field, Input, Stack } from '@wordpress/ui'; import { useCallback, useEffect, useState } from 'react'; -import { createTZDateFromParts } from '@next-woo-analytics/datetime'; -import { formatDate } from '@wc-analytics/formatters'; +import { createTZDateFromParts } from '@jetpack-premium-analytics/datetime'; +import { formatDate } from '@jetpack-premium-analytics/formatters'; /** * Internal dependencies @@ -94,7 +94,7 @@ export function DateRangeInput( { return ( { @@ -105,7 +105,7 @@ export function DateRangeInput( { /> { diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx index d09f8b634c51..33caecc2f0ec 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx @@ -12,7 +12,7 @@ import { calendar } from '@wordpress/icons'; import { Badge, Button, Stack } from '@wordpress/ui'; import { useState, useCallback, useMemo, useEffect } from 'react'; import { useResizeObserver } from '@wordpress/compose'; -import { formatDateRange } from '@wc-analytics/formatters'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import '@automattic/ui/style.css'; @@ -22,7 +22,7 @@ import { PRESET_CUSTOM, type PrimaryPresetId, type DateRangePreset, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -130,7 +130,7 @@ function DateRangePopoverActions( { className="date-range-popover-actions" > ); @@ -168,7 +168,7 @@ function DateRangePresetsDropdown( { } ) ), { value: PRESET_CUSTOM, - label: __( 'Custom range', 'woocommerce-analytics' ), + label: __( 'Custom range', 'jetpack-premium-analytics' ), }, ], [ presets ] diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx index e60fd29057b5..7048c34be05f 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx @@ -10,7 +10,7 @@ import { getDefaultDateRangePresets, type PrimaryPresetId, type DateRangePreset, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -123,7 +123,7 @@ export function DateRangePresets( { disabled > - { __( 'Custom', 'woocommerce-analytics' ) } + { __( 'Custom', 'jetpack-premium-analytics' ) } @@ -138,7 +138,7 @@ export function DateRangePresets( { hideOnClick > - { __( 'No comparison', 'woocommerce-analytics' ) } + { __( 'No comparison', 'jetpack-premium-analytics' ) } ) } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts index 2d34bfc5454d..ed2d27e2e31b 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts @@ -20,10 +20,10 @@ export { PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR, PRESET_CUSTOM, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; export type { PrimaryPresetId, SelectablePresetId, DateRangePreset, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts index a7f9f48e731f..021b5702696e 100644 --- a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts +++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts @@ -6,7 +6,7 @@ import { getComparisonRangeFromPreset, getComparisonPresetConfigs, type ComparisonPresetId, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/ui/tsconfig.json b/projects/packages/premium-analytics/packages/ui/tsconfig.json deleted file mode 100644 index 18b7c843caa5..000000000000 --- a/projects/packages/premium-analytics/packages/ui/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "build", - "skipLibCheck": true - }, - "include": [ "src/**/*" ], - "exclude": [ "build", "node_modules" ] -} \ No newline at end of file From cd2ec28f25a8a906a905921e218bcb7a11fd8c0f Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:54:02 +0800 Subject: [PATCH 16/32] refactor(premium-analytics): decouple ui package from admin-toolkit unlock --- .../date-comparison-dropdown.tsx | 2 +- .../date-range-popover/date-range-filter.tsx | 2 +- .../date-range-presets/date-range-presets.tsx | 2 +- .../packages/ui/src/lock/unlock.ts | 22 +++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx index 3088dd941c6c..19cfe7f37f4e 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../lock/unlock'; import { Button } from '@wordpress/ui'; import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { sprintf, __ } from '@wordpress/i18n'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx index 33caecc2f0ec..51d015ff72b2 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx @@ -7,7 +7,7 @@ import { SelectControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../lock/unlock'; import { calendar } from '@wordpress/icons'; import { Badge, Button, Stack } from '@wordpress/ui'; import { useState, useCallback, useMemo, useEffect } from 'react'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx index 7048c34be05f..e401c5accf64 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../lock/unlock'; import { useMemo } from 'react'; import { PRESET_CUSTOM, diff --git a/projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts b/projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts new file mode 100644 index 000000000000..ca799e25b34c --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts @@ -0,0 +1,22 @@ +/** + * Local unlock helper for reaching the private `Menu` component that + * `@wordpress/components` exposes through `@wordpress/private-apis`. + * + * Upstream reached `Menu` via `@automattic/admin-toolkit`'s `unlock`, which is + * not available in this monorepo. This mirrors existing Jetpack precedent that + * opts in to the private APIs directly: + * + * - `projects/packages/jetpack-mu-wpcom/src/common/utils.ts` (`getUnlock()`) + * - `projects/js-packages/charts/src/stories/unlock.ts` + * + * The opt-in module name only needs to be an allow-listed core module; the + * returned `unlock` reads private data bound to any object, so it resolves the + * private APIs locked onto `@wordpress/components`' `privateApis`. + */ + +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/components' +); From 050efb667450906da4874a30c1c55f47e1aa52db Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:54:13 +0800 Subject: [PATCH 17/32] style(premium-analytics): align ported ui package with jetpack prettier --- .../date-comparison-dropdown.scss | 10 +-- .../date-comparison-dropdown.tsx | 19 ++---- .../date-filters-panel/date-filters-panel.tsx | 34 +++------- .../date-range-input/date-range-input.scss | 10 +-- .../src/date-range-input/date-range-input.tsx | 29 ++------ .../date-range-popover/date-range-filter.scss | 23 ++++--- .../date-range-popover/date-range-filter.tsx | 66 +++++-------------- .../date-range-presets.scss | 10 +-- .../date-range-presets/date-range-presets.tsx | 13 +--- .../use-comparison-date-presets.ts | 14 +--- 10 files changed, 70 insertions(+), 158 deletions(-) diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss index 93626fe01f0b..a6f6f1909e14 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss @@ -1,6 +1,6 @@ .date-comparison-dropdown { &__button { - background-color: var( --wpds-color-bg-surface-neutral-strong ); + background-color: var( --wpds-color-bg-surface-neutral-strong ); } } @@ -14,7 +14,7 @@ /* disable animation for the date range popover */ /* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ -@media not (prefers-reduced-motion: reduce) { +@media not ( prefers-reduced-motion: reduce ) { .date-comparison-dropdown__popover { view-transition-name: next-admin--date-comparison-dropdown; transition: none !important; @@ -22,13 +22,13 @@ } /* ensure it's above the canvas/stage during the transition */ -::view-transition-group(next-admin--date-comparison-dropdown) { +::view-transition-group( next-admin--date-comparison-dropdown ) { z-index: 3000; } /* no animation for the snapshot (avoid "flashing") */ -::view-transition-new(next-admin--date-comparison-dropdown), -::view-transition-old(next-admin--date-comparison-dropdown) { +::view-transition-new( next-admin--date-comparison-dropdown ), +::view-transition-old( next-admin--date-comparison-dropdown ) { animation: none; } /* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx index 19cfe7f37f4e..58379d52d21c 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx @@ -59,8 +59,7 @@ export function DateComparisonDropdown( { onClear, }: DateComparisonDropdownProps ) { const selectedPreset = useMemo( - () => - presetId ? presets.find( ( p ) => p.id === presetId ) : undefined, + () => ( presetId ? presets.find( p => p.id === presetId ) : undefined ), [ presets, presetId ] ); @@ -86,16 +85,9 @@ export function DateComparisonDropdown( { /> - + - { __( - 'No comparison', - 'jetpack-premium-analytics' - ) } + { __( 'No comparison', 'jetpack-premium-analytics' ) } @@ -107,10 +99,7 @@ export function DateComparisonDropdown( { hideOnClick > - { __( - 'Comparison to past', - 'jetpack-premium-analytics' - ) } + { __( 'Comparison to past', 'jetpack-premium-analytics' ) } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx index d2a3202800ad..fc06b24ef7c3 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx @@ -47,26 +47,17 @@ export type DateFiltersPanelProps = { * Callback when the comparison date range changes. * Receives the calculated comparison range and the preset ID used. */ - onComparisonChange: ( - range: DateRange | undefined, - presetId?: ComparisonPresetId - ) => void; + onComparisonChange: ( range: DateRange | undefined, presetId?: ComparisonPresetId ) => void; /** * Props for the date range popover. */ - rangeControlProps?: Omit< - Parameters< typeof BaseControl >[ 0 ], - 'children' - >; + rangeControlProps?: Omit< Parameters< typeof BaseControl >[ 0 ], 'children' >; /** * Props for the date comparison dropdown. */ - comparisonControlProps?: Omit< - Parameters< typeof BaseControl >[ 0 ], - 'children' - >; + comparisonControlProps?: Omit< Parameters< typeof BaseControl >[ 0 ], 'children' >; /** * Callback when the primary date range is applied. @@ -141,9 +132,7 @@ export function DateFiltersPanel( { // Validate and normalize the comparison preset ID const validatedComparisonPresetId = useMemo( () => { - return isComparisonPresetId( comparisonPresetId ) - ? comparisonPresetId - : undefined; + return isComparisonPresetId( comparisonPresetId ) ? comparisonPresetId : undefined; }, [ comparisonPresetId ] ); // Derive comparison enabled state directly from validated prop @@ -161,8 +150,8 @@ export function DateFiltersPanel( { */ const defaultPresetId = useMemo( () => { return ( - presets.find( ( p ) => p.id === 'previous-period' )?.id ?? - presets.find( ( p ) => p.id === 'previous-month' )?.id ?? + presets.find( p => p.id === 'previous-period' )?.id ?? + presets.find( p => p.id === 'previous-month' )?.id ?? presets[ 0 ]?.id ); }, [ presets ] ); @@ -175,12 +164,12 @@ export function DateFiltersPanel( { */ const preset = useMemo( () => { const id = validatedComparisonPresetId ?? defaultPresetId; - return id ? presets.find( ( p ) => p.id === id ) : undefined; + return id ? presets.find( p => p.id === id ) : undefined; }, [ presets, validatedComparisonPresetId, defaultPresetId ] ); const presetChange = useCallback( ( id: ComparisonPresetId ) => { - const nextPreset = presets.find( ( p ) => p.id === id ); + const nextPreset = presets.find( p => p.id === id ); onComparisonChange( nextPreset?.range, id ); }, [ onComparisonChange, presets ] @@ -200,12 +189,7 @@ export function DateFiltersPanel( { if ( preset?.range && presetIdToUse ) { onComparisonChange( preset.range, presetIdToUse ); } - }, [ - onComparisonChange, - preset, - validatedComparisonPresetId, - defaultPresetId, - ] ); + }, [ onComparisonChange, preset, validatedComparisonPresetId, defaultPresetId ] ); return ( diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss index 754e7105acaf..8d74ebc150f4 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss @@ -2,8 +2,8 @@ flex: 1; font-size: var( --wpds-font-size-sm ); - @supports selector(&::-webkit-calendar-picker-indicator) { - input[type="date"] { + @supports selector( &::-webkit-calendar-picker-indicator ) { + input[type='date'] { // Removes extra spaces for the calendar icon. width: fit-content; padding-right: 0; @@ -21,15 +21,15 @@ } } - @supports not selector(&::-webkit-calendar-picker-indicator) { - input[type="date"] { + @supports not selector( &::-webkit-calendar-picker-indicator ) { + input[type='date'] { padding: 0; // We'll control input's inner spacing manually min-width: fit-content; // Prevent extra space on smaller screens // Use flex to center the input's content horizontally display: flex; - width: calc(100% - 20px); // Take almost all the space + width: calc( 100% - 20px ); // Take almost all the space margin-inline: auto; // Keep things centered } } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx index 18e5d3d3e955..64e3419a0ace 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx @@ -26,18 +26,12 @@ type DateInputProps = Pick< DateRangeInputProps, 'timeZone' > & { onChange: ( date?: Date ) => void; }; -const formatToString = ( date?: Date ) => - date ? formatDate( date, 'iso' ) : ''; +const formatToString = ( date?: Date ) => ( date ? formatDate( date, 'iso' ) : '' ); function parseFromString( dateString: string, timeZone: string ) { - const [ year, month, day ] = dateString - .split( '-' ) - .map( ( x ) => Number( x ) ); + const [ year, month, day ] = dateString.split( '-' ).map( x => Number( x ) ); - const parsedDate = createTZDateFromParts( - [ year, month - 1, day ], - timeZone - ); + const parsedDate = createTZDateFromParts( [ year, month - 1, day ], timeZone ); return ! isNaN( parsedDate.getTime() ) ? parsedDate : undefined; } @@ -74,21 +68,12 @@ function DateInput( { label, date, onChange, timeZone }: DateInputProps ) { return ( { label } - + ); } -export function DateRangeInput( { - range, - onChange, - timeZone, -}: DateRangeInputProps ) { +export function DateRangeInput( { range, onChange, timeZone }: DateRangeInputProps ) { const { from, to } = range; return ( @@ -97,7 +82,7 @@ export function DateRangeInput( { label={ __( 'From', 'jetpack-premium-analytics' ) } date={ from } timeZone={ timeZone } - onChange={ ( nextFrom ) => { + onChange={ nextFrom => { if ( nextFrom && to && nextFrom <= to ) { onChange( { from: nextFrom, to } ); } @@ -108,7 +93,7 @@ export function DateRangeInput( { label={ __( 'To', 'jetpack-premium-analytics' ) } date={ to } timeZone={ timeZone } - onChange={ ( nextTo ) => { + onChange={ nextTo => { if ( nextTo && from && from <= nextTo ) { onChange( { from, to: nextTo } ); } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss index b0bc48158ac7..9eca700420e4 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss @@ -1,5 +1,5 @@ -@use "@wordpress/base-styles/variables" as vars; -@use "@wordpress/base-styles/colors" as colors; +@use '@wordpress/base-styles/variables' as vars; +@use '@wordpress/base-styles/colors' as colors; .date-range-popover-content { --wca-popover-padding: var( --wpds-dimension-padding-lg ); @@ -44,7 +44,7 @@ .date-range-presets-wrapper { grid-row: 1; display: grid; - grid-template-columns: minmax(0, max-content) 1fr; + grid-template-columns: minmax( 0, max-content ) 1fr; background-color: #fff; padding-bottom: var( --wpds-dimension-gap-sm ); width: calc( 60 * var( --wpds-dimension-base ) ); @@ -60,8 +60,13 @@ --wca-calendar-button-height: 32px; --wca-calendar-button-gap: 1rem; // consistent with automattic/ui style --wca-calendar-padding: var( --wca-popover-padding ); - --wca-calendar-width: calc( var( --wca-calendar-button-width ) * 7 + var( --wca-calendar-padding ) * 2 ); - --wca-calendar-width-wide: calc( var( --wca-calendar-button-width ) * 14 + var( --wca-calendar-padding ) * 2 + var( --wca-calendar-button-gap ) ); + --wca-calendar-width: calc( + var( --wca-calendar-button-width ) * 7 + var( --wca-calendar-padding ) * 2 + ); + --wca-calendar-width-wide: calc( + var( --wca-calendar-button-width ) * 14 + var( --wca-calendar-padding ) * 2 + + var( --wca-calendar-button-gap ) + ); grid-row: 1; padding: var( --wca-calendar-padding ); @@ -86,20 +91,20 @@ /* disable animation for the date range popover */ /* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ -@media not (prefers-reduced-motion: reduce) { +@media not ( prefers-reduced-motion: reduce ) { .date-filters-panel__popover { view-transition-name: next-admin--date-range-popover; } } /* ensure it's above the canvas/stage during the transition */ -::view-transition-group(next-admin--date-range-popover) { +::view-transition-group( next-admin--date-range-popover ) { z-index: 3000; } /* no animation for the snapshot (avoid "flashing") */ -::view-transition-new(next-admin--date-range-popover), -::view-transition-old(next-admin--date-range-popover) { +::view-transition-new( next-admin--date-range-popover ), +::view-transition-old( next-admin--date-range-popover ) { animation: none; } /* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx index 51d015ff72b2..6d35760113bf 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx @@ -44,9 +44,7 @@ const MOBILE_CONTAINER_WIDTH_THRESHOLD = 480; * Date range type from @automattic/ui. * Represents a range with `from` and `to` Date objects. */ -export type DateRange = NonNullable< - Parameters< typeof DateRangeCalendar >[ 0 ][ 'selected' ] ->; +export type DateRange = NonNullable< Parameters< typeof DateRangeCalendar >[ 0 ][ 'selected' ] >; /** * Props for DateRangePopoverContent component. @@ -132,12 +130,7 @@ function DateRangePopoverActions( { - @@ -176,7 +169,7 @@ function DateRangePresetsDropdown( { const handleChange = useCallback( ( selectedValue: string ) => { - const preset = presets.find( ( p ) => p.id === selectedValue ); + const preset = presets.find( p => p.id === selectedValue ); if ( preset ) { onRangeChange( preset.range, preset.id ); } @@ -209,14 +202,9 @@ export function DateRangePopoverContent( { isMobile = false, timeZone, }: DateRangePopoverContentProps ) { - const [ displayedMonth, setDisplayedMonth ] = useState( - getDisplayedMonth( range ) - ); + const [ displayedMonth, setDisplayedMonth ] = useState( getDisplayedMonth( range ) ); - const handleChange = ( - nextRange?: DateRange, - nextPrimaryPresetId?: PrimaryPresetId - ) => { + const handleChange = ( nextRange?: DateRange, nextPrimaryPresetId?: PrimaryPresetId ) => { if ( nextRange ) { setDisplayedMonth( getDisplayedMonth( nextRange ) ); } @@ -238,27 +226,19 @@ export function DateRangePopoverContent( { timeZone={ timeZone } /> - + handleChange( nextRange ) } + onSelect={ nextRange => handleChange( nextRange ) } numberOfMonths={ 1 } month={ displayedMonth } onMonthChange={ setDisplayedMonth } timeZone={ timeZone } /> - + ); } @@ -283,16 +263,12 @@ export function DateRangePopoverContent( { gap="lg" direction="column" > - + handleChange( nextRange ) } + onSelect={ nextRange => handleChange( nextRange ) } numberOfMonths={ isWideScreen ? 2 : 1 } month={ displayedMonth } onMonthChange={ setDisplayedMonth } @@ -300,19 +276,12 @@ export function DateRangePopoverContent( { /> - + ); } -type DateRangePopoverProps = Omit< - DateRangePopoverContentProps, - 'isWideScreen' | 'isMobile' -> & { +type DateRangePopoverProps = Omit< DateRangePopoverContentProps, 'isWideScreen' | 'isMobile' > & { /** * Optional external container element for responsive calculations. * When provided, the component will measure this container's width @@ -337,9 +306,7 @@ export function DateRangePopover( { timeZone, containerElement, }: DateRangePopoverProps ) { - const [ containerWidth, setContainerWidth ] = useState< number | null >( - null - ); + const [ containerWidth, setContainerWidth ] = useState< number | null >( null ); // Callback to update container width const handleResize = useCallback( ( entries: ResizeObserverEntry[] ) => { @@ -359,12 +326,9 @@ export function DateRangePopover( { }, [ containerElement, setObserverRef ] ); // Determine layout based on container width - const isMobile = - containerWidth !== null && - containerWidth < MOBILE_CONTAINER_WIDTH_THRESHOLD; + const isMobile = containerWidth !== null && containerWidth < MOBILE_CONTAINER_WIDTH_THRESHOLD; - const isWideScreen = - containerWidth !== null && containerWidth >= WIDE_CONTAINER_THRESHOLD; + const isWideScreen = containerWidth !== null && containerWidth >= WIDE_CONTAINER_THRESHOLD; const presetLabel = getPresetLabel( presetId ); diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss index 1c1f1306fc48..729a1d328ccd 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss @@ -1,7 +1,7 @@ -@use "@wordpress/base-styles/variables" as vars; -@use "@wordpress/base-styles/colors" as colors; +@use '@wordpress/base-styles/variables' as vars; +@use '@wordpress/base-styles/colors' as colors; -.date-range-presets{ +.date-range-presets { max-width: 240px; } @@ -16,11 +16,11 @@ // Custom button acts as a label, not an interactive element. // Override disabled styles to show selection state visually. .date-range-presets__custom { - &[aria-disabled="true"] { + &[aria-disabled='true'] { color: var( --wpds-color-fg-content-neutral-weak ); } - &[aria-checked="true"] { + &[aria-checked='true'] { color: var( --wpds-color-fg-content-neutral ); } } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx index e401c5accf64..4037bbc96cc4 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx @@ -88,10 +88,7 @@ export function DateRangePresets( { return getDefaultDateRangePresets( timeZone ); }, [ presetsProp, timeZone ] ); - const presets = useMemo( - () => presetsProp || defaultPresets, - [ presetsProp, defaultPresets ] - ); + const presets = useMemo( () => presetsProp || defaultPresets, [ presetsProp, defaultPresets ] ); return ( <> @@ -122,9 +119,7 @@ export function DateRangePresets( { checked={ value === PRESET_CUSTOM } disabled > - - { __( 'Custom', 'jetpack-premium-analytics' ) } - + { __( 'Custom', 'jetpack-premium-analytics' ) } { onClear && ( @@ -137,9 +132,7 @@ export function DateRangePresets( { onChange={ onClear } hideOnClick > - - { __( 'No comparison', 'jetpack-premium-analytics' ) } - + { __( 'No comparison', 'jetpack-premium-analytics' ) } ) } diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts index 021b5702696e..74acf7ac072d 100644 --- a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts +++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts @@ -30,9 +30,7 @@ export type ComparisonDateRangePreset = { * @param referenceRange - The primary date range to compare against * @return Array of comparison presets with strongly-typed IDs */ -export function useComparisonDatePresets( - referenceRange: DateRange -): ComparisonDateRangePreset[] { +export function useComparisonDatePresets( referenceRange: DateRange ): ComparisonDateRangePreset[] { return useMemo( () => { if ( ! referenceRange.from || ! referenceRange.to ) { return []; @@ -40,15 +38,9 @@ export function useComparisonDatePresets( return getComparisonPresetConfigs() .map( ( { id, label } ) => { - const range = getComparisonRangeFromPreset( - referenceRange, - id - ); + const range = getComparisonRangeFromPreset( referenceRange, id ); return range ? { id, label, range } : null; } ) - .filter( - ( preset ): preset is ComparisonDateRangePreset => - preset !== null - ); + .filter( ( preset ): preset is ComparisonDateRangePreset => preset !== null ); }, [ referenceRange ] ); } From 5f71f64fd4e915408995bcd7654e134ace911aa8 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:54:31 +0800 Subject: [PATCH 18/32] fix(premium-analytics): satisfy ui package typecheck and import lint --- .../date-comparison-dropdown.tsx | 27 ++++++++++++------ .../date-filters-panel/date-filters-panel.tsx | 9 +++--- .../src/date-range-input/date-range-input.tsx | 5 ++-- .../date-range-popover/date-range-filter.tsx | 28 +++++++++---------- .../date-range-presets/date-range-presets.tsx | 10 +++---- .../use-comparison-date-presets.ts | 3 +- 6 files changed, 42 insertions(+), 40 deletions(-) diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx index 58379d52d21c..34f39700b781 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx @@ -1,23 +1,26 @@ /** * External dependencies */ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../lock/unlock'; -import { Button } from '@wordpress/ui'; import { formatDateRange } from '@jetpack-premium-analytics/formatters'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { sprintf, __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/ui'; import { useMemo } from 'react'; -import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; - -const { Menu } = unlock( componentsPrivateApis ); - /** * Internal dependencies */ import { DateRangePresets } from '../date-range-presets'; +import { unlock } from '../lock/unlock'; import type { ComparisonDateRangePreset } from '../use-comparison-date-presets'; +import type { + ComparisonPresetId, + DateRangePreset, + PrimaryPresetId, +} from '@jetpack-premium-analytics/datetime'; import './date-comparison-dropdown.scss'; +const { Menu } = unlock( componentsPrivateApis ); + type DateComparisonDropdownProps = { /** * Available comparison presets (e.g., previous-period, previous-month) @@ -138,8 +141,14 @@ export function DateComparisonDropdown( { { hasPresets && ( { /* diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx index fc06b24ef7c3..dd9f6a3fc189 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx @@ -1,21 +1,20 @@ /** * External dependencies */ -import { Stack } from '@wordpress/ui'; -import { BaseControl } from '@wordpress/components'; -import { useMemo, useCallback } from 'react'; import { isComparisonPresetId, isPrimaryPreset, type ComparisonPresetId, type PrimaryPresetId, } from '@jetpack-premium-analytics/datetime'; - +import { BaseControl } from '@wordpress/components'; +import { Stack } from '@wordpress/ui'; +import { useMemo, useCallback } from 'react'; /** * Internal dependencies */ -import { DateRangePopover } from '../date-range-popover'; import { DateComparisonDropdown } from '../date-comparison-dropdown'; +import { DateRangePopover } from '../date-range-popover'; import { useComparisonDatePresets } from '../use-comparison-date-presets'; type DateRangePopoverProps = Parameters< typeof DateRangePopover >[ 0 ]; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx index 64e3419a0ace..562fe3f07f98 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx @@ -1,12 +1,11 @@ /** * External dependencies */ +import { createTZDateFromParts } from '@jetpack-premium-analytics/datetime'; +import { formatDate } from '@jetpack-premium-analytics/formatters'; import { __ } from '@wordpress/i18n'; import { Field, Input, Stack } from '@wordpress/ui'; import { useCallback, useEffect, useState } from 'react'; -import { createTZDateFromParts } from '@jetpack-premium-analytics/datetime'; -import { formatDate } from '@jetpack-premium-analytics/formatters'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx index 6d35760113bf..926074092897 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx @@ -2,36 +2,34 @@ * External dependencies */ import { DateRangeCalendar } from '@automattic/ui'; +import { + getPresetLabel, + getDefaultDateRangePresets, + PRESET_CUSTOM, + type PrimaryPresetId, + type DateRangePreset, +} from '@jetpack-premium-analytics/datetime'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { Dropdown, SelectControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { unlock } from '../lock/unlock'; -import { calendar } from '@wordpress/icons'; -import { Badge, Button, Stack } from '@wordpress/ui'; -import { useState, useCallback, useMemo, useEffect } from 'react'; import { useResizeObserver } from '@wordpress/compose'; -import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { __ } from '@wordpress/i18n'; +import { calendar } from '@wordpress/icons'; +import { Badge, Button, Stack } from '@wordpress/ui'; import clsx from 'clsx'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import '@automattic/ui/style.css'; -import { - getPresetLabel, - getDefaultDateRangePresets, - PRESET_CUSTOM, - type PrimaryPresetId, - type DateRangePreset, -} from '@jetpack-premium-analytics/datetime'; - /** * Internal dependencies */ -import { DateRangePresets } from '../date-range-presets'; import { DateRangeInput } from '../date-range-input'; +import { DateRangePresets } from '../date-range-presets'; +import { unlock } from '../lock/unlock'; import './date-range-filter.scss'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { Menu } = unlock( componentsPrivateApis ); /** diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx index 4037bbc96cc4..f27ac51d264c 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx @@ -1,24 +1,22 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../lock/unlock'; -import { useMemo } from 'react'; import { PRESET_CUSTOM, getDefaultDateRangePresets, type PrimaryPresetId, type DateRangePreset, } from '@jetpack-premium-analytics/datetime'; - +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useMemo } from 'react'; /** * Internal dependencies */ import { DateRangePopover } from '../date-range-popover/date-range-filter'; +import { unlock } from '../lock/unlock'; import './date-range-presets.scss'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { Menu } = unlock( componentsPrivateApis ); type DateRange = Parameters< typeof DateRangePopover >[ 0 ][ 'range' ]; diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts index 74acf7ac072d..1a018083db1c 100644 --- a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts +++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts @@ -1,13 +1,12 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { getComparisonRangeFromPreset, getComparisonPresetConfigs, type ComparisonPresetId, } from '@jetpack-premium-analytics/datetime'; - +import { useMemo } from 'react'; /** * Internal dependencies */ From 8747e241b970ec5b667421a717fcebd971fea5a9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:54:45 +0800 Subject: [PATCH 19/32] chore(premium-analytics): wire ui package deps and relax lint for the port --- pnpm-lock.yaml | 29 +++++++++++++++---- .../premium-analytics/eslint.config.mjs | 16 ++++++++++ .../packages/premium-analytics/package.json | 8 ++++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54d70fd092e0..8839b4df2df7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3869,12 +3869,21 @@ importers: '@automattic/number-formatters': specifier: workspace:* version: link:../../js-packages/number-formatters + '@automattic/ui': + specifier: 1.0.2 + version: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@date-fns/tz': specifier: 1.4.1 version: 1.4.1 '@wordpress/boot': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/components': + specifier: 33.1.0 + version: 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/compose': + specifier: 7.46.0 + version: 7.46.0(react@18.3.1) '@wordpress/data': specifier: 10.46.0 version: 10.46.0(react@18.3.1) @@ -3887,9 +3896,18 @@ importers: '@wordpress/primitives': specifier: 4.46.0 version: 4.46.0(react@18.3.1) + '@wordpress/private-apis': + specifier: 1.46.0 + version: 1.46.0 '@wordpress/route': specifier: 0.12.0 version: 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/ui': + specifier: 0.13.0 + version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: + specifier: 2.1.1 + version: 2.1.1 date-fns: specifier: 4.1.0 version: 4.1.0 @@ -3915,12 +3933,12 @@ importers: '@typescript/native-preview': specifier: 7.0.0-dev.20260225.1 version: 7.0.0-dev.20260225.1 + '@wordpress/base-styles': + specifier: 8.0.0 + version: 8.0.0 '@wordpress/build': specifier: 0.14.0 - version: 0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) - '@wordpress/ui': - specifier: 0.13.0 - version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: 4.28.2 version: 4.28.2 @@ -24109,7 +24127,7 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': + '@wordpress/build@0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 '@wordpress/style-runtime': 0.2.0 @@ -24129,6 +24147,7 @@ snapshots: sass-embedded: 1.97.3 optionalDependencies: '@wordpress/boot': 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/private-apis': 1.46.0 '@wordpress/route': 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - '@babel/core' diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index 38a1a71417d7..5dd7217b8f47 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -27,5 +27,21 @@ export default defineConfig( 'jsdoc/require-returns': 'off', 'jsdoc/check-indentation': 'off', }, + }, + { + // First UI package in the port: also soften JSDoc rules for the ui + // package and allow the upstream inline-handler JSX style. Temporary — + // tighten these up in a follow-up alongside datetime/formatters. + files: [ 'packages/ui/**' ], + rules: { + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-description': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/check-indentation': 'off', + 'jsdoc/escape-inline-tags': 'off', + 'react/jsx-no-bind': 'off', + }, } ); diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index fd3c20aab0a9..8d0b97a8552f 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -31,13 +31,19 @@ }, "dependencies": { "@automattic/number-formatters": "workspace:*", + "@automattic/ui": "1.0.2", "@date-fns/tz": "1.4.1", "@wordpress/boot": "0.13.0", + "@wordpress/components": "33.1.0", + "@wordpress/compose": "7.46.0", "@wordpress/data": "10.46.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^13.0.0", "@wordpress/primitives": "4.46.0", + "@wordpress/private-apis": "1.46.0", "@wordpress/route": "0.12.0", + "@wordpress/ui": "0.13.0", + "clsx": "2.1.1", "date-fns": "4.1.0", "react": "18.3.1", "react-dom": "18.3.1" @@ -48,8 +54,8 @@ "@testing-library/dom": "10.4.1", "@types/jest": "30.0.0", "@typescript/native-preview": "7.0.0-dev.20260225.1", + "@wordpress/base-styles": "8.0.0", "@wordpress/build": "0.14.0", - "@wordpress/ui": "0.13.0", "browserslist": "4.28.2", "storybook": "10.3.6", "typescript": "5.9.3" From 4c0d8f877c0664a3c728e52d543cd19537ba71a6 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:54:53 +0800 Subject: [PATCH 20/32] changelog: add entry for premium-analytics ui package port --- .../wooa7s-1318-integrate-components-package-into-analytics | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics new file mode 100644 index 000000000000..777a39c9cea4 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Port the components package (date range/comparison filter UI components and SCSS) from next-woocommerce-analytics as the internal `ui` package. From 0472569b8ff07a068297c4a2778503c335ba2b96 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 15:31:16 +0800 Subject: [PATCH 21/32] fix(premium-analytics): resolve stylelint errors in ui package scss --- .../date-comparison-dropdown.scss | 12 ++-- .../date-range-input/date-range-input.scss | 12 ++-- .../date-range-popover/date-range-filter.scss | 56 +++++++++---------- .../date-range-presets.scss | 16 +++--- 4 files changed, 49 insertions(+), 47 deletions(-) diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss index a6f6f1909e14..29fea4bf18a3 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss @@ -1,11 +1,12 @@ .date-comparison-dropdown { + &__button { - background-color: var( --wpds-color-bg-surface-neutral-strong ); + background-color: var(--wpds-color-bg-surface-neutral-strong); } } .date-filters-panel-button { - background-color: var( --wpds-color-bg-surface-neutral-strong ); + background-color: var(--wpds-color-bg-surface-neutral-strong); } .date-comparison-dropdown__popover { @@ -15,6 +16,7 @@ /* disable animation for the date range popover */ /* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ @media not ( prefers-reduced-motion: reduce ) { + .date-comparison-dropdown__popover { view-transition-name: next-admin--date-comparison-dropdown; transition: none !important; @@ -22,13 +24,13 @@ } /* ensure it's above the canvas/stage during the transition */ -::view-transition-group( next-admin--date-comparison-dropdown ) { +::view-transition-group(next-admin--date-comparison-dropdown) { z-index: 3000; } /* no animation for the snapshot (avoid "flashing") */ -::view-transition-new( next-admin--date-comparison-dropdown ), -::view-transition-old( next-admin--date-comparison-dropdown ) { +::view-transition-new(next-admin--date-comparison-dropdown), +::view-transition-old(next-admin--date-comparison-dropdown) { animation: none; } /* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss index 8d74ebc150f4..f0ebd4c84d75 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss @@ -1,9 +1,10 @@ .input-date-control { flex: 1; - font-size: var( --wpds-font-size-sm ); + font-size: var(--wpds-typography-font-size-sm); @supports selector( &::-webkit-calendar-picker-indicator ) { - input[type='date'] { + + input[type="date"] { // Removes extra spaces for the calendar icon. width: fit-content; padding-right: 0; @@ -12,7 +13,7 @@ -webkit-appearance: none; background: none; - font-size: var( --wpds-font-size-md ); + font-size: var(--wpds-typography-font-size-md); // Removes the calendar icon. &::-webkit-calendar-picker-indicator { @@ -22,14 +23,15 @@ } @supports not selector( &::-webkit-calendar-picker-indicator ) { - input[type='date'] { + + input[type="date"] { padding: 0; // We'll control input's inner spacing manually min-width: fit-content; // Prevent extra space on smaller screens // Use flex to center the input's content horizontally display: flex; - width: calc( 100% - 20px ); // Take almost all the space + width: calc(100% - 20px); // Take almost all the space margin-inline: auto; // Keep things centered } } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss index 9eca700420e4..aa17958cb048 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss @@ -1,8 +1,8 @@ -@use '@wordpress/base-styles/variables' as vars; -@use '@wordpress/base-styles/colors' as colors; +@use "@wordpress/base-styles/variables" as vars; +@use "@wordpress/base-styles/colors" as colors; .date-range-popover-content { - --wca-popover-padding: var( --wpds-dimension-padding-lg ); + --wca-popover-padding: var(--wpds-dimension-padding-lg); --wca-popover-border-color: #{colors.$gray-300}; --wca-popover-border-width: #{vars.$border-width}; @@ -11,9 +11,9 @@ grid-template-rows: 1fr auto; // Grid lines: background = line color, gap = line width - background-color: var( --wca-popover-border-color ); - column-gap: var( --wca-popover-border-width ); - row-gap: var( --wca-popover-border-width ); + background-color: var(--wca-popover-border-color); + column-gap: var(--wca-popover-border-width); + row-gap: var(--wca-popover-border-width); .date-range-calendar { display: flex; @@ -25,8 +25,8 @@ &--mobile { display: flex; flex-direction: column; - gap: var( --wpds-dimension-gap-lg ); - padding: var( --wca-popover-padding ); + gap: var(--wpds-dimension-gap-lg); + padding: var(--wca-popover-padding); background-color: #fff; .date-range-calendar { @@ -36,7 +36,7 @@ .date-range-popover-actions { padding: 0; - padding-block-start: var( --wpds-dimension-gap-sm ); + padding-block-start: var(--wpds-dimension-gap-sm); } } } @@ -44,11 +44,11 @@ .date-range-presets-wrapper { grid-row: 1; display: grid; - grid-template-columns: minmax( 0, max-content ) 1fr; + grid-template-columns: minmax(0, max-content) 1fr; background-color: #fff; - padding-bottom: var( --wpds-dimension-gap-sm ); - width: calc( 60 * var( --wpds-dimension-base ) ); - padding-right: var( --wpds-dimension-padding-sm ); + padding-bottom: var(--wpds-dimension-gap-sm); + width: calc(60 * var(--wpds-dimension-base)); + padding-right: var(--wpds-dimension-padding-sm); } .date-filters-panel-button { @@ -59,52 +59,48 @@ --wca-calendar-button-width: 32px; --wca-calendar-button-height: 32px; --wca-calendar-button-gap: 1rem; // consistent with automattic/ui style - --wca-calendar-padding: var( --wca-popover-padding ); - --wca-calendar-width: calc( - var( --wca-calendar-button-width ) * 7 + var( --wca-calendar-padding ) * 2 - ); - --wca-calendar-width-wide: calc( - var( --wca-calendar-button-width ) * 14 + var( --wca-calendar-padding ) * 2 + - var( --wca-calendar-button-gap ) - ); + --wca-calendar-padding: var(--wca-popover-padding); + --wca-calendar-width: calc(var(--wca-calendar-button-width) * 7 + var(--wca-calendar-padding) * 2); + --wca-calendar-width-wide: calc(var(--wca-calendar-button-width) * 14 + var(--wca-calendar-padding) * 2 + var(--wca-calendar-button-gap)); grid-row: 1; - padding: var( --wca-calendar-padding ); - width: var( --wca-calendar-width ); + padding: var(--wca-calendar-padding); + width: var(--wca-calendar-width); background-color: #fff; .date-range-calendar { - --a8c-calendar-button-width: var( --wca-calendar-button-width ); - --a8c-calendar-button-height: var( --wca-calendar-button-height ); + --a8c-calendar-button-width: var(--wca-calendar-button-width); + --a8c-calendar-button-height: var(--wca-calendar-button-height); } &__wide { - width: var( --wca-calendar-width-wide ); + width: var(--wca-calendar-width-wide); } } .date-range-popover-actions { grid-column: 1 / -1; - padding: calc( var( --wca-popover-padding ) / 2 ) var( --wca-popover-padding ); + padding: calc(var(--wca-popover-padding) / 2) var(--wca-popover-padding); background-color: #fff; } /* disable animation for the date range popover */ /* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ @media not ( prefers-reduced-motion: reduce ) { + .date-filters-panel__popover { view-transition-name: next-admin--date-range-popover; } } /* ensure it's above the canvas/stage during the transition */ -::view-transition-group( next-admin--date-range-popover ) { +::view-transition-group(next-admin--date-range-popover) { z-index: 3000; } /* no animation for the snapshot (avoid "flashing") */ -::view-transition-new( next-admin--date-range-popover ), -::view-transition-old( next-admin--date-range-popover ) { +::view-transition-new(next-admin--date-range-popover), +::view-transition-old(next-admin--date-range-popover) { animation: none; } /* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */ diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss index 729a1d328ccd..446ea4a41439 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss @@ -1,5 +1,5 @@ -@use '@wordpress/base-styles/variables' as vars; -@use '@wordpress/base-styles/colors' as colors; +@use "@wordpress/base-styles/variables" as vars; +@use "@wordpress/base-styles/colors" as colors; .date-range-presets { max-width: 240px; @@ -7,8 +7,9 @@ .date-range-presets, .date-range-presets__custom-group { + .date-range-presets__item { - min-height: var( --wpds-font-line-height-2xl ); + min-height: var(--wpds-typography-line-height-2xl); } } @@ -16,12 +17,13 @@ // Custom button acts as a label, not an interactive element. // Override disabled styles to show selection state visually. .date-range-presets__custom { - &[aria-disabled='true'] { - color: var( --wpds-color-fg-content-neutral-weak ); + + &[aria-disabled="true"] { + color: var(--wpds-color-fg-content-neutral-weak); } - &[aria-checked='true'] { - color: var( --wpds-color-fg-content-neutral ); + &[aria-checked="true"] { + color: var(--wpds-color-fg-content-neutral); } } } From 7953f05cba1edbbfb45d9fa0ab574cdf9c2069d1 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:19:00 +0800 Subject: [PATCH 22/32] feat(premium-analytics): add widgets-toolkit package from next-woocommerce-analytics --- .../packages/widgets-toolkit/README.md | 390 ++++++++++++++++++ .../packages/widgets-toolkit/package.json | 33 ++ .../src/components/chart-bar/README.md | 217 ++++++++++ .../chart-bar/bar-chart.module.scss | 41 ++ .../src/components/chart-bar/bar-chart.tsx | 271 ++++++++++++ .../src/components/chart-bar/index.ts | 6 + .../chart-comparative-line/README.md | 173 ++++++++ .../comparative-line-chart.module.scss | 26 ++ .../comparative-line-chart.tsx | 364 ++++++++++++++++ .../chart-comparative-line/index.ts | 3 + .../chart-comparative-line/types.ts | 30 ++ .../utils/align-series-dates.test.ts | 338 +++++++++++++++ .../utils/align-series-dates.ts | 79 ++++ .../chart-comparative-line/utils/index.ts | 1 + .../src/components/chart-donut/README.md | 160 +++++++ .../chart-donut/donut-chart.module.scss | 36 ++ .../components/chart-donut/donut-chart.tsx | 265 ++++++++++++ .../src/components/chart-donut/index.ts | 5 + .../chart-empty-state.module.scss | 12 + .../chart-empty-state/chart-empty-state.tsx | 61 +++ .../src/components/chart-empty-state/index.ts | 4 + .../components/chart-leaderboard/README.md | 314 ++++++++++++++ .../src/components/chart-leaderboard/index.ts | 9 + .../leaderboard-chart.module.scss | 42 ++ .../chart-leaderboard/leaderboard-chart.tsx | 221 ++++++++++ .../leaderboard-label.module.scss | 15 + .../chart-leaderboard/leaderboard-label.tsx | 79 ++++ .../components/chart-semi-circle/README.md | 127 ++++++ .../src/components/chart-semi-circle/index.ts | 5 + .../semi-circle-chart.module.scss | 16 + .../chart-semi-circle/semi-circle-chart.tsx | 239 +++++++++++ .../src/components/chart-tooltip/README.md | 207 ++++++++++ .../chart-tooltip/chart-tooltip.module.scss | 26 ++ .../chart-tooltip/chart-tooltip.tsx | 164 ++++++++ .../src/components/chart-tooltip/index.ts | 11 + .../chart-tooltip/pie-chart-tooltip.tsx | 54 +++ .../components/chart-tooltip/tooltip-row.tsx | 50 +++ .../src/components/chart-tooltip/utils.ts | 27 ++ .../widgets-toolkit/src/components/index.ts | 31 ++ .../src/components/legend/README.md | 74 ++++ .../src/components/legend/index.ts | 2 + .../components/legend/legend-with-theme.tsx | 72 ++++ .../src/components/legend/legend.module.scss | 28 ++ .../src/components/legend/legend.tsx | 93 +++++ .../src/components/legend/row/index.ts | 2 + .../src/components/legend/row/legend-row.tsx | 69 ++++ .../src/components/metric-delta/index.ts | 2 + .../metric-delta/metric-delta.module.scss | 18 + .../components/metric-delta/metric-delta.tsx | 156 +++++++ .../src/components/metric-value/index.ts | 2 + .../metric-value/metric-value.module.scss | 18 + .../components/metric-value/metric-value.tsx | 88 ++++ .../metric-with-comparison/index.ts | 1 + .../metric-with-comparison.tsx | 126 ++++++ .../src/components/report-metric/index.ts | 2 + .../report-metric/report-metric.tsx | 146 +++++++ .../src/components/widget-root/README.md | 152 +++++++ .../src/components/widget-root/context.tsx | 65 +++ .../src/components/widget-root/index.ts | 3 + .../widget-root/widget-root.module.scss | 5 + .../components/widget-root/widget-root.tsx | 139 +++++++ .../widgets-toolkit/src/constants/chart.ts | 4 + .../src/constants/color-palette.ts | 21 + .../widgets-toolkit/src/constants/index.ts | 2 + .../fields/date-report-params-field/README.md | 34 ++ .../date-report-params-field.tsx | 170 ++++++++ .../fields/date-report-params-field/index.ts | 4 + .../widgets-toolkit/src/fields/index.ts | 8 + .../src/fields/metrics-field/index.ts | 2 + .../fields/metrics-field/metrics-field.tsx | 71 ++++ .../src/fields/metrics-field/metrics.ts | 121 ++++++ .../__tests__/build-coupon-use-data.test.ts | 165 ++++++++ .../build-sales-by-coupon-data.test.ts | 212 ++++++++++ .../build-bookings-by-attendance-data.ts | 142 +++++++ .../src/helpers/build-coupon-use-data.ts | 117 ++++++ .../build-new-vs-returning-customer-data.ts | 116 ++++++ .../helpers/build-orders-fulfillment-data.ts | 102 +++++ .../src/helpers/build-payment-status-data.ts | 111 +++++ .../build-revenue-by-customer-type-data.ts | 80 ++++ .../src/helpers/build-sales-by-coupon-data.ts | 109 +++++ .../src/helpers/build-sales-by-device-data.ts | 67 +++ .../src/helpers/build-sales-by-utm-data.ts | 58 +++ .../helpers/build-sessions-by-device-data.ts | 120 ++++++ .../helpers/build-time-series-chart-data.ts | 106 +++++ .../src/helpers/build-total-returns-data.ts | 98 +++++ .../build-visitors-by-location-data.ts | 103 +++++ .../src/helpers/calculate-delta.ts | 33 ++ .../src/helpers/chart-empty-state.ts | 68 +++ .../widgets-toolkit/src/helpers/flag-url.ts | 13 + .../src/helpers/format-legend-labels.ts | 53 +++ .../src/helpers/format-orders-metrics.ts | 114 +++++ .../src/helpers/fulfillment-filters.ts | 22 + .../widgets-toolkit/src/helpers/index.ts | 78 ++++ .../src/helpers/payment-status-filters.ts | 31 ++ .../src/helpers/product-type-filters.ts | 38 ++ .../src/helpers/segment-styles.ts | 59 +++ .../widgets-toolkit/src/hooks/index.ts | 4 + .../use-attributes-with-search-fallback.ts | 67 +++ .../src/hooks/use-chart-theme.ts | 115 ++++++ .../src/hooks/use-series-styles.ts | 48 +++ .../src/hooks/use-widget-error.ts | 100 +++++ .../packages/widgets-toolkit/src/index.ts | 106 +++++ .../src/styles/_widget-container.scss | 70 ++++ .../packages/widgets-toolkit/src/types.ts | 65 +++ .../widgets-toolkit/src/widgets/README.md | 43 ++ .../bookings-by-attendance-widget.tsx | 97 +++++ .../widgets/bookings-by-attendance/index.ts | 1 + .../widgets/common/donut-widget.module.scss | 10 + .../src/widgets/common/index.ts | 2 + .../src/widgets/common/use-bar-styles.ts | 41 ++ .../src/widgets/common/use-segment-styles.ts | 49 +++ .../conversion-rate-widget.module.scss | 11 + .../conversion-rate-widget.tsx | 155 +++++++ .../src/widgets/conversion-rate/index.ts | 4 + .../widgets/coupon-use/coupon-use-widget.tsx | 95 +++++ .../src/widgets/coupon-use/index.ts | 1 + .../widgets-toolkit/src/widgets/index.ts | 30 ++ .../src/widgets/metric-comparison/index.ts | 1 + .../metric-comparison-widget.module.scss | 4 + .../metric-comparison-widget.tsx | 69 ++++ .../new-vs-returning-customer/index.ts | 1 + .../new-vs-returning-customer-widget.tsx | 99 +++++ .../booking-order-metric-widget.tsx | 56 +++ .../src/widgets/order-metric/index.ts | 2 + .../order-metric/widget-order-metric.tsx | 48 +++ .../src/widgets/orders-fulfillment/index.ts | 1 + .../orders-fulfillment-widget.tsx | 131 ++++++ .../src/widgets/payment-status/index.ts | 1 + .../payment-status/payment-status-widget.tsx | 98 +++++ .../src/widgets/product-leaderboard/index.ts | 12 + .../top-performing-bookings-widget.tsx | 53 +++ ...-performing-product-leaderboard-widget.tsx | 221 ++++++++++ .../top-performing-products-widget.tsx | 47 +++ .../widgets/revenue-by-customer-type/index.ts | 4 + .../revenue-by-customer-type-widget.tsx | 124 ++++++ .../src/widgets/sales-by-coupon/index.ts | 1 + .../sales-by-coupon-widget.tsx | 87 ++++ .../src/widgets/sales-by-device/index.ts | 4 + .../sales-by-device-widget.tsx | 135 ++++++ .../src/widgets/sales-by-utm/index.ts | 1 + .../sales-by-utm/sales-by-utm-widget.tsx | 125 ++++++ .../src/widgets/sessions-by-device/index.ts | 1 + .../sessions-by-device-widget.module.scss | 3 + .../sessions-by-device-widget.tsx | 101 +++++ .../src/widgets/total-returns/index.ts | 1 + .../total-returns/total-returns-widget.tsx | 85 ++++ .../src/widgets/visitor-metric/index.ts | 1 + .../visitor-metric/widget-visitor-metric.tsx | 39 ++ .../widgets/visitors-by-location/index.tsx | 1 + .../use-visitors-by-location.ts | 110 +++++ .../visitors-by-location-widget.module.scss | 40 ++ .../visitors-by-location-widget.tsx | 293 +++++++++++++ 152 files changed, 11181 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/package.json create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/chart.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/donut-widget.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/index.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/README.md new file mode 100644 index 000000000000..0d04716eabe8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/README.md @@ -0,0 +1,390 @@ +# @next-woo-analytics/widgets-toolkit + +A collection of focused, single-responsibility components for building WooCommerce Analytics widgets. +Each component has a clear API and specific purpose, making them easy to understand, test, and compose. + +## Installation + +```bash +npm install @next-woo-analytics/widgets-toolkit +``` + +## Components + +### MetricValue + +Displays a formatted numeric value. Does NOT handle comparisons or deltas. + +**Props:** +- `value` (number) - The numeric value to display +- `format` ('number' | 'currency' | 'percentage') - How to format the value (default: 'number') +- `formatter` ((value: number) => string) - Custom formatter function (overrides format) +- `size` ('small' | 'medium' | 'large') - Size variant (default: 'medium') +- `color` ('neutral' | 'positive' | 'negative') - Color variant (default: 'neutral') +- `className` (string) - CSS class for styling + +**Examples:** + +```tsx +import { MetricValue } from '@next-woo-analytics/widgets-toolkit'; + +// Simple number + + +// Currency + + +// Custom formatter + `${ v } items` } /> + +// Large positive value + +``` + +--- + +### MetricDelta + +Displays the change between two values (as percentage or absolute). + +**Props:** +- `current` (number) - The current/new value +- `previous` (number) - The previous/comparison value +- `fallback` (string) - Display when calculation fails (default: '—') +- `hideZero` (boolean) - Hide when delta is zero (default: false) +- `invertColors` (boolean) - For metrics where decrease is improvement (default: false) +- `showAbsolute` (boolean) - Show absolute change instead of percentage (default: false) +- `absoluteFormat` ('number' | 'currency') - Format for absolute values (default: 'number') +- `className` (string) - CSS class for styling + +**Examples:** + +```tsx +import { MetricDelta } from '@next-woo-analytics/widgets-toolkit'; + +// Percentage change: +50% + + +// Absolute change: +50 + + +// Inverted colors (for metrics where lower is better) +// Shows -33% in green + + +// Hide when no change + +``` + +**Delta Calculation:** +- Returns percentage change: `( ( current - previous ) / |previous| ) * 100` +- Returns `null` if inputs are invalid or previous is zero (displays fallback) +- Returns `0` if both current and previous are zero + +--- + +### MetricWithComparison + +Composite component that combines MetricValue and MetricDelta. + +**Props:** +- `value` (number) - The current value +- `previousValue` (number | null) - Previous value for comparison (no delta if null) +- `format` ('number' | 'currency' | 'percentage') - How to format the value (default: 'number') +- `formatter` ((value: number) => string) - Custom formatter for the value +- `direction` ('row' | 'column') - Layout direction (default: 'row') +- `size` ('small' | 'medium' | 'large') - Size of the main value (default: 'medium') +- `invertDeltaColors` (boolean) - Invert delta colors (default: false) +- `hideDeltaOnZero` (boolean) - Hide delta when zero (default: false) +- `showAbsoluteDelta` (boolean) - Show absolute change (default: false) +- `deltaFallback` (string) - Delta fallback text +- `className` (string) - Container CSS class + +**Examples:** + +```tsx +import { MetricWithComparison } from '@next-woo-analytics/widgets-toolkit'; + +// Simple metric with comparison + +// Renders: $1,250 +25% + +// Metric where lower is better (e.g., bounce rate) + +// Renders: 15% -25% (in green) + +// Vertical layout + + +// No comparison + +// Renders: $1,250 (no delta) +``` + +--- + +### ComparativeLineChart + +Responsive line chart wrapper for displaying time-series data with comparison support. +Handles automatic resizing and provides sensible defaults for analytics visualizations. + +**Props:** +- `series` (SeriesData[]) - Array of series data to display in the chart +- `dataFormat` (DataFormat) - Format configuration for tooltips (required) +- `className` (string) - CSS class for the chart container (optional) + +**Note:** Y-axis ticks are automatically formatted using the `dataFormat.type` with multipliers and zero decimals for concise labels (e.g., "1K", "2.5M"). Tooltips display full precision values according to `dataFormat` configuration. + +**DataFormat Type:** +```tsx +type DataFormat = { + type: 'number' | 'currency' | 'percentage' | 'average'; + options?: { + useMultipliers?: boolean; + decimals?: number; + }; +}; +``` + +**Examples:** + +```tsx +import { ComparativeLineChart, getFormatByMetricKey } from '@next-woo-analytics/widgets-toolkit'; + +// Simple line chart with currency formatting + + +// Chart with comparison data and number format + + +// Chart with multipliers (for large numbers like visitors) + + +// Using helper for predefined metric formats + +``` + +--- + +### ChartTooltip + +Internal chart tooltip component used by `ComparativeLineChart`. Displays formatted values and dates for primary and comparison series. + +**Props:** +- `tooltipData` - Tooltip data from chart (provided by LineChart) +- `colorScale` - Function to get color for series keys +- `dataFormat` (DataFormat) - Format configuration for values +- `shape` ('line' | 'circle' | 'rect') - Legend shape type (default: 'line') +- `shapeSize` (number) - Size of legend shape in pixels (default: 16) + +**Note:** This component is typically used internally by `ComparativeLineChart` and doesn't need to be used directly. + +--- + +## Helpers + +### getFormatByMetricKey + +Returns the appropriate `DataFormat` configuration for a given metric key. + +**Signature:** +```tsx +function getFormatByMetricKey( metricKey: MetricKey ): DataFormat +``` + +**Supported Metrics:** +- `orders_no` - Number format +- `total_sales` - Currency format +- `average_order_value` - Currency format +- `avg_items` - Average format +- `orders_value_net` - Currency format +- `orders_value_gross` - Currency format +- `coupons` - Currency format +- `profit_margin` - Currency format +- `visitors` - Number format with multipliers + +**Example:** +```tsx +import { getFormatByMetricKey, ComparativeLineChart } from '@next-woo-analytics/widgets-toolkit'; + + +// Returns: { type: 'currency' } + + +// Returns: { type: 'number', options: { useMultipliers: true, decimals: 0 } } +``` + +--- + +### applyThemeStylesToSeries + +Injects theme styles into chart series, so each series has everything it needs to render correctly (stroke color, strokeDasharray, strokeWidth, etc.) without depending on the theme context at render time. + +**Signature:** +```tsx +function applyThemeStylesToSeries( + series: SeriesData[], + chartTheme: ReturnType< typeof useChartTheme > +): SeriesData[] +``` + +**Example:** +```tsx +import { + applyThemeStylesToSeries, + useChartTheme, + ComparativeLineChart, +} from '@next-woo-analytics/widgets-toolkit'; + +const chartTheme = useChartTheme(); +const styledSeries = applyThemeStylesToSeries( series, chartTheme ); + + +``` + +**What it does:** +- Maps `chartTheme.seriesLineStyles` to each series +- Sets `options.stroke` from `chartTheme.colors[ 0 ]` +- Sets `options.seriesLineStyle` with strokeWidth, strokeDasharray, etc. +- Returns original series unchanged if no theme styles available + +--- + +### formatOrderMetric + +Creates a formatter function for a specific order metric. + +**Signature:** +```tsx +function formatOrderMetric( + metricKey: MetricKey, + options?: FormatMetricValueOptions +): ( value: number ) => string +``` + +**Example:** +```tsx +import { formatOrderMetric } from '@next-woo-analytics/widgets-toolkit'; + +const formatter = formatOrderMetric( 'total_sales' ); +formatter( 1234.56 ); // Returns: "$1,234.56" + +const visitorFormatter = formatOrderMetric( 'visitors' ); +visitorFormatter( 15000 ); // Returns: "15K" +``` + +--- + +## Types + +### DataFormat + +Configuration object for formatting chart values and tooltips. + +```tsx +type DataFormat = { + type: 'number' | 'currency' | 'percentage' | 'average'; + options?: { + useMultipliers?: boolean; // Use K, M, B suffixes for large numbers + decimals?: number; // Number of decimal places + }; +}; +``` + +### MetricKey + +Union type of all supported metric keys. + +```tsx +type OrderMetricKey = + | 'orders_no' + | 'total_sales' + | 'average_order_value' + | 'avg_items' + | 'orders_value_net' + | 'orders_value_gross' + | 'coupons' + | 'profit_margin'; + +type VisitorsMetricKey = 'visitors'; + +type MetricKey = OrderMetricKey | VisitorsMetricKey; +``` + +--- + +## Styling + +Components use CSS Modules for styling. You can customize appearance by: + +1. **Using className props**: Pass custom classes to any component +2. **CSS variables**: Components respect design system tokens +3. **Overriding styles**: Use CSS Modules or styled-components + +Example: +```tsx + +``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/package.json b/projects/packages/premium-analytics/packages/widgets-toolkit/package.json new file mode 100644 index 000000000000..b34c61a23e4b --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/package.json @@ -0,0 +1,33 @@ +{ + "name": "@next-woo-analytics/widgets-toolkit", + "description": "Widgets Toolkit for WooCommerce Analytics", + "version": "1.0.0", + "wpModule": true, + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./build/src/index.js" + }, + "dependencies": { + "@automattic/dashboard": "*", + "@automattic/design-system": "*", + "@automattic/admin-toolkit": "*", + "@automattic/charts": "*", + "@next-woo-analytics/components": "workspace:*", + "@next-woo-analytics/data": "workspace:*", + "@woocommerce-next/data": "*", + "@next-woo-analytics/datetime": "workspace:*", + "@wc-analytics/formatters": "workspace:*", + "@next-woo-analytics/icons": "workspace:*", + "@next-woo-analytics/routing": "workspace:*", + "@wordpress/components": "*", + "@wordpress/compose": "*", + "@wordpress/icons": "*", + "@ciab/dataviews": "*", + "@tanstack/react-router": "*", + "date-fns": "*", + "@wordpress/theme": "*", + "@wordpress/ui": "*", + "clsx": "*" + } +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md new file mode 100644 index 000000000000..19a23124b322 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md @@ -0,0 +1,217 @@ +# BarChart + +A **pure** vertical bar chart component for displaying categorical data. Built on top of `@automattic/charts` with support for negative values, making it ideal for monetary widgets showing refunds, returns, or discounts. + +## Pure Component Design + +This component is **pure and self-contained**—it receives all styling via props and has no external dependencies on context providers or themes. + +```tsx +import { BarChart } from '@next-woo-analytics/widgets-toolkit'; + +; +``` + +**Why this matters:** + +- Predictable rendering — same props always produce the same output +- Easy to test in isolation +- No implicit dependencies to track + +## Basic Usage + +### With `styles` prop (recommended) + +The cleanest approach is to pass styles as a separate prop. Styles are applied to series by index: + +```tsx +import { BarChart, type BarChartStyle } from '@next-woo-analytics/widgets-toolkit'; + +const styles: BarChartStyle[] = [ + { stroke: '#3858E9' }, +]; + +const chartData = [ + { + label: 'Sales', + data: [ + { label: 'SUMMER20', value: 4500 }, + { label: 'WELCOME10', value: 3200 }, + { label: 'FLASH25', value: 2800 }, + ], + }, +]; + +; +``` + +### With styles in chartData (fallback) + +Alternatively, define styles directly in each series via `options`. +This is useful when styles are dynamically generated per-series: + +```tsx +const chartData = [ + { + label: 'Sales', + data: [ ... ], + options: { stroke: '#10B981' }, + }, +]; + +; +``` + +**Style resolution priority:** `styles` prop > `chartData[].options.stroke` fallback + +## Handling Negative Values + +The component supports negative values, making it ideal for showing refunds, returns, or discounts: + +```tsx +const revenueData = [ + { + label: 'Revenue', + data: [ + { label: 'Product Sales', value: 15000 }, + { label: 'Shipping', value: 2500 }, + { label: 'Refunds', value: -3200 }, + { label: 'Discounts', value: -1500 }, + ], + }, +]; + +; +``` + +## Comparison Mode + +Display multiple series to compare periods: + +```tsx +const comparisonData = [ + { + label: 'Current Period', + data: [ + { label: 'SUMMER20', value: 4500 }, + { label: 'WELCOME10', value: 3200 }, + ], + }, + { + label: 'Previous Period', + data: [ + { label: 'SUMMER20', value: 3800 }, + { label: 'WELCOME10', value: 2900 }, + ], + }, +]; + +const styles: BarChartStyle[] = [ + { stroke: '#3858E9' }, // Primary - Blueberry + { stroke: '#66BDFF' }, // Comparison - Blue 30 +]; + +; +``` + +## Using with Theme Providers + +Widgets wrapped in `GlobalChartsProvider` can use `getElementStyles` from the context to resolve theme colors: + +```tsx +import { BarChart } from '@next-woo-analytics/widgets-toolkit'; +import { useGlobalChartsContext } from '@automattic/charts'; + +function MyWidget( { chartData } ) { + const { getElementStyles } = useGlobalChartsContext(); + + const barStyles = chartData.map( ( seriesData, index ) => { + const { color } = getElementStyles( { + data: seriesData, + index, + } ); + return { stroke: color }; + } ); + + return ( + + ); +} +``` + +## Props + +| Prop | Type | Required | Description | +| ------------ | ----------------- | -------- | ----------------------------------------------------- | +| `chartData` | `BarChartData` | Yes | Array of series with categorical data points | +| `dataFormat` | `DataFormat` | Yes | Format for values (tooltips): currency, number, percentage | +| `styles` | `BarChartStyle[]` | No | Styles for each series (by index) | +| `className` | `string` | No | CSS class for the chart container | + +## BarChartStyle Type + +```typescript +type BarChartStyle = { + /** Bar fill color */ + stroke: string; +}; +``` + +## DataFormat Type + +```typescript +type DataFormat = { + type: 'currency' | 'number' | 'percentage'; +}; +``` + +## Empty State + +When all values are zero, the chart: + +1. **Disables tooltips** — no meaningless "0" tooltips on hover +2. **Shows a fixed Y-axis domain** — so 0 appears at the bottom with meaningful tick values + +Default domains by data format: +- `currency`: 0 - 4K +- `number`: 0 - 80 +- `percentage`: 0% - 100% + +## Internal Components + +### ChartTooltip + +The tooltip displays data points when hovering over bars. It uses: +- Rectangle indicators (matching bar shape) +- WPDS design tokens for consistent styling +- `MetricValue` component for formatted values + +## Features + +- **Responsive sizing**: Automatically adapts to container dimensions +- **Pure component**: No context dependencies - all data flows through props +- **Negative value support**: Can display both positive and negative values +- **Multiple series**: Support for comparison periods +- **Tooltips**: Built-in tooltip support with formatted values +- **Empty state handling**: Fixed Y-axis domain when data is empty +- **Custom styling**: Apply custom colors via `styles` prop or `className` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss new file mode 100644 index 000000000000..6da45934886d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss @@ -0,0 +1,41 @@ +.chart { + + // Override visx-bar default styles that break the layout + // Todo: address upstream in Charts package. + /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ + :global(.visx-bar) { + // All corners rounded to handle both positive and negative bar values consistently. + clip-path: inset(0 round 4px); + } + + + .legend { + height: var(--wpds-font-line-height-lg); + min-height: var(--wpds-font-line-height-lg); + flex-wrap: nowrap; + + // Vertically center the legend shape. + // The Charts package applies a non-zero transform to legend circles, which offsets them + // from the label baseline in this layout. Reset the transform here so the circle is + // aligned with the accompanying text. TODO: address this default upstream in the Charts package. + circle { + transform: translate(0, 0) !important; + } + } + + .legendItem { + min-width: 0; + gap: var(--wpds-dimension-gap-sm); + padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px + } + + .legendLabel { + min-width: 0; + flex: 1 1 0 !important; // Override the 0 0 auto default value + } + + .emptyState { + flex: 1; + min-height: 200px; + } +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx new file mode 100644 index 000000000000..ecbf1482c415 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx @@ -0,0 +1,271 @@ +/** + * External dependencies + */ +import { BarChart as BarChartBase } from '@automattic/charts'; +import { Icon } from '@wordpress/ui'; +import clsx from 'clsx'; +import { useCallback, useMemo, useId } from 'react'; + +import type { ComponentProps } from 'react'; +/** + * Internal dependencies + */ +import type { DataFormat } from '../../types'; +import { RESIZE_DEBOUNCE_MS } from '../../constants'; +import { isEmptyChartData, getEmptyChartDomain } from '../../helpers'; +import { ChartEmptyState } from '../chart-empty-state'; +import { ChartTooltip } from '../chart-tooltip'; +import styles from './bar-chart.module.scss'; + +export type BarChartData = ComponentProps< typeof BarChartBase >[ 'data' ]; + +/** + * Inferred types from BarChart (BarChartBase) + */ +type BarChartBaseProps = ComponentProps< typeof BarChartBase >; +type RenderTooltipParams = Parameters< + NonNullable< BarChartBaseProps[ 'renderTooltip' ] > +>[ 0 ]; + +/** + * Style configuration for bar chart. + */ +export type BarChartStyle = { + /** Bar fill color */ + stroke: string; +}; + +export type BarChartProps = { + /** + * Chart data (series with data points). + * Colors can be provided via chartData[].options.stroke. + */ + chartData: BarChartData; + + /** + * Format configuration for chart values (tooltips) + */ + dataFormat: DataFormat; + + /** + * Explicit styles for bars. When provided, these take priority + * over styles defined in chartData[].options.stroke. + */ + styles?: BarChartStyle[]; + + /** + * Optional className for the container + */ + className?: string; + + /** + * Icon to display in the empty state + */ + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; + + /** + * Text to display in the empty state + */ + emptyStateText?: string; + + /** + * Whether to show a thin bar for zero values when the chart is rendered. + * When true and the data is not considered empty, zero-value bars render + * with a small visible height so users have something to hover over for + * tooltips. When all values are 0 or null and the chart is treated as + * empty, an empty state is shown instead and this option has no effect. + * @default true + */ + showZeroValues?: boolean; +}; + +/** + * Resolves bar styles from either the explicit styles prop or series options. + * Priority: styles prop > chartData[].options.stroke fallback + * + * @param stylesFromProp - Explicit styles from component prop + * @param chartData - Chart data with optional stroke colors + * @return Array of resolved styles, one per series + */ +function resolveSeriesStyles( + stylesFromProp: BarChartStyle[] | undefined, + chartData: BarChartData +): BarChartStyle[] { + // If styles prop is provided, use it directly + if ( stylesFromProp?.length ) { + return stylesFromProp; + } + + // Fallback: extract styles from chartData options + return ( + chartData?.map( ( series ) => ( { + stroke: series.options?.stroke ?? 'currentColor', + } ) ) ?? [ { stroke: 'currentColor' } ] + ); +} + +/** + * Applies resolved styles to chart data for the internal BarChart. + * Sets options.stroke on each series. + * + * @param chartData - Original chart data + * @param resolvedStyles - Styles to apply + * @return Chart data with styles applied to options + */ +function applyStylesToSeries( + chartData: BarChartData, + resolvedStyles: BarChartStyle[] +): BarChartData { + return chartData.map( ( seriesItem, index ) => { + const style = resolvedStyles[ index ] ?? resolvedStyles[ 0 ]; + + if ( ! style?.stroke ) { + return seriesItem; + } + + return { + ...seriesItem, + options: { + ...( seriesItem.options ?? {} ), + stroke: style.stroke, + }, + }; + } ); +} + +/** + * Pure BarChart component. + * Does not depend on any context provider - all data flows through props. + * + * Colors can be provided via chartData[].options.stroke or via styles prop. + * Uses RectShape from chart library for tooltip indicators. + */ +export function BarChart( { + chartData, + dataFormat, + styles: stylesProp, + className, + emptyStateIcon, + emptyStateText, + showZeroValues = true, +}: BarChartProps ) { + const chartId = useId(); + + /** + * Resolve styles: prop takes priority, fallback to chartData options. + * This array is used for tooltip styling and to decorate chart data. + */ + const resolvedStyles = useMemo< BarChartStyle[] >( + () => resolveSeriesStyles( stylesProp, chartData ), + [ stylesProp, chartData ] + ); + + /** + * Apply resolved styles to chart data for the internal BarChart. + * Only needed when styles come from prop; otherwise chartData already has styles. + */ + const styledChartData = useMemo( () => { + // If no styles prop, chartData already has its styles in options + if ( ! stylesProp?.length ) { + return chartData; + } + return applyStylesToSeries( chartData, resolvedStyles ); + }, [ stylesProp, chartData, resolvedStyles ] ); + + /** + * Detect if chart data is empty (all values are 0). + * Used to disable tooltips when there's no meaningful data to display. + */ + const isEmptyData = useMemo( + () => isEmptyChartData( styledChartData ), + [ styledChartData ] + ); + + /** + * Chart options for empty data state. + * Sets a fixed Y-axis domain so the chart shows 0 at the bottom + * with meaningful tick values instead of a flat line. + */ + const chartOptions = useMemo( () => { + if ( ! isEmptyData ) { + return { + // Apply ellipsis to x-axis labels when they overflow. + axis: { + x: { + labelOverflow: 'ellipsis' as const, + }, + }, + }; + } + + const domain = getEmptyChartDomain( dataFormat.type ); + return { + yScale: { domain }, + }; + }, [ isEmptyData, dataFormat.type ] ); + + const getTooltipLabel = useCallback( + ( datum: { label: string }, _index: number, key: string ): string => { + if ( key ) { + // Show the key (typically the date range label) in the tooltip if available, + // since the bar's label is already shown on the x-axis. This helps distinguish + // between current period and comparison period bars in tooltips. + return key; + } + return datum.label; + }, + [] + ); + + const renderTooltip = useCallback( + ( params: RenderTooltipParams ) => { + return ( + + ); + }, + [ dataFormat, resolvedStyles, getTooltipLabel ] + ); + + if ( isEmptyData ) { + return ( + + ); + } + + return ( + + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts new file mode 100644 index 000000000000..e2b31a36db21 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts @@ -0,0 +1,6 @@ +export { + BarChart, + type BarChartProps, + type BarChartData, + type BarChartStyle, +} from './bar-chart'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md new file mode 100644 index 000000000000..f4caad09bdfb --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md @@ -0,0 +1,173 @@ +# ComparativeLineChart + +A **pure** line chart component for comparing time series data across different periods. Built on top of `@automattic/charts` with automatic date alignment for comparison series. + +## Pure Component Design + +This component is **pure and self-contained**—it receives all styling via props and has no external dependencies on context providers or themes. + +```tsx +import { ComparativeLineChart } from '@next-woo-analytics/widgets-toolkit'; + +; +``` + +**Why this matters:** + +- Predictable rendering — same props always produce the same output +- Easy to test in isolation +- No implicit dependencies to track + +## Basic Usage + +### With `styles` prop (recommended) + +The cleanest approach is to pass styles as a separate prop. Styles are applied to series by index: + +```tsx +import { + ComparativeLineChart, + type SeriesStyle, +} from '@next-woo-analytics/widgets-toolkit'; + +const styles: SeriesStyle[] = [ + { stroke: '#3858E9', strokeWidth: 2 }, + { stroke: '#3858E9', strokeDasharray: '4 4', strokeWidth: 1.5 }, +]; + +const series = [ + { + label: 'Jan 1-7, 2024', + group: 'primary', + options: {}, + data: [ + { date: new Date( '2024-01-01' ), value: 1000 }, + { date: new Date( '2024-01-02' ), value: 1200 }, + ], + }, + { + label: 'Dec 25-31, 2023', + group: 'primary', + options: { type: 'comparison' }, + data: [ + { date: new Date( '2023-12-25' ), value: 900 }, + { date: new Date( '2023-12-26' ), value: 1100 }, + ], + }, +]; + +; +``` + +### With styles in series (fallback) + +Alternatively, define styles directly in each series via `options`. +This is useful when each series needs different colors or when styles are dynamically generated per-series: + +```tsx +const series = [ + { + label: 'Jan 1-7, 2024', + group: 'primary', + data: [ ... ], + options: { + stroke: '#10B981', + seriesLineStyle: { strokeWidth: 2 }, + }, + }, + { + label: 'Dec 25-31, 2023', + group: 'comparison', + data: [ ... ], + options: { + stroke: '#F59E0B', + seriesLineStyle: { strokeDasharray: '4 4', strokeWidth: 1.5 }, + }, + }, +]; + +; +``` + +**Style resolution priority:** `styles` prop > `series[].options` fallback + +## Using with Theme Providers + +Widgets wrapped in `GlobalChartsProvider` can use `getElementStyles` from the context to resolve theme colors: + +```tsx +import { ComparativeLineChart } from '@next-woo-analytics/widgets-toolkit'; +import { useGlobalChartsContext } from '@automattic/charts'; + +function MyWidget( { series } ) { + const { getElementStyles } = useGlobalChartsContext(); + + const seriesStyles = series.map( ( seriesData, index ) => { + const { color, lineStyles } = getElementStyles( { + data: seriesData, + index, + } ); + return { + stroke: color, + ...lineStyles, + }; + } ); + + return ( + + ); +} +``` + +## Props + +| Prop | Type | Required | Description | +| ------------ | ------------------------------ | -------- | -------------------------------------------------- | +| `series` | `ComparativeLineChartSeries[]` | Yes | Array of series with data | +| `styles` | `SeriesStyle[]` | No | Styles for each series (by index) | +| `dataFormat` | `DataFormat` | Yes | Format for values (Y-axis ticks and tooltips) | +| `tickFormat` | `string` | No | Custom X-axis date format (date-fns format string) | +| `className` | `string` | No | CSS class for the chart container | + +## SeriesStyle Type + +```typescript +type SeriesStyle = { + stroke: string; + strokeWidth?: number | string; + strokeDasharray?: string | number; + strokeLinecap?: 'butt' | 'round' | 'square' | 'inherit'; + strokeLinejoin?: 'miter' | 'round' | 'bevel' | 'inherit'; + opacity?: number | string; +}; +``` + +## Date Alignment + +The component automatically aligns comparison series to the primary series for X-axis display: + +1. First series (`series[0]`) is the reference +2. Comparison series dates are shifted to align with the primary +3. Original dates are preserved for tooltip display + +**Example**: A comparison series with Dec 25-31 dates will visually align to Jan 1-7 on the X-axis, but tooltips show the real Dec 25-31 dates. + +## Empty State + +When all values are zero, the chart shows a fixed Y-axis domain: + +- `currency`: 0 - 4K +- `number`: 0 - 80 +- `percentage`: 0% - 100% diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss new file mode 100644 index 000000000000..fa2581da88ea --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss @@ -0,0 +1,26 @@ +.chart { + height: 100%; + + .legend { + flex: 0 0 auto; + height: var(--wpds-font-line-height-lg); + min-height: var(--wpds-font-line-height-lg); + flex-wrap: nowrap; + } + + .legendItem { + min-width: 0; + gap: var(--wpds-dimension-gap-sm); + padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px + } + + .legendLabel { + // font-size and color come from chartTheme.legendLabelStyles + min-width: 0; + flex: 1 1 0 !important; // Override the 0 0 auto default value + + span { + display: block; + } + } +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx new file mode 100644 index 000000000000..74bedaea6aa1 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx @@ -0,0 +1,364 @@ +/** + * External dependencies + */ +import { useCallback, useMemo } from 'react'; +import { LineChart } from '@automattic/charts'; +import { formatDate, formatMetricValue } from '@wc-analytics/formatters'; +import { type ComponentProps } from 'react'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import { ChartTooltip } from '../chart-tooltip'; +import { alignSeriesDates } from './utils'; +import { RESIZE_DEBOUNCE_MS } from '../../constants'; +import { isEmptyChartData, getEmptyChartDomain } from '../../helpers'; +import type { ComparativeLineChartSeries, SeriesStyle } from './types'; +import type { DataFormat } from '../../types'; +import styles from './comparative-line-chart.module.scss'; + +/** + * Resolves series styles from either the explicit styles prop or series options. + * Priority: styles prop > series[].options fallback + * + * @param stylesFromProp - Explicit styles passed as component prop + * @param series - Series data (may contain options with styles) + * @return Array of resolved styles, one per series + */ +function resolveSeriesStyles( + stylesFromProp: SeriesStyle[] | undefined, + series: ComparativeLineChartSeries[] +): SeriesStyle[] { + // If styles prop is provided, use it directly + if ( stylesFromProp?.length ) { + return stylesFromProp; + } + + // Fallback: extract styles from series options + return series.map( ( s ) => { + const lineStyle = s.options?.seriesLineStyle; + + return { + stroke: s.options?.stroke ?? '', + strokeWidth: lineStyle?.strokeWidth, + strokeDasharray: lineStyle?.strokeDasharray, + strokeLinecap: lineStyle?.strokeLinecap, + strokeLinejoin: lineStyle?.strokeLinejoin, + opacity: lineStyle?.opacity, + }; + } ); +} + +/** + * Default margin for charts. + * Y-axis is on the left, so right margin is always 0. + */ +const DEFAULT_MARGIN = { right: 0 }; + +/** + * Applies resolved styles to series data for the internal LineChart. + * Sets options.stroke and options.seriesLineStyle on each series. + * + * @param series - Original series data + * @param resolvedStyles - Styles to apply + * @return Series with styles applied to options + */ +function applyStylesToSeries( + series: ComparativeLineChartSeries[], + resolvedStyles: SeriesStyle[] +): ComparativeLineChartSeries[] { + return series.map( ( seriesItem, index ) => { + const style = resolvedStyles[ index ] ?? resolvedStyles[ 0 ]; + + if ( ! style?.stroke ) { + return seriesItem; + } + + const { stroke, ...lineStyleProps } = style; + + return { + ...seriesItem, + options: { + ...( seriesItem.options ?? {} ), + stroke, + seriesLineStyle: lineStyleProps, + }, + }; + } ); +} + +/** + * Inferred types + */ +type LineChartProps = ComponentProps< typeof LineChart >; +type RenderTooltipParams = Parameters< + NonNullable< LineChartProps[ 'renderTooltip' ] > +>[ 0 ]; + +/** + * Props for the ComparativeLineChart component. + * + * Combines series data with chart options, formatting, and responsive behavior. + * Wraps @automattic/charts LineChart with sensible defaults for comparative data visualization. + * + * Note: The chart defaults to margin.right = 0 since the Y-axis is positioned on the left. + */ +export type ComparativeLineChartProps = { + /** + * Array of series data to display in the chart. + * Series can include styling via options.stroke and options.seriesLineStyle + * as a fallback when styles prop is not provided. + */ + series: ComparativeLineChartSeries[]; + + /** + * Explicit styles for each series. When provided, these take priority + * over any styles defined in series[].options. + * Array index corresponds to series index. + */ + styles?: SeriesStyle[]; + + /** + * CSS class for the chart container + */ + className?: string; + + /** + * Format configuration for chart values (Y-axis ticks and tooltips) + */ + dataFormat: DataFormat; + + tickFormat?: string; +} & Omit< + ComponentProps< typeof LineChart >, + | 'data' + | 'options' + | 'withLegendGlyph' + | 'smoothing' + | 'showLegend' + | 'withGradientFill' + | 'resizeDebounceTime' + | 'withTooltips' + | 'renderTooltip' +>; + +export function ComparativeLineChart( { + series, + styles: stylesProp, + className, + dataFormat, + tickFormat: xTickFormatType, + maxWidth = Infinity, +}: ComparativeLineChartProps ) { + /** + * Resolve styles: prop takes priority, fallback to series options. + * This array is used for tooltip styling and to decorate series data. + */ + const resolvedStyles = useMemo< SeriesStyle[] >( + () => resolveSeriesStyles( stylesProp, series ), + [ stylesProp, series ] + ); + + /** + * Custom label extractor for line chart datum. + * Uses realDate for comparison series to show the actual date. + * + * @param datum - The data point with date information + * @param index - Index of this entry in the tooltip + */ + const getTooltipLabel = useCallback( + ( datum: { date: Date; realDate?: Date }, index: number ): string => { + const isComparison = index > 0; + const displayDate = isComparison + ? datum.realDate ?? datum.date + : datum.date; + return formatDate( displayDate ); + }, + [] + ); + + const renderTooltip = useCallback( + ( params: RenderTooltipParams ) => { + return ( + + ); + }, + [ dataFormat, resolvedStyles, getTooltipLabel ] + ); + + /** + * Y-axis formatter using dataFormat configuration, + * but using multipliers and 0 decimals to keep strings short and concise. + */ + const yTickFormat = useMemo( + () => ( value: number ) => + formatMetricValue( value, dataFormat.type, { + useMultipliers: true, + decimals: 0, + } ), + [ dataFormat ] + ); + + /** + * Creates margin object for fixed domain charts. + * The chart library doesn't auto-adjust left margin for fixed domains, + * so we estimate based on the formatted max value length. + */ + const createDomainMargin = useCallback( + ( maxValue: number ) => ( { + ...DEFAULT_MARGIN, + left: yTickFormat( maxValue ).length * 10, + } ), + [ yTickFormat ] + ); + + /** + * Align comparison series dates to primary series for X-axis display. + * Original dates are preserved in realDate for tooltip display. + */ + const alignedSeries = useMemo( + () => alignSeriesDates( series ), + [ series ] + ); + + /** + * Apply resolved styles to series data for the internal LineChart. + * Only needed when styles come from prop; otherwise series already have styles. + */ + const styledSeries = useMemo( () => { + // If no styles prop, series already have their styles in options + if ( ! stylesProp?.length ) { + return alignedSeries; + } + return applyStylesToSeries( alignedSeries, resolvedStyles ); + }, [ stylesProp, alignedSeries, resolvedStyles ] ); + + /** + * Detect if chart data is empty and apply special props for empty state + */ + const isEmptyData = useMemo( + () => isEmptyChartData( styledSeries ), + [ styledSeries ] + ); + + /** + * For percentage metrics, always use a fixed domain [0, 1.0] (0% to 100%) + * regardless of actual data values or empty state + */ + const percentageDomain: [ number, number ] | null = useMemo( () => { + return dataFormat.type === 'percentage' ? [ 0, 1.0 ] : null; + }, [ dataFormat.type ] ); + + const emptyChartProps = useMemo( () => { + if ( ! isEmptyData ) { + return {}; + } + + const domain = getEmptyChartDomain( dataFormat.type ); + + return { + chartOptions: { yScale: { domain } }, + margin: createDomainMargin( domain[ 1 ] ), + }; + }, [ isEmptyData, dataFormat.type, createDomainMargin ] ); + + /** + * Calculate margin for percentage charts + */ + const percentageMargin = useMemo( () => { + if ( ! percentageDomain ) { + return undefined; + } + return createDomainMargin( percentageDomain[ 1 ] ); + }, [ percentageDomain, createDomainMargin ] ); + + const xTickFormat = useCallback( + ( date: number ) => formatDate( date, xTickFormatType ?? 'short' ), + [ xTickFormatType ] + ); + + /** + * Merge chart options with empty chart options if data is empty + * For percentage metrics, always apply fixed domain + */ + const chartOptions = useMemo( () => { + const baseOptions = { + axis: { + x: { + // Use the chart library's default behavior for 'custom' presets + tickFormat: xTickFormatType ? xTickFormat : undefined, + }, + y: { + tickFormat: yTickFormat, + }, + }, + }; + + // Apply percentage domain if applicable + if ( percentageDomain ) { + return { + ...baseOptions, + yScale: { domain: percentageDomain }, + }; + } + + if ( ! isEmptyData ) { + return baseOptions; + } + + // Merge with empty chart options + return { + ...baseOptions, + ...emptyChartProps.chartOptions, + }; + }, [ + xTickFormat, + xTickFormatType, + yTickFormat, + percentageDomain, + isEmptyData, + emptyChartProps.chartOptions, + ] ); + + return ( + + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/index.ts new file mode 100644 index 000000000000..a5bae53a6c11 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/index.ts @@ -0,0 +1,3 @@ +export { ComparativeLineChart } from './comparative-line-chart'; +export type { ComparativeLineChartProps } from './comparative-line-chart'; +export type { SeriesStyle } from './types'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts new file mode 100644 index 000000000000..415cdb54ab87 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { + type SeriesData, + type DataPointDate, + type LineStyles, +} from '@automattic/charts'; + +/** + * Types + */ +export type ComparativeDatePointDate = DataPointDate & { + date: Date; // <- date is required by the comparative line chart. + realDate?: Date; +}; + +export type ComparativeLineChartSeries = SeriesData & { + // We expect SeriesData.data to be an array of DataPointDate. + data: ComparativeDatePointDate[]; +}; + +/** + * Style configuration for a single series. + * Derived from LineStyles (SVG line attributes) with required stroke. + */ +export type SeriesStyle = LineStyles & { + /** Line stroke color (required) */ + stroke: string; +}; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts new file mode 100644 index 000000000000..97f849830886 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts @@ -0,0 +1,338 @@ +/** + * Internal dependencies + */ +import { alignSeriesDates } from './align-series-dates'; +import type { ComparativeLineChartSeries } from '../types'; + +/** + * Helper to create a series with dates. + */ +function createSeries( + label: string, + dates: Date[], + values?: number[] +): ComparativeLineChartSeries { + return { + label, + data: dates.map( ( date, i ) => ( { + date, + value: values?.[ i ] ?? i * 10, + } ) ), + }; +} + +describe( 'alignSeriesDates', () => { + describe( 'edge cases', () => { + it( 'returns empty array as-is', () => { + const result = alignSeriesDates( [] ); + expect( result ).toEqual( [] ); + } ); + + it( 'returns single series unchanged', () => { + const series = [ + createSeries( 'Primary', [ + new Date( '2024-01-01' ), + new Date( '2024-01-02' ), + ] ), + ]; + + const result = alignSeriesDates( series ); + + expect( result ).toBe( series ); // Same reference + expect( result[ 0 ].data[ 0 ].date ).toEqual( + new Date( '2024-01-01' ) + ); + } ); + + it( 'handles series with empty data arrays', () => { + const series: ComparativeLineChartSeries[] = [ + { label: 'Primary', data: [] }, + { label: 'Comparison', data: [] }, + ]; + + const result = alignSeriesDates( series ); + + expect( result ).toBe( series ); // Returns original when primary has no data + } ); + + it( 'handles comparison series with empty data', () => { + const primary = createSeries( 'Primary', [ + new Date( '2024-01-01' ), + new Date( '2024-01-02' ), + ] ); + const comparison: ComparativeLineChartSeries = { + label: 'Comparison', + data: [], + }; + + const result = alignSeriesDates( [ primary, comparison ] ); + + expect( result[ 0 ] ).toBe( primary ); // Primary unchanged + expect( result[ 1 ] ).toBe( comparison ); // Empty comparison returned as-is + } ); + } ); + + describe( 'index-based date alignment', () => { + it( 'aligns comparison dates to corresponding primary dates by index', () => { + const primary = createSeries( 'This Week', [ + new Date( '2024-01-08' ), // Monday of this week + new Date( '2024-01-09' ), + new Date( '2024-01-10' ), + ] ); + + const comparison = createSeries( 'Last Week', [ + new Date( '2024-01-01' ), // Monday of last week + new Date( '2024-01-02' ), + new Date( '2024-01-03' ), + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + // Primary should be unchanged + expect( result[ 0 ].data[ 0 ].date ).toEqual( + new Date( '2024-01-08' ) + ); + + // Comparison dates should match primary dates by index + expect( result[ 1 ].data[ 0 ].date ).toEqual( + new Date( '2024-01-08' ) + ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( + new Date( '2024-01-09' ) + ); + expect( result[ 1 ].data[ 2 ].date ).toEqual( + new Date( '2024-01-10' ) + ); + } ); + + it( 'handles weekly intervals with different start days', () => { + // This is the key scenario: weeks that don't start on the same day + // Primary: Sep 12 (Thu) - period starts mid-week + // Comparison: Jun 14 (Sat) - period starts on different day + const primary = createSeries( 'Current Period', [ + new Date( '2024-09-12' ), // Week 1 starts Thu + new Date( '2024-09-16' ), // Week 2 starts Mon + new Date( '2024-09-23' ), // Week 3 + ] ); + + const comparison = createSeries( 'Previous Period', [ + new Date( '2024-06-14' ), // Week 1 starts Sat + new Date( '2024-06-17' ), // Week 2 starts Mon + new Date( '2024-06-24' ), // Week 3 + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + // Comparison should get primary's dates for perfect alignment + expect( result[ 1 ].data[ 0 ].date ).toEqual( + new Date( '2024-09-12' ) + ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( + new Date( '2024-09-16' ) + ); + expect( result[ 1 ].data[ 2 ].date ).toEqual( + new Date( '2024-09-23' ) + ); + + // Original dates preserved for tooltip + expect( result[ 1 ].data[ 0 ].realDate ).toEqual( + new Date( '2024-06-14' ) + ); + } ); + + it( 'preserves original dates in realDate property', () => { + const primary = createSeries( 'This Week', [ + new Date( '2024-01-08' ), + new Date( '2024-01-09' ), + ] ); + + const comparison = createSeries( 'Last Week', [ + new Date( '2024-01-01' ), + new Date( '2024-01-02' ), + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + // Original dates preserved in realDate + expect( result[ 1 ].data[ 0 ].realDate ).toEqual( + new Date( '2024-01-01' ) + ); + expect( result[ 1 ].data[ 1 ].realDate ).toEqual( + new Date( '2024-01-02' ) + ); + } ); + + it( 'does not add realDate to primary series', () => { + const primary = createSeries( 'This Week', [ + new Date( '2024-01-08' ), + ] ); + const comparison = createSeries( 'Last Week', [ + new Date( '2024-01-01' ), + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + expect( result[ 0 ].data[ 0 ] ).not.toHaveProperty( 'realDate' ); + } ); + + it( 'returns series unchanged when dates already align', () => { + const primary = createSeries( 'Series A', [ + new Date( '2024-01-01' ), + new Date( '2024-01-02' ), + ] ); + + const comparison = createSeries( 'Series B', [ + new Date( '2024-01-01' ), // Same start date + new Date( '2024-01-02' ), + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + // When dates already align, comparison should be returned as-is + expect( result[ 1 ] ).toBe( comparison ); + } ); + } ); + + describe( 'series with different lengths', () => { + it( 'handles comparison with more points than primary', () => { + const primary = createSeries( 'Primary', [ + new Date( '2024-01-08' ), + new Date( '2024-01-09' ), + ] ); + + const comparison = createSeries( 'Comparison', [ + new Date( '2024-01-01' ), + new Date( '2024-01-02' ), + new Date( '2024-01-03' ), // Extra point + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + // First two points align by index + expect( result[ 1 ].data[ 0 ].date ).toEqual( + new Date( '2024-01-08' ) + ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( + new Date( '2024-01-09' ) + ); + // Extra point gets last primary date + expect( result[ 1 ].data[ 2 ].date ).toEqual( + new Date( '2024-01-09' ) + ); + } ); + + it( 'handles comparison with fewer points than primary', () => { + const primary = createSeries( 'Primary', [ + new Date( '2024-01-08' ), + new Date( '2024-01-09' ), + new Date( '2024-01-10' ), + ] ); + + const comparison = createSeries( 'Comparison', [ + new Date( '2024-01-01' ), + new Date( '2024-01-02' ), + ] ); + + const result = alignSeriesDates( [ primary, comparison ] ); + + // Both comparison points align to their corresponding primary dates + expect( result[ 1 ].data[ 0 ].date ).toEqual( + new Date( '2024-01-08' ) + ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( + new Date( '2024-01-09' ) + ); + } ); + } ); + + describe( 'multiple comparison series', () => { + it( 'aligns all comparison series to primary', () => { + const primary = createSeries( 'Current', [ + new Date( '2024-03-01' ), + new Date( '2024-03-02' ), + ] ); + + const lastMonth = createSeries( 'Last Month', [ + new Date( '2024-02-01' ), + new Date( '2024-02-02' ), + ] ); + + const lastYear = createSeries( 'Last Year', [ + new Date( '2023-03-01' ), + new Date( '2023-03-02' ), + ] ); + + const result = alignSeriesDates( [ primary, lastMonth, lastYear ] ); + + // All series should now use primary's dates + expect( result[ 0 ].data[ 0 ].date ).toEqual( + new Date( '2024-03-01' ) + ); + expect( result[ 1 ].data[ 0 ].date ).toEqual( + new Date( '2024-03-01' ) + ); + expect( result[ 2 ].data[ 0 ].date ).toEqual( + new Date( '2024-03-01' ) + ); + + // Original dates preserved + expect( result[ 1 ].data[ 0 ].realDate ).toEqual( + new Date( '2024-02-01' ) + ); + expect( result[ 2 ].data[ 0 ].realDate ).toEqual( + new Date( '2023-03-01' ) + ); + } ); + } ); + + describe( 'data preservation', () => { + it( 'preserves all other data point properties', () => { + const primary: ComparativeLineChartSeries = { + label: 'Primary', + data: [ + { date: new Date( '2024-01-08' ), value: 100 }, + { date: new Date( '2024-01-09' ), value: 200 }, + ], + }; + + const comparison: ComparativeLineChartSeries = { + label: 'Comparison', + data: [ + { date: new Date( '2024-01-01' ), value: 50 }, + { date: new Date( '2024-01-02' ), value: 75 }, + ], + }; + + const result = alignSeriesDates( [ primary, comparison ] ); + + // Values should be preserved + expect( result[ 1 ].data[ 0 ].value ).toBe( 50 ); + expect( result[ 1 ].data[ 1 ].value ).toBe( 75 ); + } ); + + it( 'preserves series options and other properties', () => { + const primary: ComparativeLineChartSeries = { + label: 'Primary', + data: [ { date: new Date( '2024-01-08' ), value: 100 } ], + options: { stroke: '#ff0000' }, + }; + + const comparison: ComparativeLineChartSeries = { + label: 'Comparison', + data: [ { date: new Date( '2024-01-01' ), value: 50 } ], + options: { + stroke: '#0000ff', + seriesLineStyle: { opacity: 0.5 }, + }, + }; + + const result = alignSeriesDates( [ primary, comparison ] ); + + expect( result[ 1 ].label ).toBe( 'Comparison' ); + expect( result[ 1 ].options ).toEqual( { + stroke: '#0000ff', + seriesLineStyle: { opacity: 0.5 }, + } ); + } ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts new file mode 100644 index 000000000000..0c807c70d9bf --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts @@ -0,0 +1,79 @@ +/** + * Internal dependencies + */ +import type { ComparativeLineChartSeries } from '../types'; + +/** + * Aligns comparison series dates to primary series dates by index. + * + * Each comparison point gets assigned the date of the corresponding primary point + * (same index), ensuring both series align perfectly on the X-axis regardless of + * their original date intervals. Original dates are preserved in realDate for tooltips. + * + * This approach handles: + * - Different period lengths (e.g., weeks starting on different days) + * - Partial intervals at period boundaries + * - Any time granularity (daily, weekly, monthly) + * + * @param series - Array of series data where index 0 is primary and index 1+ are comparison + * @return New array with aligned series (comparison dates match primary, originals in realDate) + */ +export function alignSeriesDates( + series: ComparativeLineChartSeries[] +): ComparativeLineChartSeries[] { + if ( series.length < 2 ) { + return series; + } + + const [ primary, ...rest ] = series; + + if ( ! primary.data.length ) { + return series; + } + + const alignedRest = rest.map( ( comparisonSeries ) => { + if ( ! comparisonSeries.data.length ) { + return comparisonSeries; + } + + // Check if alignment is needed by comparing first dates + const primaryFirstDate = primary.data[ 0 ]?.date; + const comparisonFirstDate = comparisonSeries.data[ 0 ]?.date; + + const primaryFirstMs = + primaryFirstDate instanceof Date + ? primaryFirstDate.getTime() + : primaryFirstDate; + + const comparisonFirstMs = + comparisonFirstDate instanceof Date + ? comparisonFirstDate.getTime() + : comparisonFirstDate; + + // If dates already align, return as-is + if ( primaryFirstMs === comparisonFirstMs ) { + return comparisonSeries; + } + + // Align by index: each comparison point gets the primary point's date + return { + ...comparisonSeries, + data: comparisonSeries.data.map( ( point, index ) => { + // Use corresponding primary date, or last primary date if comparison has more points + const primaryDate = + primary.data[ index ]?.date ?? + primary.data[ primary.data.length - 1 ]?.date; + + return { + ...point, + // Use primary's date for X-axis alignment + date: primaryDate, + // Preserve original date for tooltip display + realDate: point.date, + }; + } ), + }; + } ); + + return [ primary, ...alignedRest ]; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/index.ts new file mode 100644 index 000000000000..0ab92737831e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/index.ts @@ -0,0 +1 @@ +export { alignSeriesDates } from './align-series-dates'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md new file mode 100644 index 000000000000..d868aa9856a8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md @@ -0,0 +1,160 @@ +# DonutChart + +A responsive donut (pie) chart component that automatically adapts to its container size. + +## Features + +- **Auto-resize**: Automatically scales to fit the parent container +- **Pure component**: No context dependencies - all data flows through props +- **Theme support**: Colors can be provided via `styles` prop or inline in `chartData` +- **Comparison mode**: Shows delta percentage when `comparisonValue` is provided +- **Legend integration**: Optional legend with comparison deltas +- **Validation**: Falls back to metric-only display when data is invalid + +## Usage + +```tsx +import { DonutChart } from '@next-woo-analytics/widgets-toolkit'; + +const chartData = [ + { label: 'Completed', value: 45, percentage: 56.25 }, + { label: 'Pending', value: 25, percentage: 31.25 }, + { label: 'Cancelled', value: 10, percentage: 12.5 }, +]; + +const styles = [ + { color: '#3858E9' }, + { color: '#66BDFF' }, + { color: '#A77EFF' }, +]; + + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `chartData` | `DonutChartData` | required | Array of segments with `label`, `value`, and `percentage` | +| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | +| `value` | `number` | required | Primary metric value displayed in center | +| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | +| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | +| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | +| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | +| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | + +## Data Validation + +The component validates chart data before rendering: + +1. **No negative values**: Both `value` and `percentage` must be >= 0 +2. **100% total**: Percentages must sum to approximately 100% (within 0.01 tolerance) + +When validation fails, the component displays a fallback view showing only the metric and legend without the chart. + +## Responsive Layout + +The component uses a reference/wrapper pattern to achieve fluid sizing: + +``` +┌─────────────────────────────┐ +│ .reference (relative) │ ← Takes 100% width from parent +│ ┌─────────────────────────┐ │ +│ │ .wrapper (absolute) │ │ ← Fills reference, observed by ResizeObserver +│ │ ┌─────────────────────┐ │ │ +│ │ │ Stack (content) │ │ │ ← Chart + Legend +│ │ └─────────────────────┘ │ │ +│ └─────────────────────────┘ │ +└─────────────────────────────┘ +``` + +### How it works + +1. **`.reference`** - Outer container with `position: relative` and `width: 100%`. Sets initial height from content or defaults to 164px. + +2. **`.wrapper`** - Absolutely positioned to fill the reference. The `ResizeObserver` attached to the inner `Stack` captures available dimensions. + +3. **Dynamic sizing** - The chart size is calculated as the minimum of container width, height, and the default size (164px). + +4. **SVG scaling** - The `PieChart` receives the calculated size and renders proportionally. + +### Default dimensions + +Before the first resize observation, the chart uses sensible defaults: +- Size: 164px (width and height) + +## Storybook + +Run `pnpm storybook` and navigate to **Widgets Toolkit / Components / DonutChart** to see: + +- **Default** - Basic chart without legend +- **WithLegend** - Chart with legend items +- **WithComparison** - Shows positive delta +- **NegativeComparison** - Shows negative delta +- **CurrencyFormat** - Currency formatted values +- **Resizable** - Drag container edges to test auto-resize +- **SmallContainer** - 200px narrow container +- **LargeContainer** - 400px wide container with 5 segments +- **BookingsByStatus** - Real-world booking status example +- **NewVsReturning** - Customer segmentation example +- **InvalidData** - Shows fallback when data is invalid + +## Providing Colors + +Colors can be provided in two ways: + +### 1. Via `styles` prop (recommended) + +```tsx +const styles = [ + { color: '#3858E9' }, + { color: '#66BDFF' }, + { color: '#A77EFF' }, +]; + + +``` + +### 2. Inline in chartData + +```tsx +const chartData = [ + { label: 'Completed', value: 45, percentage: 56, color: '#3858E9' }, + { label: 'Pending', value: 25, percentage: 31, color: '#66BDFF' }, +]; +``` + +The `styles` prop takes priority when both are provided. + +## Integration with Theme + +For widgets using `GlobalChartsProvider`, obtain colors via `getElementStyles`: + +```tsx +const { getElementStyles } = useGlobalChartsContext(); + +const segmentStyles = chartData.map( ( segment, index ) => { + const { color } = getElementStyles( { data: segment, index } ); + return { color }; +} ); + + +``` + +## Comparison with SemiCircleChart + +| Feature | DonutChart | SemiCircleChart | +|---------|------------|-----------------| +| Shape | Full circle | Half circle | +| Use case | Status distribution | Two-segment comparison | +| Default size | 164px | 220x100px | +| Metric position | Center | Bottom center | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.module.scss new file mode 100644 index 000000000000..36d8d6262c04 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.module.scss @@ -0,0 +1,36 @@ +.reference { + width: 100%; + height: 100%; + position: relative; +} + +.wrapper { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +.chart { + position: relative; + display: flex; + justify-content: center; + align-items: center; + min-width: 96px; + max-width: 192px; + width: 100%; +} + +.metricContainer { + position: absolute; + pointer-events: none; +} + +.noChart { + height: 100%; +} + +.legendContainer { + width: 200px; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx new file mode 100644 index 000000000000..1e4420992a52 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx @@ -0,0 +1,265 @@ +/** + * External dependencies + */ +import { PieChartUnresponsive as PieChart } from '@automattic/charts'; +import { Icon, Stack } from '@wordpress/ui'; +import { useResizeObserver } from '@wordpress/compose'; +import { useMemo, useState } from 'react'; + +import type { ComponentProps } from 'react'; +/** + * Internal dependencies + */ +import { PieChartTooltip } from '../chart-tooltip'; +import { MetricWithComparison } from '../metric-with-comparison'; +import { Legend as LegendPure } from '../legend/legend'; +import type { LegendItem } from '../legend/legend'; +import type { DataFormat } from '../../types'; +import { + resolveSegmentStyles, + applyStylesToItems, + isEmptyPieChartData, + type SegmentStyle, +} from '../../helpers'; +import { ChartEmptyState } from '../chart-empty-state'; +import styles from './donut-chart.module.scss'; + +// Default chart configuration +const DEFAULT_THICKNESS = 0.3; +const DEFAULT_CORNER_SCALE = 0.03; +const DEFAULT_GAP_SCALE = 0.01; + +export type DonutChartData = ComponentProps< typeof PieChart >[ 'data' ]; + +const DEFAULT_SIZE = 164; + +export type DonutChartProps = { + /** + * Chart segment data (label, value, percentage). + * Colors can be provided here or via styles prop. + */ + chartData: DonutChartData; + + /** + * Explicit styles for each segment. When provided, these take priority + * over colors defined in chartData[].color. + * Array index corresponds to segment index. + */ + styles?: SegmentStyle[]; + + /** + * Primary metric value (total) + */ + value: number; + + /** + * Optional comparison value (previous period) + */ + comparisonValue?: number | null; + + /** + * Format for displaying values + */ + dataFormat?: DataFormat; + + /** + * Legend items. Colors will be applied from styles prop if provided. + */ + legendData?: LegendItem[]; + + /** + * Show legend below chart + */ + showLegend?: boolean; + + /** + * Thickness of the arc (0-1). + * @default 0.3 + */ + thickness?: number; + + /** + * Icon to display in the empty state + */ + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; + + /** + * Text to display in the empty state + */ + emptyStateText?: string; + + /** + * Enable tooltips on pie chart hover. + * @default false + */ + withTooltips?: boolean; + + /** + * Horizontal offset for tooltip positioning. + */ + tooltipOffsetX?: number; + + /** + * Vertical offset for tooltip positioning. + */ + tooltipOffsetY?: number; + + /** + * Format for tooltip segment values. Use when the segment values have a + * different format than the center metric's `dataFormat` (e.g. center shows + * percentage but segments are currency). Falls back to `dataFormat`. + */ + tooltipDataFormat?: DataFormat; +}; + +/** + * Pure DonutChart component. + * Does not depend on any context provider - all data flows through props. + * + * Colors can be provided via: + * 1. `styles` prop (takes priority) - array of { color } per segment + * 2. `chartData[].color` - inline color per segment + */ +export function DonutChart( { + chartData, + styles: stylesProp, + value, + comparisonValue, + dataFormat = { + type: 'number', + options: { useMultipliers: true, decimals: 0 }, + }, + legendData, + showLegend = true, + thickness = DEFAULT_THICKNESS, + emptyStateIcon, + emptyStateText, + withTooltips = false, + tooltipOffsetX, + tooltipOffsetY, + tooltipDataFormat, +}: DonutChartProps ) { + const hasComparison = + comparisonValue !== null && comparisonValue !== undefined; + + const [ widgetHeight, setWidgetHeight ] = useState< number >( 0 ); + + /** + * Chart width will pick the width of the chart element + * via CSS, following the `chart` class name + */ + const [ chartWidth, setChartWidth ] = useState< number >( 0 ); + + const ref = useResizeObserver( ( entries ) => { + const entry = entries?.[ 0 ]; + if ( ! entry?.contentRect ) { + return; + } + + setWidgetHeight( entry.contentRect.height ); + + const chartElement = entry.target.children[ 0 ]; + if ( chartElement ) { + setChartWidth( chartElement.clientWidth ); + } + } ); + + /** + * Resolve styles: prop takes priority, fallback to chartData colors. + */ + const resolvedStyles = useMemo( + () => resolveSegmentStyles( stylesProp, chartData ), + [ stylesProp, chartData ] + ); + + /** + * Apply styles to chart data + */ + const styledChartData = useMemo( () => { + if ( ! stylesProp?.length ) { + return chartData; + } + return applyStylesToItems( chartData, resolvedStyles ); + }, [ stylesProp, chartData, resolvedStyles ] ); + + /** + * Apply styles to legend data + */ + const styledLegendData = useMemo( () => { + if ( ! legendData ) { + return undefined; + } + return applyStylesToItems( legendData, resolvedStyles ); + }, [ legendData, resolvedStyles ] ); + + const isEmptyData = isEmptyPieChartData( chartData ); + + // Render empty state when no data is available + if ( isEmptyData ) { + return ( + + ); + } + + return ( +
+
+ + ( + + ) } + showLabels={ false } + > + + + + { showLegend && styledLegendData && ( +
+ +
+ ) } +
+
+
+ ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts new file mode 100644 index 000000000000..d57f81465c1f --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts @@ -0,0 +1,5 @@ +export { + DonutChart, + type DonutChartProps, + type DonutChartData, +} from './donut-chart'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss new file mode 100644 index 000000000000..b06aad82338f --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss @@ -0,0 +1,12 @@ +.container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + gap: var(--wpds-dimension-gap-lg); +} + +.icon { + color: var(--wpds-color-stroke-surface-neutral-weak, #E0E0E0); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx new file mode 100644 index 000000000000..a94df1e8ce9d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import { EmptyState } from '@automattic/design-system'; +import { Icon } from '@wordpress/ui'; +import { __ } from '@wordpress/i18n'; +import { cautionFilled } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import styles from './chart-empty-state.module.scss'; + +export type ChartEmptyStateProps = { + /** + * Icon to display in the empty state. + * Should be a ReactNode (typically an SVG icon). + * Defaults to cautionFilled if not provided. + */ + icon?: React.ComponentProps< typeof Icon >[ 'icon' ]; + + /** + * Text to display in the empty state. + * @default "No data found for this date range." + */ + text?: string; +}; + +/** + * ChartEmptyState component. + * + * A reusable empty state component for charts that displays an icon and text + * when no data is available. Designed to be used by chart wrapper components. + * + * @example + * ```tsx + * import { customer } from '@next-woo-analytics/icons'; + * + * // With custom icon + * + * + * // With custom text + * + * ``` + */ +export function ChartEmptyState( { + icon = cautionFilled, + text = __( 'No data found for this date range.', 'woocommerce-analytics' ), +}: ChartEmptyStateProps ) { + return ( + + { icon && ( + + ) } + { text } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts new file mode 100644 index 000000000000..e0bdf02935d9 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts @@ -0,0 +1,4 @@ +export { + ChartEmptyState, + type ChartEmptyStateProps, +} from './chart-empty-state'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md new file mode 100644 index 000000000000..592c2cad5f2b --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md @@ -0,0 +1,314 @@ +# LeaderboardChart + +A responsive leaderboard (horizontal bar) chart component for displaying ranking and "top X by Y" data visualizations. + +## Features + +- **Context-aware styling**: Integrates with GlobalChartsProvider for consistent theming +- **Comparison mode**: Shows current vs. previous period data with delta indicators +- **Flexible formatting**: Supports currency, number, percentage, and custom formats +- **Empty state handling**: Built-in empty state with customizable content +- **Legend support**: Optional legend with customizable labels +- **Overlay labels**: Alternative styling with labels on top of bars +- **Loading states**: Skeleton loaders during data fetch +- **Long label handling**: Automatic truncation and tooltips for long labels + +## Requirements + +**Important**: This component must be rendered within a `GlobalChartsProvider` context to access chart styling (colors, themes, element styles). + +```tsx +import { GlobalChartsProvider } from '@automattic/charts'; + + + +; +``` + +## Usage + +```tsx +import { LeaderboardChart } from '@next-woo-analytics/widgets-toolkit'; + +const data = [ + { + id: '1', + label: 'Direct traffic', + currentValue: 125000, + previousValue: 98000, + currentShare: 42, + previousShare: 35, + delta: 27.55, + }, + { + id: '2', + label: 'Google Ads', + currentValue: 87500, + previousValue: 92000, + currentShare: 29, + previousShare: 33, + delta: -4.89, + }, + { + id: '3', + label: 'Email campaign', + currentValue: 53000, + previousValue: 61000, + currentShare: 18, + previousShare: 22, + delta: -13.11, + }, +]; + +; +``` + +## Props + +| Prop | Type | Default | Description | +| ------------------ | ---------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `data` | `LeaderboardChartData` | required | Array of leaderboard items with label, values, shares, and deltas | +| `className` | `string` | - | Additional CSS classes for container | +| `loading` | `boolean` | `false` | Shows loading skeleton when true | +| `withComparison` | `boolean` | `false` | Enables comparison mode with previous period data | +| `withOverlayLabel` | `boolean` | `false` | Places labels on top of bars instead of beside them | +| `legendLabels` | `LegendLabels` | `{ primary: 'Current period', comparison: 'Previous period' }` | Custom legend labels | +| `showLegend` | `boolean` | `true` | Whether to show the legend | +| `dataFormat` | `DataFormat` | `{ type: 'currency', options: { useMultipliers: true, decimals: 2 } }` | Value formatting configuration | +| `emptyState` | `ReactNode` | - | Custom empty state content (overrides default) | +| `emptyStateIcon` | `ReactNode` | - | Icon to display in default empty state | +| `emptyStateText` | `string` | `'No data available'` | Text for default empty state | + +### LeaderboardChartData Type + +```tsx +type LeaderboardChartData = Array< { + id: string; + label: string; + currentValue: number; + previousValue: number; + currentShare: number; // Percentage (0-100) + previousShare: number; // Percentage (0-100) + delta: number; // Percentage change +} >; +``` + +### DataFormat Type + +```tsx +type DataFormat = { + type: 'currency' | 'number' | 'percentage' | 'average'; + options?: { + useMultipliers?: boolean; // Show 1K, 1M, etc. + decimals?: number; // Number of decimal places + signDisplay?: 'auto' | 'never' | 'always' | 'exceptZero'; // Sign display for numbers + // ... other format-specific options + }; +}; +``` + +## Common Use Cases + +### Basic Leaderboard (No Comparison) + +```tsx + +``` + +### With Comparison Period + +```tsx + +``` + +### Number Format (Not Currency) + +```tsx + +``` + +### Percentage Values + +```tsx + +``` + +### With Overlay Labels + +```tsx + +``` + +### Custom Empty State + +```tsx + } + emptyStateText="No results found for this period" +/> +``` + +Or with fully custom empty state: + +```tsx + + +

No Data Yet

+

Start tracking your metrics to see insights here

+ + } +/> +``` + +## Integration with GlobalChartsProvider + +The component automatically retrieves colors from the GlobalChartsProvider context: + +```tsx +import { GlobalChartsProvider } from '@automattic/charts'; +import { LeaderboardChart } from '@next-woo-analytics/widgets-toolkit'; + +function MyWidget() { + return ( + + + + ); +} +``` + +The component uses `getElementStyles()` from the context to: + +- Retrieve primary and secondary colors for bars +- Apply consistent theming across all charts +- Support both current period (index 0) and comparison period (index 1) colors + +## Empty State Behavior + +The component handles empty data gracefully: + +1. **No data + custom `emptyState` prop**: Renders your custom empty state component +2. **No data + `emptyStateIcon` and/or `emptyStateText`**: Renders default empty state with your customizations +3. **No data + no customization**: Renders default empty state with "No data available" message + +## Loading State + +When `loading={true}`, the component displays skeleton loaders that match the structure of the actual chart, providing visual feedback during data fetch operations. + +## Responsive Behavior + +The LeaderboardChart automatically adapts to its container width. For optimal display: + +- **Minimum width**: 280px recommended +- **Ideal width**: 400px+ for comfortable reading +- **Label truncation**: Long labels automatically truncate with ellipsis +- **Bar scaling**: Bars scale proportionally to container width + +## Storybook + +Run `pnpm storybook` and navigate to **Widgets Toolkit / Components / LeaderboardChart** to see: + +- **Default** - Basic leaderboard without comparison +- **WithComparison** - Current vs. previous period +- **Loading** - Loading skeleton state +- **EmptyState** - No data handling +- **WithOverlayLabel** - Labels on top of bars +- **WithoutLegend** - Chart without legend +- **LongLabels** - Label truncation handling +- **NumberFormat** - Number formatting (not currency) +- **PercentageFormat** - Percentage values +- **Container size variants** - Small (280px), Medium (400px), Large (600px) + +## Comparison with Other Chart Components + +| Feature | LeaderboardChart | DonutChart | SemiCircleChart | +| ------------------ | -------------------------- | ------------------ | ---------------------- | +| Shape | Horizontal bars | Full circle | Half circle | +| Use case | Rankings, top N | Distribution | Two-segment comparison | +| Context dependency | Yes (GlobalChartsProvider) | No (pure) | No (pure) | +| Comparison mode | Yes | Yes | Yes | +| Data items | Unlimited | Unlimited segments | 2-5 segments typical | + +## Common Patterns + +### Sales by Traffic Source + +```tsx + +``` + +### Top Products by Revenue + +```tsx + +``` + +### Conversion Rates by Campaign + +```tsx + +``` + +### Sales by Device Type + +```tsx + +``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/index.ts new file mode 100644 index 000000000000..443db065d04b --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/index.ts @@ -0,0 +1,9 @@ +export { LeaderboardChart } from './leaderboard-chart'; +export type { + LeaderboardChartProps, + LeaderboardChartData, + LegendLabels, +} from './leaderboard-chart'; + +export { LeaderboardLabel } from './leaderboard-label'; +export type { LeaderboardLabelProps } from './leaderboard-label'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss new file mode 100644 index 000000000000..31919b013c4b --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss @@ -0,0 +1,42 @@ +.container { + height: 100%; +} + +.chart { + height: 100%; + justify-content: space-between; + + .legend { + flex-wrap: nowrap; + } + + .legendItem { + min-width: 0; + } + + .legendLabel { + min-width: 0; + flex: 1 1 0 !important; // Override the 0 0 auto default value + + span { + display: block; + } + } +} + +.emptyState { + padding: 48px 24px; + min-height: 200px; +} + +.emptyStateIcon { + color: var(--wpds-color-fg-content-neutral-weak); + opacity: 0.5; +} + +.emptyStateText { + margin: 0; + color: var(--wpds-color-fg-content-neutral); + font-size: 14px; + text-align: center; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx new file mode 100644 index 000000000000..8918ea3bf652 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx @@ -0,0 +1,221 @@ +/** + * External dependencies + */ +import { Icon, Stack } from '@wordpress/ui'; +import { + LeaderboardChartUnresponsive as BaseLeaderboardChart, + useGlobalChartsContext, + Legend, + hexToRgba, +} from '@automattic/charts'; +import clsx from 'clsx'; +import type { ComponentProps, ReactNode } from 'react'; +import { useMemo } from 'react'; +import { formatMetricValue } from '@wc-analytics/formatters'; + +/** + * Internal dependencies + */ +import type { DataFormat } from '../../types'; +import type { WooChartTheme } from '../../hooks/use-chart-theme'; +import { ChartEmptyState } from '../chart-empty-state'; +import styles from './leaderboard-chart.module.scss'; + +type LeaderboardChartData = ComponentProps< + typeof BaseLeaderboardChart +>[ 'data' ]; + +export type { LeaderboardChartData }; + +export type LegendLabels = { + primary: string; + comparison: string; +}; + +export type LeaderboardChartProps = { + /** + * Card container styles + */ + className?: string; + + /** + * Leaderboard data (label, currentValue, previousValue, currentShare, previousShare, delta) + */ + data: LeaderboardChartData; + + /** + * Whether the widget is in a loading state + */ + loading?: boolean; + + /** + * Whether to show comparison data + */ + withComparison?: boolean; + + /** + * Whether to show overlay label on bars + */ + withOverlayLabel?: boolean; + + /** + * Custom legend labels + */ + legendLabels?: LegendLabels; + + /** + * Format for displaying values + */ + dataFormat?: DataFormat; + + /** + * Whether to show the legend + */ + showLegend?: boolean; + + /** + * Custom empty state content to display when no data is available + */ + emptyState?: ReactNode; + + /** + * Icon to display in the empty state + */ + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; + + /** + * Text to display in the empty state + */ + emptyStateText?: string; + + /** + * Custom styling for the chart container + */ + style?: React.CSSProperties & { + '--a8c--charts--leaderboard--bar--border-radius'?: string; + }; +}; + +/** + * Generic LeaderboardChart component for displaying ranking/leaderboard data. + * Used for "top X by Y" type visualizations (e.g., sales by source, by channel, by campaign). + * + * This component wraps @automattic/charts LeaderboardChartUnresponsive with standardized formatting and styling. + * + * **Requirements:** + * - Must be rendered within a GlobalChartsProvider context to access chart styling (colors, themes, element styles) + * + * Features: + * - Automatic empty state handling + * - Configurable value formatting (currency, number, percentage, etc.) + * - Comparison mode support + * - Customizable legend labels + * - Overlay label support for alternative styling + */ +export function LeaderboardChart( { + className, + data, + loading = false, + withComparison = false, + withOverlayLabel = false, + showLegend = true, + legendLabels, + dataFormat = { + type: 'currency', + options: { useMultipliers: true, decimals: 2 }, + }, + emptyStateIcon, + emptyStateText, + style, +}: LeaderboardChartProps ) { + const { getElementStyles, theme } = useGlobalChartsContext(); + + /** + * Create value formatter from dataFormat configuration + */ + const valueFormatter = useMemo( + () => ( value: number ) => + formatMetricValue( value, dataFormat.type, dataFormat.options ), + [ dataFormat ] + ); + + /** + * Get chart colors for legend + */ + const chartColors = useMemo( () => { + const { color: primaryColor } = getElementStyles( { index: 0 } ); + if ( ! withComparison ) { + return { primaryColor }; + } + const { color: secondaryColor } = getElementStyles( { index: 1 } ); + return { primaryColor, secondaryColor }; + }, [ withComparison, getElementStyles ] ); + + /** + * Merge theme bar border radius with style prop. + * Style prop takes precedence for per-widget overrides. + */ + const chartStyle = useMemo( () => { + const wooTheme = theme as WooChartTheme | undefined; + const barBorderRadius = wooTheme?.leaderboardChart?.barBorderRadius; + if ( ! barBorderRadius && ! style ) { + return undefined; + } + return { + '--a8c--charts--leaderboard--bar--border-radius': barBorderRadius, + ...style, + } as React.CSSProperties; + }, [ theme, style ] ); + + // Check if we have valid data + const isEmptyData = ! data || data.length === 0; + + if ( isEmptyData ) { + return ( + + ); + } + + return ( + + + { showLegend && ( + + ) } + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss new file mode 100644 index 000000000000..d1bc27cbfb8a --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss @@ -0,0 +1,15 @@ +.container { + padding: var(--wpds-dimension-padding-sm); +} + +.label { + font-size: var(--wpds-font-size-sm); +} + +.labelImage { + width: 28px; + height: 28px; + vertical-align: middle; + border-radius: var(--wpds-border-radius-md); + object-fit: cover; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx new file mode 100644 index 000000000000..13fffb7e3b4e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx @@ -0,0 +1,79 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import styles from './leaderboard-label.module.scss'; + +export type LeaderboardLabelProps = { + /** + * Label text + */ + label: string; + /** + * Image URL + */ + imageUrl?: string; + /** + * Alt text for the image + */ + imageAlt?: string; + /** + * Class name for the image + */ + imageClassName?: string; +}; + +// Simple default image for when the image is not available. +const DEFAULT_IMAGE_URL = + 'data:image/svg+xml;utf8,'; + +/** + * Leaderboard Label Component + * + * Renders a label with an optional image thumbnail for use in leaderboard charts. + * Displays image (if available) alongside the label. + * + * Features: + * - Image thumbnail with fallback + * - Error handling for failed image loads + * - Responsive layout with consistent spacing + * + * @param props - Component props + * @param props.label - Label text + * @param props.imageUrl - Optional image URL + * @param props.imageAlt - Alt text for the image + * @param props.imageClassName - Class name for the image + */ +export function LeaderboardLabel( { + label, + imageUrl, + imageAlt, + imageClassName, +}: LeaderboardLabelProps ) { + // Use default if undefined OR empty string to prevent broken image flash + const finalImageUrl = imageUrl || DEFAULT_IMAGE_URL; + + return ( + + ) => { + e.currentTarget.src = DEFAULT_IMAGE_URL; + } } + alt={ imageAlt || label } + className={ clsx( styles.labelImage, imageClassName ) } + /> + { label } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md new file mode 100644 index 000000000000..2312b755ff89 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md @@ -0,0 +1,127 @@ +# SemiCircleChart + +A responsive semi-circle (half-donut) chart component that fills its parent container. + +## Features + +- **Responsive**: Uses `@automattic/charts` responsive `PieSemiCircleChart` to fill the parent container +- **Pure component**: No context dependencies - all data flows through props +- **Theme support**: Colors can be provided via `styles` prop or inline in `chartData` +- **Comparison mode**: Shows delta percentage when `comparisonValue` is provided +- **Legend integration**: Optional legend with comparison deltas +- **Tooltips**: Optional hover tooltips with configurable formatting + +## Usage + +```tsx +import { SemiCircleChart } from '@next-woo-analytics/widgets-toolkit'; + +const chartData = [ + { label: 'Mobile', value: 4500 }, + { label: 'Desktop', value: 2500 }, + { label: 'Tablet', value: 1000 }, +]; + +const styles = [ + { color: '#3858E9' }, + { color: '#66BDFF' }, + { color: '#A77EFF' }, +]; + + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `chartData` | `SemiCircleChartData` | required | Array of segments with `label` and `value` (percentage is auto-calculated) | +| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | +| `value` | `number` | required | Primary metric value displayed in center | +| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | +| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | +| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | +| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | +| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | +| `maxWidth` | `number` | `Infinity` | Maximum width constraint for the chart | +| `withTooltips` | `boolean` | `false` | Enable tooltips on hover | +| `tooltipOffsetX` | `number` | - | Horizontal offset for tooltip positioning | +| `tooltipOffsetY` | `number` | - | Vertical offset for tooltip positioning | +| `tooltipDataFormat` | `DataFormat` | - | Format for tooltip values (falls back to `dataFormat`) | +| `emptyStateIcon` | `IconProps['icon']` | - | Icon for empty state | +| `emptyStateText` | `string` | - | Text for empty state | + +## Responsive Layout + +The chart fills its parent container automatically using the responsive `PieSemiCircleChart` from `@automattic/charts`. Use `maxWidth` to constrain the size when needed: + +```tsx +// Fills parent container + + +// Constrained to 220px max + +``` + +## Storybook + +Run `pnpm storybook` and navigate to **Widgets Toolkit / Components / SemiCircleChart** to see: + +- **Default** - Basic chart without legend +- **WithLegend** - Chart with legend items +- **WithComparison** - Shows positive delta +- **NegativeComparison** - Shows negative delta +- **Resizable** - Drag container edges to test auto-resize +- **SmallContainer** - 200px narrow container +- **LargeContainer** - 400px wide container with 5 segments + +## Providing Colors + +Colors can be provided in two ways: + +### 1. Via `styles` prop (recommended) + +```tsx +const styles = [ + { color: '#3858E9' }, + { color: '#66BDFF' }, + { color: '#A77EFF' }, +]; + + +``` + +### 2. Inline in chartData + +```tsx +const chartData = [ + { label: 'Mobile', value: 4500, color: '#3858E9' }, + { label: 'Desktop', value: 2500, color: '#66BDFF' }, +]; +``` + +The `styles` prop takes priority when both are provided. + +## Integration with Theme + +For widgets using `GlobalChartsProvider`, obtain colors via `getElementStyles`: + +```tsx +const { getElementStyles } = useGlobalChartsContext(); + +const segmentStyles = chartData.map( ( segment, index ) => { + const { color } = getElementStyles( { data: segment, index } ); + return { color }; +} ); + + +``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/index.ts new file mode 100644 index 000000000000..ed9c059697de --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/index.ts @@ -0,0 +1,5 @@ +export { + SemiCircleChart, + type SemiCircleChartProps, + type SemiCircleChartData, +} from './semi-circle-chart'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.module.scss new file mode 100644 index 000000000000..767f0251d757 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.module.scss @@ -0,0 +1,16 @@ +.container, +.wrapper { + width: 100%; +} + +.chart { + position: relative; +} + +.metricContainer { + position: absolute; + left: 0; + bottom: 0; + right: 0; + pointer-events: none; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx new file mode 100644 index 000000000000..374195855490 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx @@ -0,0 +1,239 @@ +/** + * External dependencies + */ +import { PieSemiCircleChart } from '@automattic/charts'; +import { Icon, Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; + +import type { ComponentProps } from 'react'; +/** + * Internal dependencies + */ +import { PieChartTooltip } from '../chart-tooltip'; +import { MetricWithComparison } from '../metric-with-comparison'; +import { Legend as LegendPure } from '../legend/legend'; +import type { LegendItem } from '../legend/legend'; +import type { DataFormat } from '../../types'; +import { + resolveSegmentStyles, + applyStylesToItems, + isEmptyPieChartData, + type SegmentStyle, +} from '../../helpers'; +import { ChartEmptyState } from '../chart-empty-state'; +import styles from './semi-circle-chart.module.scss'; +import { RESIZE_DEBOUNCE_MS } from '../../constants'; + +// Default chart configuration +const DEFAULT_THICKNESS = 0.3; + +export type SemiCircleChartData = ComponentProps< + typeof PieSemiCircleChart +>[ 'data' ]; + +export type SemiCircleChartProps = { + /** + * Chart segment data (label, value). + * Colors can be provided here or via styles prop. + */ + chartData: SemiCircleChartData; + + /** + * Explicit styles for each segment. When provided, these take priority + * over colors defined in chartData[].color. + * Array index corresponds to segment index. + */ + styles?: SegmentStyle[]; + + /** + * Primary metric value (total) + */ + value: number; + + /** + * Optional comparison value (previous period) + */ + comparisonValue?: number | null; + + /** + * Format for displaying values + */ + dataFormat?: DataFormat; + + /** + * Legend items. Colors will be applied from styles prop if provided. + */ + legendData?: LegendItem[]; + + /** + * Show legend below chart + */ + showLegend?: boolean; + + /** + * Thickness of the arc (0-1). + * @default 0.3 + */ + thickness?: number; + + /** + * Width of the chart. + * @default Infinity + */ + maxWidth?: number; + + /** + * Icon to display in the empty state + */ + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; + + /** + * Text to display in the empty state + */ + emptyStateText?: string; + + /** + * Enable tooltips on pie chart hover. + * @default false + */ + withTooltips?: boolean; + + /** + * Horizontal offset for tooltip positioning. + */ + tooltipOffsetX?: number; + + /** + * Vertical offset for tooltip positioning. + */ + tooltipOffsetY?: number; + + /** + * Format for tooltip segment values. Use when the segment values have a + * different format than the center metric's `dataFormat` (e.g. center shows + * percentage but segments are currency). Falls back to `dataFormat`. + */ + tooltipDataFormat?: DataFormat; +}; + +/** + * Pure SemiCircleChart component. + * Does not depend on any context provider - all data flows through props. + * + * Colors can be provided via: + * 1. `styles` prop (takes priority) - array of { color } per segment + * 2. `chartData[].color` - inline color per segment + */ +export function SemiCircleChart( { + chartData, + styles: stylesProp, + value, + comparisonValue, + dataFormat = { + type: 'number', + options: { useMultipliers: true, decimals: 0 }, + }, + legendData, + showLegend = true, + thickness = DEFAULT_THICKNESS, + maxWidth = Infinity, + emptyStateIcon, + emptyStateText, + withTooltips = false, + tooltipOffsetX, + tooltipOffsetY, + tooltipDataFormat, +}: SemiCircleChartProps ) { + const hasComparison = + comparisonValue !== null && comparisonValue !== undefined; + + /** + * Resolve styles: prop takes priority, fallback to chartData colors. + */ + const resolvedStyles = useMemo( + () => resolveSegmentStyles( stylesProp, chartData ), + [ stylesProp, chartData ] + ); + + /** + * Apply styles to chart data + */ + const styledChartData = useMemo( () => { + if ( ! stylesProp?.length ) { + return chartData; + } + return applyStylesToItems( chartData, resolvedStyles ); + }, [ stylesProp, chartData, resolvedStyles ] ); + + /** + * Apply styles to legend data + */ + const styledLegendData = useMemo( () => { + if ( ! legendData ) { + return undefined; + } + return applyStylesToItems( legendData, resolvedStyles ); + }, [ legendData, resolvedStyles ] ); + + const isEmptyData = isEmptyPieChartData( chartData ); + + // Render empty state when no data is available + if ( isEmptyData ) { + return ( + + ); + } + + return ( + + + ( + + ) } + resizeDebounceTime={ RESIZE_DEBOUNCE_MS } + > + + + + { showLegend && styledLegendData && ( + + ) } + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md new file mode 100644 index 000000000000..cfd2d4a0c417 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md @@ -0,0 +1,207 @@ +# ChartTooltip + +A **shared** tooltip component for chart visualizations. Supports both line charts and bar charts with configurable indicator types and value formatting. + +## Features + +- **Dual indicator types**: `line` for line charts, `rect` for bar charts +- **Configurable extractors**: Custom `getLabel` and `getValue` functions +- **Sensible defaults**: Works with `datum.label` and `datum.value` out of the box +- **WPDS styling**: Uses design tokens for consistent appearance +- **MetricValue integration**: Formatted values with currency, number, or percentage + +## Basic Usage + +### With Line Charts + +```tsx +import { ChartTooltip } from '../chart-tooltip'; + +const renderTooltip = ( params ) => ( + formatDate( datum.date ) } + /> +); +``` + +### With Bar Charts + +```tsx +import { ChartTooltip } from '../chart-tooltip'; + +const renderTooltip = ( params ) => ( + +); +``` + +## Props + +| Prop | Type | Required | Description | +|-----------------|-------------------------------------------|----------|-----------------------------------------------------------| +| `tooltipData` | `{ datumByKey?: Record }`| No | Tooltip data from visx chart | +| `dataFormat` | `DataFormat` | Yes | Format for values: currency, number, percentage | +| `seriesStyles` | `TooltipStyle[]` | Yes | Styles for each series (color, stroke properties) | +| `indicatorType` | `'line' \| 'rect'` | Yes | Shape indicator: line for line charts, rect for bars | +| `getLabel` | `(datum, index, key) => string` | No | Custom label extractor. `key` is the series key/label (default: `datum.label`) | +| `getValue` | `(datum) => number` | No | Custom value extractor (default: `datum.value`) | + + +## TooltipStyle Type + +```typescript +type TooltipStyle = { + /** Color for the indicator */ + stroke: string; + /** Stroke width (for line indicator) */ + strokeWidth?: string | number; + /** Stroke dash array (for line indicator) */ + strokeDasharray?: string | number; +}; +``` + +## Default Extractors + +The component provides sensible defaults that work with common chart data patterns: + +```typescript +// Default label extractor - uses datum.label +// The key parameter contains the series key (e.g., date range for bar charts) +function defaultGetLabel( datum: unknown, _index: number, _key: string ): string { + return ( datum as { label: string } ).label ?? ''; +} + +// Default value extractor - uses datum.value +function defaultGetValue( datum: unknown ): number { + return ( datum as { value: number } ).value; +} +``` + +### When to Use Custom Extractors + +**Line charts with dates**: Pass a custom `getLabel` to format dates: + +```tsx +const getLabel = ( datum, index, _key ) => { + const isComparison = index > 0; + const displayDate = isComparison ? datum.realDate ?? datum.date : datum.date; + return formatDate( displayDate ); +}; +``` + +**Bar charts with label-value data**: Use defaults (no custom extractors needed): + +```tsx +// Data format: { label: 'Category A', value: 1000 } +// Default extractors work automatically +``` + +## Indicator Types + +### Line Indicator (`indicatorType="line"`) + +Uses `LineShape` from the chart library. Supports: + +- `stroke` - Line color +- `strokeWidth` - Line thickness +- `strokeDasharray` - Dashed line pattern (e.g., `'4 4'`) + +### Rectangle Indicator (`indicatorType="rect"`) + +Uses `RectShape` from the chart library. Supports: + +- `stroke` - Fill color (8x8 pixel rectangle) + +## Styling + +The tooltip uses WPDS design tokens: + +- `--wpds-color-fg-content-neutral` - Text color +- `--wpds-elevation-sm` - Box shadow +- `--wpds-dimension-padding-sm` - Padding + +Global visx-tooltip overrides are applied to ensure consistent layout. + +## Used By + +- `ComparativeLineChart` - With `indicatorType="line"` and custom date label +- `BarChart` - With `indicatorType="rect"` and default label/value extractors + +--- + +# PieChartTooltip + +A tooltip component for **pie** and **semi-circle** charts. Renders a single row with a color indicator, label, and formatted value. + +Reuses the same SCSS module as `ChartTooltip` so styling (box-shadow, padding, visx-tooltip override) is shared. + +## Basic Usage + +```tsx +import { PieChartTooltip } from '../chart-tooltip'; + +const renderTooltip = ( { tooltipData } ) => ( + +); +``` + +## Props + +| Prop | Type | Required | Description | +|---------------|-----------------------|----------|----------------------------------------------------| +| `tooltipData` | `DataPointPercentage` | Yes | Tooltip data from pie chart hover (label, value, color) | +| `dataFormat` | `DataFormat` | Yes | Format for values: currency, number, percentage | + +## Used By + +- `DonutChart` - Pie chart tooltip with color indicators +- `SemiCircleChart` - Half-pie chart tooltip with color indicators + +--- + +# TooltipRow + +A shared building-block component that renders a single tooltip row: **indicator + label + formatted value**. Used internally by both `ChartTooltip` and `PieChartTooltip`. + +## Basic Usage + +```tsx +import { TooltipRow } from '../chart-tooltip'; +import { RectShape } from '@automattic/charts/visx/legend'; + + } + label="Revenue" + value={ 1234.56 } + dataFormat={ { type: 'currency' } } +/> +``` + +## Props + +| Prop | Type | Required | Description | +|-------------|-------------------|----------|-----------------------------------------------------| +| `indicator` | `React.ReactNode` | Yes | Pre-rendered indicator element (LineShape, RectShape, etc.) | +| `label` | `string` | Yes | Row label text | +| `value` | `number` | Yes | Numeric value to format | +| `dataFormat`| `DataFormat` | Yes | Format configuration (currency, number, percentage) | + +## Used By + +- `ChartTooltip` - For line and bar chart tooltip rows +- `PieChartTooltip` - For pie and semi-circle chart tooltip rows diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss new file mode 100644 index 000000000000..e45308e68118 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss @@ -0,0 +1,26 @@ +.tooltip { + color: var( --wpds-color-fg-content-neutral ); + padding: var( --wpds-dimension-padding-sm ); + margin: 0; + box-shadow: var( --wpds-elevation-sm ); + min-width: 200px; +} + +.item { + font-weight: 400; + line-height: var( --wpds-font-line-height-xs ); +} + +.label { + flex: 1; +} + +// Override visx-tooltip ONLY when our custom tooltip components are used. +// This applies to ChartTooltip (line/bar) and PieChartTooltip (pie/semi-circle). +/* stylelint-disable-next-line selector-pseudo-class-no-unknown */ +:global(.visx-tooltip):has( .tooltip ) { + max-width: none !important; + box-shadow: none !important; + margin: 0 !important; + padding: 0 !important; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx new file mode 100644 index 000000000000..dce120f41b30 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx @@ -0,0 +1,164 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import { LineShape, RectShape } from '@automattic/charts/visx/legend'; + +/** + * Internal dependencies + */ +import type { DataFormat } from '../../types'; +import { isChartDatumEntry } from './utils'; +import { TooltipRow } from './tooltip-row'; +import styles from './chart-tooltip.module.scss'; + +/** + * Style configuration for tooltip indicators. + * Matches SeriesStyle pattern from chart components. + */ +export type TooltipStyle = { + /** Color for the indicator */ + stroke: string; + + /** Stroke width (for line indicator) */ + strokeWidth?: string | number; + + /** Stroke dash array (for line indicator) */ + strokeDasharray?: string | number; + + /** Stroke dash offset (for line indicator) */ + strokeDashoffset?: string | number; +}; + +/** + * Common datum shape with label and value properties. + * Used by default extractors. + */ +type DatumWithLabel = { label: string }; +type DatumWithValue = { value: number }; + +/** + * Default label extractor - assumes datum has a 'label' property. + * Override for custom label formatting (e.g., date formatting for line charts). + * + * @param datum - The data point + */ +function defaultGetLabel( datum: unknown ): string { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic datum narrowed by convention + return ( datum as DatumWithLabel ).label ?? ''; +} + +/** + * Default value extractor - assumes datum has a 'value' property. + */ +function defaultGetValue( datum: unknown ): number { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic datum narrowed by convention + return ( datum as DatumWithValue ).value; +} + +export type ChartTooltipProps< TDatum = unknown > = { + /** + * Tooltip data from visx chart + */ + tooltipData?: { + datumByKey?: Record< string, unknown >; + }; + + /** + * Format configuration for chart values + */ + dataFormat: DataFormat; + + /** + * Array of styles for each series (required). + * Index corresponds to series index. + */ + seriesStyles: TooltipStyle[]; + + /** + * Indicator type: 'line' for line charts, 'rect' for bar charts + * Uses chart library's LineShape and RectShape components. + */ + indicatorType: 'line' | 'rect'; + + /** + * Function to extract label from datum. + * Defaults to extracting 'label' property. + */ + getLabel?: ( datum: TDatum, index: number, key: string ) => string; + + /** + * Function to extract value from datum. + * Defaults to extracting 'value' property. + */ + getValue?: ( datum: TDatum ) => number; +}; + +/** + * Self-contained tooltip component for charts. + * Handles rendering of tooltip rows with configurable indicators. + * + * Uses chart library's shape components (LineShape, RectShape) for visual consistency. + * + * Provides sensible defaults for common chart data patterns: + * - getLabel: Extracts 'label' property from datum + * - getValue: Extracts 'value' property from datum + */ +export function ChartTooltip< TDatum >( { + tooltipData, + dataFormat, + seriesStyles, + indicatorType, + getLabel = defaultGetLabel, + getValue = defaultGetValue, +}: ChartTooltipProps< TDatum > ) { + if ( ! tooltipData?.datumByKey ) { + return null; + } + + const datumEntries = Object.values( tooltipData.datumByKey ); + + if ( datumEntries.length === 0 ) { + return null; + } + + return ( + + { datumEntries.map( ( entry, index ) => { + if ( ! isChartDatumEntry< TDatum >( entry ) ) { + return null; + } + + const { stroke, ...lineShapeStyle } = + seriesStyles[ index ] || seriesStyles[ 0 ]; + const label = getLabel( entry.datum, index, entry.key ); + const value = getValue( entry.datum ); + + return ( + + ) : ( + + ) + } + label={ label } + value={ value } + dataFormat={ dataFormat } + /> + ); + } ) } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts new file mode 100644 index 000000000000..c3e6abcb721c --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts @@ -0,0 +1,11 @@ +export { + ChartTooltip, + type ChartTooltipProps, + type TooltipStyle, +} from './chart-tooltip'; +export { + PieChartTooltip, + type PieChartTooltipProps, +} from './pie-chart-tooltip'; +export { TooltipRow, type TooltipRowProps } from './tooltip-row'; +export { isChartDatumEntry, type ChartDatumEntry } from './utils'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx new file mode 100644 index 000000000000..7c39b24f40e0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import type { DataPointPercentage } from '@automattic/charts'; +import { RectShape } from '@automattic/charts/visx/legend'; + +/** + * Internal dependencies + */ +import type { DataFormat } from '../../types'; +import { TooltipRow } from './tooltip-row'; +import styles from './chart-tooltip.module.scss'; + +export type PieChartTooltipProps = { + /** + * Tooltip data from pie chart hover — a single DataPointPercentage. + */ + tooltipData: DataPointPercentage; + + /** + * Format configuration for the value display. + */ + dataFormat: DataFormat; +}; + +/** + * Tooltip component for pie and semi-circle charts. + * Renders a single row with a color indicator, label, and formatted value. + * + * Reuses the same SCSS module as ChartTooltip so styling (box-shadow, padding, + * the `:global(.visx-tooltip):has(.tooltip)` override) is shared. + */ +export function PieChartTooltip( { + tooltipData, + dataFormat, +}: PieChartTooltipProps ) { + return ( + + + } + label={ tooltipData.label } + value={ tooltipData.value } + dataFormat={ dataFormat } + /> + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx new file mode 100644 index 000000000000..526d49631833 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import type { DataFormat } from '../../types'; +import { MetricValue } from '../metric-value'; +import styles from './chart-tooltip.module.scss'; + +export type TooltipRowProps = { + /** Pre-rendered indicator element (LineShape, RectShape, etc.) */ + indicator: React.ReactNode; + /** Row label text */ + label: string; + /** Numeric value to format */ + value: number; + /** Format configuration */ + dataFormat: DataFormat; +}; + +export function TooltipRow( { + indicator, + label, + value, + dataFormat, +}: TooltipRowProps ) { + return ( + + { indicator } + +
{ label }
+ + +
+ ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts new file mode 100644 index 000000000000..7d7540e0c8f0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts @@ -0,0 +1,27 @@ +/** + * Generic chart datum entry type from visx tooltip data. + * Both line and bar charts use this structure. + */ +export type ChartDatumEntry< T = unknown > = { + datum: T; + index: number; + key: string; +}; + +/** + * Type guard to check if an entry is a valid chart datum entry. + * + * @param entry The entry to check. + * @return True if the entry has the expected structure. + */ +export const isChartDatumEntry = < T >( + entry: unknown +): entry is ChartDatumEntry< T > => { + return ( + typeof entry === 'object' && + entry !== null && + 'datum' in entry && + 'index' in entry && + 'key' in entry + ); +}; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts new file mode 100644 index 000000000000..ea68110f259e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts @@ -0,0 +1,31 @@ +export { MetricDelta } from './metric-delta'; +export { MetricValue } from './metric-value'; +export { MetricWithComparison } from './metric-with-comparison'; +export { + ComparativeLineChart, + type SeriesStyle, +} from './chart-comparative-line'; +export { Legend, type LegendItem } from './legend'; +export { WidgetRoot, useWidgetRootContext } from './widget-root'; + +export { SemiCircleChart } from './chart-semi-circle'; +export { DonutChart, type DonutChartData } from './chart-donut'; +export { ReportMetricWidget } from './report-metric'; +export { + LeaderboardChart, + type LeaderboardChartProps, + type LeaderboardChartData, + type LegendLabels, + LeaderboardLabel, + type LeaderboardLabelProps, +} from './chart-leaderboard'; +export { + BarChart, + type BarChartProps, + type BarChartData, + type BarChartStyle, +} from './chart-bar'; +export { + ChartEmptyState, + type ChartEmptyStateProps, +} from './chart-empty-state'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md new file mode 100644 index 000000000000..65426a7d3b80 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md @@ -0,0 +1,74 @@ +# Legend + +A pure component for rendering chart legends with optional comparison deltas. + +## Usage + +```tsx +import { Legend } from '@next-woo-analytics/widgets-toolkit'; + +const items = [ + { label: 'Mobile', value: 241950, displayValue: '$241.95K', color: '#3858E9' }, + { label: 'Desktop', value: 148130, displayValue: '$148.13K', color: '#66BDFF' }, + { label: 'Tablet', value: 44740, displayValue: '$44.74K', color: '#A77EFF' }, +]; + + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `items` | `LegendItem[]` | required | Array of legend items to display | +| `withComparison` | `boolean` | `false` | Show comparison deltas | + +### LegendItem + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `label` | `string` | yes | Item label text | +| `value` | `number` | yes | Current numeric value | +| `displayValue` | `string` | yes | Display-ready formatted value | +| `color` | `string` | no | Bullet color (hex, rgb, etc.) | +| `comparison` | `number` | no | Previous value for delta calculation | + +## With Comparison + +```tsx +const items = [ + { label: 'Mobile', value: 241950, displayValue: '$241.95K', color: '#3858E9', comparison: 200000 }, + { label: 'Desktop', value: 148130, displayValue: '$148.13K', color: '#66BDFF', comparison: 160000 }, +]; + + +``` + +## Theme Integration + +For widgets inside `GlobalChartsProvider`, use `LegendWithTheme` instead. It automatically resolves colors from the chart theme: + +```tsx +import { LegendWithTheme as Legend } from '@next-woo-analytics/widgets-toolkit'; + +// Colors are injected from theme - no need to specify them + ( + + ) } +/> +``` + +## Architecture + +``` +Legend (pure) +├── Receives items with colors already resolved +├── Renders Grid with LegendRow components +└── No context dependencies + +LegendWithTheme (wrapper) +├── Resolves colors: item.color → chartItems → theme +├── Passes items with colors to Legend +└── Requires GlobalChartsProvider +``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/index.ts new file mode 100644 index 000000000000..50fbad1f0beb --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/index.ts @@ -0,0 +1,2 @@ +export { type LegendItem } from './legend'; +export { LegendWithTheme as Legend } from './legend-with-theme'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx new file mode 100644 index 000000000000..6d5f55c9bf2f --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { + type BaseLegendItem, + useGlobalChartsContext, +} from '@automattic/charts'; + +/** + * Internal dependencies + */ +import { Legend, type LegendItem } from './legend'; + +type LegendWithThemeProps = { + chartItems?: BaseLegendItem[]; + items: LegendItem[]; + withComparison?: boolean; +}; + +/** + * Resolves the color for a legend item using the following priority: + * 1. item.color (explicit per-item) + * 2. chartItems color (matched by label) + * 3. theme color (from GlobalChartsProvider) + */ +function resolveItemColor( + item: LegendItem, + index: number, + chartItems: BaseLegendItem[] | undefined, + getElementStyles: ( opts: { index: number } ) => { color: string } +): string { + if ( item.color ) { + return item.color; + } + + const correspondingChartItem = chartItems?.find( + ( chartItem ) => chartItem.label === item.label + ); + + if ( correspondingChartItem?.color ) { + return correspondingChartItem.color; + } + + return getElementStyles( { index } ).color; +} + +/** + * Legend wrapper that injects theme colors from GlobalChartsProvider. + * Use this for widgets that render inside a GlobalChartsProvider context. + * + * For standalone usage, use the pure Legend component instead. + * + * @deprecated Prefer using the pure Legend component with explicit colors. + * This wrapper will be removed once all widgets are migrated. + */ +export function LegendWithTheme( { + chartItems, + items, + withComparison = false, +}: LegendWithThemeProps ) { + const { getElementStyles } = useGlobalChartsContext(); + + // Resolve all colors before passing to Legend + const itemsWithColors = items.map( ( item, index ) => ( { + ...item, + color: resolveItemColor( item, index, chartItems, getElementStyles ), + } ) ); + + return ( + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.module.scss new file mode 100644 index 000000000000..b0b708366c43 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.module.scss @@ -0,0 +1,28 @@ +.legend { + width: 100%; +} + +.labelContainer { + overflow: hidden; + min-width: 0; +} + +.bullet { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.value { + text-align: right; + font-weight: 600; + white-space: nowrap; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx new file mode 100644 index 000000000000..fe3e96b9a8eb --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import { __experimentalGrid as Grid } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { LegendRow } from './row'; +import { MetricDelta } from '../metric-delta'; +import styles from './legend.module.scss'; + +export type LegendItem = { + label: string; + value: number; + displayValue: string; + /** + * Color for the legend item bullet. + */ + color?: string; + comparison?: number; +}; + +type LegendProps = { + items: LegendItem[]; + /** + * Show comparison deltas. + * @default false + */ + withComparison?: boolean; + /** + * Hide the displayValue column. + * Useful when only showing labels and comparison deltas. + * @default false + */ + hideValue?: boolean; +}; + +/** + * Pure Legend component that renders a grid of legend items. + * Does not depend on any context provider - all data flows through props. + * + * For widgets using GlobalChartsProvider, use LegendWithTheme instead. + */ +/** + * Determines the number of grid columns based on visibility options. + */ +function getTemplateColumns( + hideValue: boolean, + withComparison: boolean +): string { + if ( hideValue ) { + return withComparison ? '1fr auto' : '1fr'; + } + return withComparison ? '1fr auto auto' : '1fr auto'; +} + +export function Legend( { + items, + withComparison = false, + hideValue = false, +}: LegendProps ) { + return ( + + { items.map( ( item ) => ( + + ) : null + } + color={ item.color } + title={ item.label } + > + { item.label } + + ) ) } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/index.ts new file mode 100644 index 000000000000..f1a30908b036 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/index.ts @@ -0,0 +1,2 @@ +export { LegendRow } from './legend-row'; +export type { LegendRowProps } from './legend-row'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx new file mode 100644 index 000000000000..c35f9a6bff4c --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import type { ReactNode } from 'react'; + +/** + * Internal dependencies + */ +import styles from '../legend.module.scss'; + +export type LegendRowProps = { + /** + * The label content (usually text) + */ + children: ReactNode; + + /** + * Formatted value to display. + * When false, the value column is not rendered. + */ + value: string | false; + + /** + * Comparison display (can be MetricDelta component) + */ + comparison?: ReactNode; + + /** + * Color for the bullet indicator + */ + color?: string; + + /** + * Title for the label (shown on hover, useful when text is truncated) + */ + title?: string; +}; + +export function LegendRow( { + children, + value, + comparison, + color, + title, +}: LegendRowProps ) { + return ( + <> + +
+ + { children } + + + { value !== false && ( + { value } + ) } + { comparison } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/index.ts new file mode 100644 index 000000000000..32a713c5bf4e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/index.ts @@ -0,0 +1,2 @@ +export { MetricDelta } from './metric-delta'; +export type { MetricDeltaProps } from './metric-delta'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss new file mode 100644 index 000000000000..304a53577c06 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss @@ -0,0 +1,18 @@ +.delta { + font-size: var(--wpds-font-size-md); + font-weight: 400; + line-height: var(--wpds-font-size-lg); + + &.invalid, + &.neutral { + color: var(--wpds-color-fg-content-neutral-weak); + } + + &.positive { + color: var(--wpds-color-stroke-surface-success-strong); + } + + &.negative { + color: var(--wpds-color-stroke-surface-error-strong); + } +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx new file mode 100644 index 000000000000..e5565650d4bb --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx @@ -0,0 +1,156 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ComponentProps } from 'react'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import styles from './metric-delta.module.scss'; + +export type MetricDeltaProps = { + /** + * The current/new value + */ + current: number; + + /** + * The previous/comparison value + */ + previous: number; + + /** + * What to display when calculation is not possible + * @default '—' + */ + fallback?: string; + + /** + * Whether to hide when delta is zero + * @default false + */ + hideZero?: boolean; + + /** + * For metrics where decrease is improvement (e.g., bounce rate, returns) + * @default false + */ + invertColors?: boolean; + + /** + * CSS class for styling + */ + className?: string; + + /** + * Text alignment + * @default 'center' + */ + justify?: ComponentProps< typeof Stack >[ 'justify' ]; + + /** + * Show absolute change instead of percentage + * @default false + */ + showAbsolute?: boolean; + + /** + * Format for absolute values + * @default 'number' + */ + absoluteFormat?: 'number' | 'currency'; +}; + +function calculatePercentageChange( + current: number, + previous: number +): number | null { + // Handle invalid inputs + if ( ! Number.isFinite( current ) || ! Number.isFinite( previous ) ) { + return null; + } + + // Handle zero previous value + if ( previous === 0 ) { + return current === 0 ? 0 : null; + } + + // Calculate percentage change, rounded to integer + return Math.round( + ( ( current - previous ) / Math.abs( previous ) ) * 100 + ); +} + +export function MetricDelta( { + current, + previous, + fallback = '—', + hideZero = false, + invertColors = false, + className, + justify = 'center', + showAbsolute = false, + absoluteFormat = 'number', +}: MetricDeltaProps ) { + // Calculate the change + const absoluteChange = current - previous; + const percentageChange = calculatePercentageChange( current, previous ); + + // Handle edge cases + if ( percentageChange === null ) { + return ( + + { fallback } + + ); + } + + if ( hideZero && percentageChange === 0 ) { + return null; + } + + // Determine display value + let displayValue: string; + if ( showAbsolute ) { + displayValue = formatMetricValue( absoluteChange, absoluteFormat ); + if ( absoluteChange > 0 ) { + displayValue = `+${ displayValue }`; + } + } else { + displayValue = formatMetricValue( + percentageChange / 100, + 'percentage' + ); + } + + // Determine color based on direction and inversion + const isPositive = + ( percentageChange > 0 && ! invertColors ) || + ( percentageChange < 0 && invertColors ); + const isNegative = + ( percentageChange < 0 && ! invertColors ) || + ( percentageChange > 0 && invertColors ); + + return ( + + { displayValue } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/index.ts new file mode 100644 index 000000000000..1c84a4c29630 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/index.ts @@ -0,0 +1,2 @@ +export { MetricValue } from './metric-value'; +export type { MetricValueProps } from './metric-value'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss new file mode 100644 index 000000000000..94e9daf138a8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss @@ -0,0 +1,18 @@ +.metricValue { + font-weight: 500; + font-size: var( --wp-ui-metric-font-size ); + line-height: var( --wpds-font-line-height-sm ); + + // Color variants + &.color--neutral { + color: var(--wpds-color-fg-content-neutral); + } + + &.color--positive { + color: var(--wpds-color-stroke-surface-success-strong); + } + + &.color--negative { + color: var(--wpds-color-stroke-surface-error-strong); + } +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx new file mode 100644 index 000000000000..edb3a6f2f3ab --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx @@ -0,0 +1,88 @@ +/** + * External dependencies + */ +import { type CSSProperties, useMemo } from 'react'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { FontSize } from '@wordpress/theme'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import styles from './metric-value.module.scss'; +import type { DataFormat } from '../../types'; + +export type MetricValueProps = { + /** + * The numeric value to display + */ + value: number; + + /** + * Format configuration for value display + * @default { type: 'number' } + */ + dataFormat?: DataFormat; + + /** + * ISO 4217 currency code (e.g. `'USD'`, `'EUR'`). + */ + currencyCode?: string; + + /** + * CSS class for styling + */ + className?: string; + + /** + * Font size token from the WordPress Design System. + * Maps directly to `--wpds-font-size-{value}`. + * @default 'lg' + */ + fontSize?: FontSize; + + /** + * Color variant + * @default 'neutral' + */ + color?: 'neutral' | 'positive' | 'negative'; +}; + +export function MetricValue( { + value, + dataFormat = { type: 'number' }, + currencyCode, + className, + fontSize = 'lg', + color = 'neutral', +}: MetricValueProps ) { + /** + * Create display value using dataFormat configuration + */ + const displayValue = useMemo( + () => + formatMetricValue( value, dataFormat.type, { + ...dataFormat.options, + currencyCode, + } ), + [ value, dataFormat, currencyCode ] + ); + + const style = { + '--wp-ui-metric-font-size': + `var( --wpds-font-size-${ fontSize } )`, + } as CSSProperties; + + return ( + + { displayValue } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/index.ts new file mode 100644 index 000000000000..037dddb1fff6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/index.ts @@ -0,0 +1 @@ +export { MetricWithComparison } from './metric-with-comparison'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx new file mode 100644 index 000000000000..f6772f110c4a --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx @@ -0,0 +1,126 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import { ComponentProps } from 'react'; + +/** + * Internal dependencies + */ +import { MetricValue } from '../metric-value'; +import { MetricDelta } from '../metric-delta'; +import type { MetricValueProps } from '../metric-value'; +import type { DataFormat } from '../../types'; + +export type MetricWithComparisonProps = { + /** + * The current value to display + */ + value: number; + + /** + * The previous value for comparison. If null/undefined, delta won't be shown. + */ + previousValue?: number | null; + + /** + * Format configuration for value and delta display + * @default { type: 'number' } + */ + dataFormat?: DataFormat; + + /** + * Layout direction + * @default 'row' + */ + direction?: ComponentProps< typeof Stack >[ 'direction' ]; + + /** + * Alignment of items + * @default 'flex-end' + */ + align?: ComponentProps< typeof Stack >[ 'align' ]; + + /** + * Font size token for the primary value + * @default 'xl' + */ + fontSize?: MetricValueProps[ 'fontSize' ]; + + /** + * For metrics where decrease is improvement (e.g., bounce rate) + * @default false + */ + invertDeltaColors?: boolean; + + /** + * Hide delta when it's zero + * @default false + */ + hideDeltaOnZero?: boolean; + + /** + * CSS class for the container + */ + className?: string; + + /** + * What to display for delta when calculation is not possible + */ + deltaFallback?: string; + + /** + * Show absolute change instead of percentage in delta + * @default false + */ + showAbsoluteDelta?: boolean; +}; + +export function MetricWithComparison( { + value, + previousValue, + dataFormat = { type: 'number' }, + direction = 'row', + align = 'baseline', + fontSize = 'xl', + invertDeltaColors = false, + hideDeltaOnZero = false, + className, + deltaFallback, + showAbsoluteDelta = false, +}: MetricWithComparisonProps ) { + const showDelta = previousValue !== null && previousValue !== undefined; + + /** + * Determine absolute format for delta based on data type + */ + const absoluteFormat = + dataFormat.type === 'currency' ? 'currency' : 'number'; + + return ( + + + + { showDelta && ( + + ) } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/index.ts new file mode 100644 index 000000000000..679ee591c93f --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/index.ts @@ -0,0 +1,2 @@ +export { ReportMetricWidget } from './report-metric'; +export type { ReportMetricWidgetProps } from './report-metric'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx new file mode 100644 index 000000000000..320db90f4889 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx @@ -0,0 +1,146 @@ +/** + * External dependencies + */ +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useGlobalChartsContext } from '@automattic/charts'; +import { useMemo } from 'react'; + +/** + * Internal dependencies + */ +import { MetricComparisonWidget } from '../../widgets/metric-comparison'; +import { buildTimeSeriesChartData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import type { DataFormat } from '../../types'; + +/** + * Generic type for report data with time series + */ +type ReportData = { + summary: { + date_start: string; + date_end: string; + [ key: string ]: string | number; + }; + data: Array< { + date_start: string; + [ key: string ]: string | number; + } >; +}; + +/** + * Type for the data prop - the result from useReport hooks + */ +type ReportHookResult = { + primary: { data?: ReportData }; + comparison: { data?: ReportData }; + isLoading: boolean; + isFetching: boolean; + hasData: boolean; + isError: boolean; + error: Error | null | undefined; + refetch: () => void; +}; + +export type ReportMetricWidgetProps = { + /** + * The metric key to display from the data + */ + metricKey: string; + + /** + * The report data from useReport hooks (e.g., useReportOrders, useReportVisitors) + */ + data: ReportHookResult; + + /** + * The format configuration for the metric + */ + dataFormat: DataFormat; +}; + +/** + * Report Metric Widget - Internal Component + * + * @param {ReportMetricWidgetProps} props - The component props + * + * @internal + */ +export function ReportMetricWidget( { + metricKey, + data, + dataFormat, +}: ReportMetricWidgetProps ) { + const { getElementStyles } = useGlobalChartsContext(); + + const primaryData = data.primary.data; + const comparisonData = data.comparison.data; + const { isLoading, isFetching, hasData, isError, error, refetch } = data; + + // Compute unified loading states (same logic as useWidgetLoading in dashboard v1) + const isInitialLoading = isLoading && ! hasData; + const isRefetching = ( isLoading || isFetching ) && hasData; + + // Build series[] data. + const series = buildTimeSeriesChartData( { + primary: primaryData ?? { + summary: { + date_start: '', + date_end: '', + [ metricKey ]: 0, + }, + data: [], + }, + comparison: comparisonData, + metricKey, + emptyDataFallback: 'empty-array', + } ); + + // Build seriesStyles[] data. + const seriesStyles = useMemo( + () => + series.map( ( seriesData, index ) => { + const { color, lineStyles } = getElementStyles( { + data: seriesData, + index, + } ); + + return { + stroke: color, + ...lineStyles, + }; + } ), + [ series, getElementStyles ] + ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; // Dashboard shows error UI via WidgetErrorBoundary + } + + // No data and not loading = nothing to show + if ( ! primaryData && ! isInitialLoading ) { + return null; + } + + // metricKey always refers to a numeric metric field (e.g., "visitors", "orders_no"), + // never to date fields (e.g., "date_start"). The summary type includes both for flexibility, + // but we know the actual value will be a number at runtime. + const primaryValue = ( primaryData?.summary[ metricKey ] as number ) ?? 0; + const comparisonValue = comparisonData?.summary[ metricKey ] as + | number + | undefined; + + return ( + <> + + { ( isInitialLoading || isRefetching ) && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md new file mode 100644 index 000000000000..56680cb42618 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md @@ -0,0 +1,152 @@ +# WidgetRoot + +A wrapper component that encapsulates all infrastructure a lazy-loaded dashboard widget needs. + +## Problem + +Dashboard widgets are ES Modules loaded asynchronously via lazy-load. This means they don't share context with other widgets—providers **must** be instantiated per widget. + +`WidgetRoot` centralizes this "bootstrap" logic instead of scattering it across multiple widget files. + +## What WidgetRoot Provides + +- **AnalyticsQueryClientProvider** - React Query client for data fetching +- **GlobalChartsProvider** - Chart theming via `useChartTheme()` +- **Report params resolution** - From widget attributes or URL fallback +- **Context provider** - Child widgets access resolved params via `useWidgetRootContext()` + +## Usage + +### In dashboard-widgets (consumer) + +```tsx +// dashboard-widgets/my-widget/render.tsx +import { WidgetRoot, MyWidget } from '@next-woo-analytics/widgets-toolkit'; + +export default function MyWidgetRender( { attributes } ) { + return ( + + + + ); +} +``` + +### In widgets-toolkit (internal widget) + +```tsx +// widgets-toolkit/widgets/my-widget/widget-my-widget.tsx +import { useWidgetRootContext } from '../../components/widget-root'; + +export function MyWidget() { + const { reportParams } = useWidgetRootContext(); + + // Use reportParams for data fetching + const { data } = useReportOrders( reportParams ); + + return
{ /* render widget */ }
; +} +``` + +## API + +### WidgetRoot Props + +| Prop | Type | Description | +|------|------|-------------| +| `attributes` | `Partial` | Widget attributes, may include `reportParams` | +| `children` | `ReactNode` | Child components (widgets) | +| `options.from` | `string` | Router path for URL params (default: `/wc-analytics/dashboard`) | + +### useWidgetRootContext + +Returns the resolved context value: + +```typescript +type WidgetRootContextValue = { + reportParams: ReportParams; +}; +``` + +**Important**: Must be called within a `WidgetRoot` component. Throws an error otherwise. + +## Report Params Resolution + +`WidgetRoot` resolves `reportParams` with the following priority: + +1. **From attributes** - If `attributes.reportParams` is provided and non-empty +2. **From URL** - Falls back to URL search params via `@wordpress/route` + +This allows widgets to work both: +- In the Analytics dashboard (params from URL) +- Other contexts (params from attributes) + +## Architecture + +``` +WidgetRoot +├── AnalyticsQueryClientProvider (shared React Query client) +│ └── GlobalChartsProvider (chart theme) +│ └── WidgetRootContext.Provider (reportParams) +│ └── children (widget components) +``` + +## Responsive Widgets with Container Queries + +`WidgetRoot` wraps children in a container query context, enabling widgets to adapt their layout based on their own size (not viewport). + +### Why Container Queries? + +Dashboard widgets live in a resizable grid. Users can change tile sizes, so widgets must adapt to their container—not the viewport. CSS Container Queries solve this. + +### Available Breakpoints + +Aligned with [Tailwind container query defaults](https://tailwindcss.com/docs/responsive-design#container-size-reference) and [ARC-464](https://linear.app/a8c/issue/ARC-464). + +| Token | Size | Use Case | +|-------|------|----------| +| `xxs` | 256px (16rem) | Extra extra small tiles | +| `xs` | 320px (20rem) | Extra small tiles | +| `sm` | 384px (24rem) | Small tiles | +| `md` | 448px (28rem) | Standard tile size | +| `lg` | 512px (32rem) | Large tiles | +| `xl` | 576px (36rem) | Extra large tiles | +| `2xl` | 672px (42rem) | Full-width widgets | + +### Usage in Widget SCSS + +```scss +@use '../../styles/widget-container' as *; + +.myWidget { + // Mobile-first: vertical layout for small containers + flex-direction: column; + + // >= 448px: switch to horizontal layout + @include widget-query( md ) { + flex-direction: row; + } + + // >= 576px: add more spacing + @include widget-query( xl ) { + gap: var( --wpds-dimension-base ); + } +} +``` + +### How It Works + +1. `WidgetRoot` wraps children in a div with `container-type: inline-size` +2. Child widgets use `@container` queries via the `widget-query()` mixin +3. Styles apply based on the widget's actual width, not the viewport + +### Files + +- `../../styles/_widget-container.scss` - Breakpoints and mixin definitions + +## Files + +- `widget-root.tsx` - Main component +- `widget-root.module.scss` - Container query setup +- `context.tsx` - React context and `useWidgetRootContext` hook +- `index.ts` - Public exports diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx new file mode 100644 index 000000000000..1dc41a858801 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { createContext, useContext } from 'react'; +import type { ReportParams } from '@next-woo-analytics/data'; +import type { WidgetErrorConfig } from '@automattic/dashboard'; + +export type WidgetRootContextValue = { + /** + * Normalized report parameters resolved from widget attributes or URL. + */ + reportParams: ReportParams; + + /** + * Function to report an error state in the widget. + * Pass `true` for default error, a config object for custom error, or `null` to clear. + * + * @example + * ```tsx + * // Show error with retry action + * setError( { + * message: 'Failed to load data', + * action: { label: 'Retry', onClick: handleRetry } + * } ); + * + * // Clear error state + * setError( null ); + * ``` + */ + setError?: ( error: WidgetErrorConfig | true | null ) => void; +}; + +const WidgetRootContext = createContext< WidgetRootContextValue | null >( + null +); + +/** + * Hook to access the WidgetRoot context. + * + * Must be used within a WidgetRoot component. + * + * @throws {Error} If used outside of WidgetRoot + * @return {WidgetRootContextValue} The widget root context value + * + * @example + * ```tsx + * function MyWidget() { + * const { reportParams } = useWidgetRootContext(); + * // Use reportParams for data fetching + * } + * ``` + */ +export function useWidgetRootContext(): WidgetRootContextValue { + const context = useContext( WidgetRootContext ); + + if ( ! context ) { + throw new Error( + 'useWidgetRootContext must be used within a WidgetRoot component' + ); + } + + return context; +} + +export { WidgetRootContext }; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/index.ts new file mode 100644 index 000000000000..71308d9f23f4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/index.ts @@ -0,0 +1,3 @@ +export { WidgetRoot } from './widget-root'; +export { useWidgetRootContext } from './context'; +export type { WidgetRootContextValue } from './context'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss new file mode 100644 index 000000000000..e241cb72599e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss @@ -0,0 +1,5 @@ +@use '../../styles/widget-container' as *; + +.root { + @extend .widgetContainer; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx new file mode 100644 index 000000000000..ab1a95f1be13 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx @@ -0,0 +1,139 @@ +/** + * External dependencies + */ +import { + AnalyticsQueryClientProvider, + getDefaultPreset, + normalizeReportParams, +} from '@next-woo-analytics/data'; +import { getStoreInfo } from '@woocommerce-next/data'; +import { GlobalChartsProvider } from '@automattic/charts'; +import { useSearch } from '@wordpress/route'; +import { useMemo, type ReactNode } from 'react'; +import type { WidgetErrorConfig } from '@automattic/dashboard'; +import '@automattic/charts/style.css'; + +/** + * Internal dependencies + */ +import { useChartTheme } from '../../hooks'; +import { WidgetRootContext } from './context'; +import type { ReportParamsFieldAttributes } from '../../fields'; +import styles from './widget-root.module.scss'; + +type WidgetRootProps = { + /** + * The attributes for the widget. + */ + attributes?: Partial< ReportParamsFieldAttributes >; + + /** + * The children of the widget root. + */ + children: ReactNode; + + /** + * Function to report an error state in the widget. + * Passed from the dashboard's WidgetRenderProps. + */ + setError?: ( error: WidgetErrorConfig | true | null ) => void; + + /** + * The options for the widget root. + */ + options?: { + /** + * The source of the search params. + * @default '/wc-analytics/dashboard' + */ + from?: string; + }; +}; + +const DEFAULT_SEARCH_FROM = '/wc-analytics/dashboard'; + +/** + * Hook that resolves widget attributes: + * - `reportParams`: with URL search params when it's not provided + */ +function useResolveReportParams( + attributes?: Partial< ReportParamsFieldAttributes >, + from?: string +) { + let search: Record< string, unknown > = {}; + + try { + // eslint-disable-next-line react-hooks/rules-of-hooks -- useSearch may throw outside a matched route + search = useSearch( { from: from ?? DEFAULT_SEARCH_FROM } ); + } catch { + // Do nothing + } + + /* + * Check if reportParams exists and is not empty. + * If it exists, use the provided reportParams. + * Otherwise, use URL search params as reportParams. + */ + const hasReportParams = + !! attributes?.reportParams && + Object.keys( attributes.reportParams ).length > 0; + + return hasReportParams ? attributes.reportParams : search; +} + +/** + * WidgetRoot + * + * A wrapper component that encapsulates all the infrastructure a lazy-loaded + * dashboard widget needs: + * - AnalyticsQueryClientProvider for data fetching + * - GlobalChartsProvider with chart theme + * - Report params resolution (from attributes or URL fallback) + * - Context provider for child widgets to access resolved params + * + * @example + * ```tsx + * // In dashboard-widgets/my-widget/render.tsx + * + * + * + * + * // In widgets-toolkit/widgets/my-widget.tsx + * function MyWidget() { + * const { reportParams } = useWidgetRootContext(); + * // Use reportParams for data fetching + * } + * ``` + */ +export function WidgetRoot( { + attributes, + children, + setError, + options, +}: WidgetRootProps ) { + const chartTheme = useChartTheme(); + const rawReportParams = useResolveReportParams( attributes, options?.from ); + + const { launchedDate } = getStoreInfo(); + const defaultPreset = getDefaultPreset( launchedDate ); + + const reportParams = useMemo( + () => normalizeReportParams( rawReportParams, defaultPreset ), + [ rawReportParams, defaultPreset ] + ); + + const contextValue = useMemo( + () => ( { reportParams, setError } ), + [ reportParams, setError ] + ); + + return ( + + + +
{ children }
+
+
+
+ ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/chart.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/chart.ts new file mode 100644 index 000000000000..b08d6832b0af --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/chart.ts @@ -0,0 +1,4 @@ +/** + * Override the @automattic/charts default (300ms) for snappier resize response. + */ +export const RESIZE_DEBOUNCE_MS = 50; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts new file mode 100644 index 000000000000..4d72d523f4e6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts @@ -0,0 +1,21 @@ +// Base colors +const COLOR_BLUEBERRY = '#3858E9'; + +const COLOR_PURPLE_30 = '#A77EFF'; + +const COLOR_BLUE_30 = '#66BDFF'; + +export const COLOR_GRAY_100 = '#F0F0F0'; + +// Semantic colors +const COLOR_PRIMARY = COLOR_BLUEBERRY; +const COLOR_SECONDARY = COLOR_BLUE_30; + +// Theme +export const WOO_COLORS = [ + COLOR_PRIMARY, + COLOR_SECONDARY, + COLOR_PURPLE_30, + '#7B90FF', + '#EB6594', +]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/index.ts new file mode 100644 index 000000000000..e3293cad3ba2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/index.ts @@ -0,0 +1,2 @@ +export * from './chart'; +export * from './color-palette'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md new file mode 100644 index 000000000000..2844306aab83 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md @@ -0,0 +1,34 @@ +# ReportParamsField + +Form control for editing a widget's date-range parameters +(preset, from/to, comparison range). + +## Data coupling + +This field depends on two external data providers: + +| Provider | Package | Purpose | +|----------|---------|---------| +| `getStoreInfo()` | `@woocommerce-next/data` | Reads `launchedDate` from the store profile | +| `getDefaultPreset()` | `@next-woo-analytics/data` | Resolves a smart date-range preset based on store age | + +### Why the coupling exists + +The field renders inside a `@wordpress/components` Modal, which is a +**sibling** of the widget render tree — not a child. That means it has +no access to `WidgetRootContext` or any provider that lives inside +`WidgetRoot`. + +Without this coupling, fresh widgets (no saved `reportParams`) would +always fall back to `last-30-days`, even when the widget itself uses a +dynamic preset like `today` or `last-7-days`. The settings modal would +show dates that don't match the widget's actual data range. + +### Alternatives considered + +| Approach | Why we didn't use it | +|----------|---------------------| +| WidgetRoot context | Modal renders outside the widget tree — context not accessible | +| Prop via attribute config | `@ciab/dataviews` `DataFormControlProps` doesn't support extra props | +| Global/singleton | Adds indirection for a problem scoped to one component | +| Attribute initialization | Side-effect on render, risk of re-render loops | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx new file mode 100644 index 000000000000..cccecbdddf32 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx @@ -0,0 +1,170 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import { DateFiltersPanel } from '@next-woo-analytics/components'; +import { + getDefaultPreset, + normalizeReportParams, + localTZDate, + getSiteTimezone, +} from '@next-woo-analytics/data'; +import { getStoreInfo } from '@woocommerce-next/data'; +import { endOfDay } from 'date-fns'; +import { + deriveComparisonRange, + encodeDateToSearchParam, +} from '@next-woo-analytics/routing'; +import { useCallback, useMemo, useState, useEffect } from 'react'; +import type { DataFormControlProps } from '@ciab/dataviews'; +import { + type ComparisonPresetId, + isPrimaryPreset, + type DateRange, +} from '@next-woo-analytics/datetime'; + +/** + * Inferred types + */ +type ReportParams = NonNullable< + Parameters< typeof normalizeReportParams >[ 0 ] +>; + +export type ReportParamsFieldAttributes = { + reportParams: ReportParams; +}; + +export function ReportParamsField( { + data: attributes, + onChange, +}: DataFormControlProps< ReportParamsFieldAttributes > ) { + const [ stagedReportParams, setStagedReportParams ] = + useState< ReportParams >( attributes?.reportParams ); + + const { launchedDate } = getStoreInfo(); + const defaultPreset = getDefaultPreset( launchedDate ); + + const reportParams = normalizeReportParams( + stagedReportParams, + defaultPreset + ); + + const range = { + from: localTZDate( reportParams.from ), + to: localTZDate( reportParams.to ), + }; + + const stageDateRange = useCallback( + ( nextRange?: DateRange, nextPresetId?: string ) => { + const nextReportParams = { ...stagedReportParams }; + + if ( nextRange?.from && nextRange?.to ) { + nextReportParams.from = encodeDateToSearchParam( + nextRange.from + ); + nextReportParams.to = encodeDateToSearchParam( + endOfDay( nextRange.to ) + ); + } + + if ( nextPresetId && isPrimaryPreset( nextPresetId ) ) { + nextReportParams.preset = nextPresetId; + } else if ( nextPresetId ) { + delete nextReportParams.preset; + } + + /* + * Derive comparison range from primary range and preset, + * when comparison is enabled. + */ + if ( reportParams.comp === '1' ) { + const derived = deriveComparisonRange( nextReportParams ); + if ( derived ) { + nextReportParams.compare_from = derived.compare_from; + nextReportParams.compare_to = derived.compare_to; + } + } + + setStagedReportParams( nextReportParams ); + }, + [ stagedReportParams, reportParams.comp ] + ); + + // Basic check if the date range has been changed. + const isDateRangeDirty = useMemo( () => { + return ( + attributes?.reportParams?.from !== stagedReportParams?.from || + attributes?.reportParams?.to !== stagedReportParams?.to || + attributes?.reportParams?.preset !== stagedReportParams?.preset + ); + }, [ + attributes?.reportParams?.from, + attributes?.reportParams?.to, + attributes?.reportParams?.preset, + stagedReportParams?.from, + stagedReportParams?.to, + stagedReportParams?.preset, + ] ); + + const commitComparisonRange = useCallback( + ( + nextComparisonRange?: DateRange, + nextComparisonPresetId?: ComparisonPresetId + ) => { + onChange( { + reportParams: { + ...reportParams, + compare_from: encodeDateToSearchParam( + nextComparisonRange?.from + ), + compare_to: encodeDateToSearchParam( + nextComparisonRange?.to + ), + compare_preset: nextComparisonPresetId, + comp: '1' as const, + }, + } ); + }, + [ onChange, reportParams ] + ); + + const commit = useCallback( () => { + onChange( { reportParams: stagedReportParams } ); + }, [ onChange, stagedReportParams ] ); + + const clear = useCallback( () => { + setStagedReportParams( attributes?.reportParams ); + }, [ setStagedReportParams, attributes ] ); + + /* + * Get the dashboard layout surface for responsive calculations. + * This is a temporary workaround until @automattic/dashboard exposes + * a Context provider. See WOOA7S-1008 for the upstream solution. + */ + const [ containerElement, setContainerElement ] = + useState< HTMLElement | null >( null ); + + useEffect( () => { + const node = document.querySelector< HTMLElement >( + '.next-admin-layout__surface' + ); + setContainerElement( node ); + }, [] ); + + return ( + + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts new file mode 100644 index 000000000000..18be99c32fc4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts @@ -0,0 +1,4 @@ +export { + ReportParamsField, + type ReportParamsFieldAttributes, +} from './date-report-params-field'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts new file mode 100644 index 000000000000..46abc93ff479 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts @@ -0,0 +1,8 @@ +/** + * Widget edit fields + */ +export { + ReportParamsField, + type ReportParamsFieldAttributes, +} from './date-report-params-field'; +export { MetricsField, DEFAULT_METRICS } from './metrics-field'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/index.ts new file mode 100644 index 000000000000..9b3b326c12fe --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/index.ts @@ -0,0 +1,2 @@ +export { MetricsField } from './metrics-field'; +export { DEFAULT_METRICS, type Metric } from './metrics'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx new file mode 100644 index 000000000000..9c001c772c18 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx @@ -0,0 +1,71 @@ +/** + * External dependencies + */ +import { Fieldset, Stack } from '@wordpress/ui'; +import { CheckboxControl } from '@wordpress/components'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { useCallback, useEffect } from 'react'; +import type { DataFormControlProps } from '@ciab/dataviews'; + +/** + * Internal dependencies + */ +import { DEFAULT_METRICS, type Metric } from './metrics'; + +type MetricsAttributes = { + metrics: Metric[]; +}; + +export function MetricsField( { + data: attributes, + onChange, +}: DataFormControlProps< MetricsAttributes > ) { + // Store the metrics in the attributes. + useEffect( () => { + if ( attributes?.metrics?.length ) { + return; + } + + onChange( { metrics: DEFAULT_METRICS } ); + }, [ onChange, attributes ] ); + + const updateMetrics = useCallback( + ( id: string ) => + onChange( { + metrics: attributes.metrics.map( ( m ) => { + return m.id === id ? { ...m, enabled: ! m.enabled } : m; + } ), + } ), + [ onChange, attributes ] + ); + + const help = sprintf( + /* translators: %d: number of metrics */ + _n( + 'Choose up to %d metric', + 'Choose up to %d metrics', + attributes.metrics?.length ?? 1, + 'woocommerce-analytics' + ), + attributes.metrics?.length ?? 1 + ); + + return ( + + + { __( 'Metrics', 'woocommerce-analytics' ) } + + { help } + + { attributes?.metrics?.map( ( metric ) => ( + updateMetrics( metric.id ) } + /> + ) ) } + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts new file mode 100644 index 000000000000..f7370d115782 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { FilterCondition } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { MetricKey } from '../../types'; + +export type Metric = { + id: string; + label: string; + description?: string; + category?: 'Finances' | 'Orders' | 'Sales' | 'Inventory'; + metricType: + | 'general' + | 'product' + | 'booking' + | 'visitors' + | 'conversion' + | 'customers'; + metricKey: MetricKey; + filters?: FilterCondition[]; + enabled: boolean; +}; + +const METRIC_NET_SALES: Metric = { + id: 'general-orders_value_net', + label: __( 'Net sales', 'woocommerce-analytics' ), + description: __( + 'Monitor your total revenue — after any discounts, returns, or adjustments — over a set period of time.', + 'woocommerce-analytics' + ), + category: 'Finances', + metricType: 'general', + metricKey: 'orders_value_net', + enabled: true, +}; + +const METRIC_ORDERS: Metric = { + id: 'general-orders_no', + label: __( 'Orders', 'woocommerce-analytics' ), + description: __( + 'See a breakdown of when orders are placed to identify peak selling periods.', + 'woocommerce-analytics' + ), + category: 'Orders', + metricType: 'general', + metricKey: 'orders_no', + enabled: true, +}; + +const METRIC_BOOKINGS: Metric = { + id: 'booking-orders_no', + label: __( 'Bookings', 'woocommerce-analytics' ), + description: __( + 'See a breakdown of when bookings are placed to identify peak selling periods.', + 'woocommerce-analytics' + ), + category: 'Orders', + metricKey: 'orders_no', + metricType: 'booking', + filters: [ + { + compare: 'IN', + key: 'product_type', + value: [ 'booking', 'bookable-event', 'bookable-service' ], + }, + ], + enabled: true, +}; + +const METRIC_VISITORS: Metric = { + id: 'visitors-visitors', + label: __( 'Visitors', 'woocommerce-analytics' ), + description: __( + 'Track website visitor trends and monitor traffic patterns over time.', + 'woocommerce-analytics' + ), + category: 'Orders', + metricType: 'visitors', + metricKey: 'visitors', + enabled: true, +}; + +const METRIC_CONVERSION_RATE: Metric = { + id: 'conversion-conversion_rate', + label: __( 'Store conversion rate', 'woocommerce-analytics' ), + description: __( + "Track your store's conversion funnel from sessions to completed orders.", + 'woocommerce-analytics' + ), + category: 'Sales', + metricType: 'conversion', + metricKey: 'conversion_rate', + enabled: true, +}; + +const METRIC_CUSTOMERS: Metric = { + id: 'customers-customers', + label: __( 'Customers', 'woocommerce-analytics' ), + description: __( + 'Track the total number of customers (new and returning) who placed orders during the selected time period.', + 'woocommerce-analytics' + ), + category: 'Orders', + metricType: 'customers', + metricKey: 'customers', + enabled: true, +}; + +export const DEFAULT_METRICS = [ + METRIC_NET_SALES, + METRIC_ORDERS, + METRIC_BOOKINGS, + METRIC_VISITORS, + METRIC_CONVERSION_RATE, + METRIC_CUSTOMERS, +]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts new file mode 100644 index 000000000000..024fa02121da --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts @@ -0,0 +1,165 @@ +/** + * Mock formatMetricValue to avoid pulling in heavy transitive deps. + */ +jest.mock( '@next-woo-analytics/formatters', () => ( { + formatMetricValue: ( value: number ) => `$${ value }`, +} ) ); + +/** + * Internal dependencies + */ +import { buildCouponUseData } from '../build-coupon-use-data'; + +type CouponsByDateSummary = { + total_orders: number; + orders_with_coupon: number; + orders_without_coupon: number; + total_sales: number; + sales_with_coupon: number; + sales_without_coupon: number; + total_discount_amount: number; + net_sales_after_discount: number; + coupon_usage_percentage: number; +}; + +function makeCouponsByDateData( summary: Partial< CouponsByDateSummary > ) { + return { + data: [], + summary: { + date_start: '2024-01-01', + date_end: '2024-01-31', + total_orders: 0, + orders_with_coupon: 0, + orders_without_coupon: 0, + total_sales: 0, + sales_with_coupon: 0, + sales_without_coupon: 0, + total_discount_amount: 0, + net_sales_after_discount: 0, + coupon_usage_percentage: 0, + ...summary, + }, + }; +} + +describe( 'buildCouponUseData', () => { + it( 'returns empty state when coupons is undefined', () => { + const result = buildCouponUseData( undefined, undefined ); + + expect( result ).toEqual( { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + } ); + } ); + + it( 'returns empty state when coupons is null', () => { + const result = buildCouponUseData( null, null ); + + expect( result ).toEqual( { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + } ); + } ); + + it( 'returns empty state when total sales is zero', () => { + const coupons = makeCouponsByDateData( { + total_sales: 0, + sales_with_coupon: 0, + sales_without_coupon: 0, + } ); + + const result = buildCouponUseData( coupons, undefined ); + + expect( result.chartData ).toEqual( [] ); + expect( result.total ).toBe( 0 ); + expect( result.legendData ).toEqual( [] ); + } ); + + it( 'builds donut data from sales with and without coupons', () => { + const coupons = makeCouponsByDateData( { + total_sales: 300, + sales_with_coupon: 200, + sales_without_coupon: 100, + } ); + + const result = buildCouponUseData( coupons, undefined ); + + expect( result.chartData ).toHaveLength( 2 ); + expect( result.chartData[ 0 ].value ).toBe( 200 ); + expect( result.chartData[ 0 ].label ).toBe( 'With coupons' ); + expect( result.chartData[ 1 ].value ).toBe( 100 ); + expect( result.chartData[ 1 ].label ).toBe( 'No coupons' ); + expect( result.total ).toBe( 300 ); + } ); + + it( 'includes comparison values in legend when hasComparison is true', () => { + const coupons = makeCouponsByDateData( { + total_sales: 300, + sales_with_coupon: 200, + sales_without_coupon: 100, + } ); + + const comparison = makeCouponsByDateData( { + total_sales: 250, + sales_with_coupon: 150, + sales_without_coupon: 100, + } ); + + const result = buildCouponUseData( coupons, comparison, true ); + + expect( result.comparisonTotal ).toBe( 250 ); + expect( result.legendData[ 0 ].comparison ).toBe( 150 ); + expect( result.legendData[ 1 ].comparison ).toBe( 100 ); + } ); + + it( 'excludes comparison values from legend when hasComparison is false', () => { + const coupons = makeCouponsByDateData( { + total_sales: 300, + sales_with_coupon: 200, + sales_without_coupon: 100, + } ); + + const comparison = makeCouponsByDateData( { + total_sales: 250, + sales_with_coupon: 150, + sales_without_coupon: 100, + } ); + + const result = buildCouponUseData( coupons, comparison, false ); + + expect( result.legendData[ 0 ].comparison ).toBeUndefined(); + expect( result.legendData[ 1 ].comparison ).toBeUndefined(); + } ); + + it( 'handles case where all sales use coupons', () => { + const coupons = makeCouponsByDateData( { + total_sales: 500, + sales_with_coupon: 500, + sales_without_coupon: 0, + } ); + + const result = buildCouponUseData( coupons, undefined ); + + expect( result.chartData[ 0 ].value ).toBe( 500 ); + expect( result.chartData[ 1 ].value ).toBe( 0 ); + expect( result.total ).toBe( 500 ); + } ); + + it( 'defaults comparison totals to 0 when comparison data is missing', () => { + const coupons = makeCouponsByDateData( { + total_sales: 300, + sales_with_coupon: 200, + sales_without_coupon: 100, + } ); + + const result = buildCouponUseData( coupons, undefined, true ); + + expect( result.comparisonTotal ).toBe( 0 ); + expect( result.legendData[ 0 ].comparison ).toBe( 0 ); + expect( result.legendData[ 1 ].comparison ).toBe( 0 ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts new file mode 100644 index 000000000000..8caae1afdbf2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts @@ -0,0 +1,212 @@ +/** + * Mock formatters to avoid pulling in heavy transitive deps. + */ +jest.mock( '@next-woo-analytics/formatters', () => ( { + formatDateRange: () => 'Jan 1 – 31, 2024', +} ) ); + +/** + * Internal dependencies + */ +import { buildSalesByCouponData } from '../build-sales-by-coupon-data'; + +const defaultReportParams = { + from: '2024-01-01', + to: '2024-01-31', + compare_from: '2023-12-01', + compare_to: '2023-12-31', + interval: 'day' as const, +}; + +function makeCouponsData( + items: Array< { + coupon_code: string; + total_sales: number; + discount_amount: number; + } >, + summary: Record< string, unknown > = {} +) { + return { + data: items.map( ( item ) => ( { + ...item, + coupon_id: 1, + orders_count: 1, + } ) ), + summary: { + total_sales: 0, + total_discount_amount: 0, + total_orders: 0, + ...summary, + }, + }; +} + +describe( 'buildSalesByCouponData', () => { + it( 'returns empty chartData when coupons is undefined', () => { + const result = buildSalesByCouponData( + undefined, + undefined, + defaultReportParams + ); + + expect( result.chartData ).toEqual( [] ); + } ); + + it( 'returns empty chartData when coupons has no summary', () => { + const result = buildSalesByCouponData( + { data: [], summary: undefined } as any, + undefined, + defaultReportParams + ); + + expect( result.chartData ).toEqual( [] ); + } ); + + it( 'builds current period data from top coupons', () => { + const coupons = makeCouponsData( + [ + { + coupon_code: 'SAVE10', + total_sales: 100, + discount_amount: 10, + }, + { + coupon_code: 'SAVE20', + total_sales: 200, + discount_amount: 20, + }, + ], + { total_sales: 300 } + ); + + const result = buildSalesByCouponData( + coupons as any, + undefined, + defaultReportParams + ); + + expect( result.chartData ).toHaveLength( 1 ); + expect( result.chartData[ 0 ].data ).toEqual( [ + { label: 'SAVE10', value: 100 }, + { label: 'SAVE20', value: 200 }, + ] ); + } ); + + it( 'aggregates remaining coupons into "Other" segment', () => { + const coupons = makeCouponsData( + [ + { coupon_code: 'A', total_sales: 100, discount_amount: 5 }, + { coupon_code: 'B', total_sales: 200, discount_amount: 10 }, + { coupon_code: 'C', total_sales: 300, discount_amount: 15 }, + { coupon_code: 'D', total_sales: 50, discount_amount: 2 }, + { coupon_code: 'E', total_sales: 75, discount_amount: 3 }, + ], + { total_sales: 725 } + ); + + const result = buildSalesByCouponData( + coupons as any, + undefined, + defaultReportParams, + 3 + ); + + const currentPeriod = result.chartData[ 0 ].data; + expect( currentPeriod ).toHaveLength( 4 ); // 3 top + Other + expect( currentPeriod[ 3 ] ).toEqual( { + label: 'Other', + value: 125, // 50 + 75 + } ); + } ); + + it( 'includes comparison period data when provided', () => { + const coupons = makeCouponsData( + [ + { + coupon_code: 'SAVE10', + total_sales: 100, + discount_amount: 10, + }, + ], + { total_sales: 100 } + ); + + const comparisonCoupons = makeCouponsData( + [ { coupon_code: 'SAVE10', total_sales: 80, discount_amount: 8 } ], + { total_sales: 80 } + ); + + const result = buildSalesByCouponData( + coupons as any, + comparisonCoupons as any, + defaultReportParams + ); + + expect( result.chartData ).toHaveLength( 2 ); + expect( result.chartData[ 1 ].data[ 0 ] ).toEqual( { + label: 'SAVE10', + value: 80, + } ); + } ); + + it( 'uses total_sales not discount_amount for values', () => { + const coupons = makeCouponsData( + [ { coupon_code: 'BIG', total_sales: 500, discount_amount: 50 } ], + { total_sales: 500 } + ); + + const result = buildSalesByCouponData( + coupons as any, + undefined, + defaultReportParams + ); + + expect( result.chartData[ 0 ].data[ 0 ].value ).toBe( 500 ); + } ); + + it( 'respects custom totalSegments parameter', () => { + const coupons = makeCouponsData( + [ + { coupon_code: 'A', total_sales: 100, discount_amount: 5 }, + { coupon_code: 'B', total_sales: 200, discount_amount: 10 }, + { coupon_code: 'C', total_sales: 300, discount_amount: 15 }, + ], + { total_sales: 600 } + ); + + const result = buildSalesByCouponData( + coupons as any, + undefined, + defaultReportParams, + 2 + ); + + const currentPeriod = result.chartData[ 0 ].data; + expect( currentPeriod ).toHaveLength( 3 ); // 2 top + Other + expect( currentPeriod[ 2 ] ).toEqual( { + label: 'Other', + value: 300, + } ); + } ); + + it( 'returns 0 for missing comparison coupon codes', () => { + const coupons = makeCouponsData( + [ { coupon_code: 'NEW', total_sales: 100, discount_amount: 10 } ], + { total_sales: 100 } + ); + + const comparisonCoupons = makeCouponsData( + [ { coupon_code: 'OLD', total_sales: 50, discount_amount: 5 } ], + { total_sales: 50 } + ); + + const result = buildSalesByCouponData( + coupons as any, + comparisonCoupons as any, + defaultReportParams + ); + + // "NEW" didn't exist in comparison period + expect( result.chartData[ 1 ].data[ 0 ].value ).toBe( 0 ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts new file mode 100644 index 000000000000..697676267a55 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts @@ -0,0 +1,142 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendItem } from '../components'; +import type { DonutChartData } from '../components/chart-donut/donut-chart'; + +// Color for cancelled status +const CANCELLED_COLOR = 'rgb(240, 240, 240)'; + +export interface BookingsByAttendanceData { + chartData: DonutChartData; + total: number; + comparisonTotal: number; + legendData: LegendItem[]; +} + +/** + * Builds chart and legend data for the Bookings by Status widget. + * + * @param bookings - Primary period bookings data + * @param comparisonBookings - Comparison period bookings data + */ +export function buildBookingsByAttendanceData( + bookings: ReportDataMap[ 'bookings' ] | undefined, + comparisonBookings: ReportDataMap[ 'bookings' ] | undefined +): BookingsByAttendanceData { + if ( ! bookings?.summary ) { + return { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + }; + } + + const { summary } = bookings; + const comparisonSummary = comparisonBookings?.summary; + + // Attendance status keys from the bookings summary + type AttendanceStatusKey = + | 'attendance_status_booked' + | 'attendance_status_checked_in' + | 'attendance_status_no_show' + | 'status_cancelled'; + + // Define status mapping with user-friendly labels + const statusMap: Array< { key: AttendanceStatusKey; label: string } > = [ + { + key: 'attendance_status_booked', + label: __( 'Booked', 'woocommerce-analytics' ), + }, + { + key: 'attendance_status_checked_in', + label: __( 'Checked In', 'woocommerce-analytics' ), + }, + { + key: 'attendance_status_no_show', + label: __( 'No Show', 'woocommerce-analytics' ), + }, + { + key: 'status_cancelled', + label: __( 'Cancelled', 'woocommerce-analytics' ), + }, + ]; + + // Calculate values for each status + const statusValues = statusMap.map( ( status ) => { + const value = summary[ status.key ] || 0; + const comparisonValue = comparisonSummary + ? comparisonSummary[ status.key ] || 0 + : 0; + + return { + ...status, + value, + comparisonValue, + }; + } ); + + // Calculate total bookings across all statuses + const totalBookings = statusValues.reduce( + ( sum, status ) => sum + status.value, + 0 + ); + + const comparisonTotalBookings = statusValues.reduce( + ( sum, status ) => sum + status.comparisonValue, + 0 + ); + + // If there are no bookings, return empty state + if ( totalBookings === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal: comparisonTotalBookings, + legendData: [], + }; + } + + // Filter out statuses with zero bookings + const statusesWithData = statusValues.filter( + ( status ) => status.value > 0 + ); + + // Build chart data + const chartData: DonutChartData = statusesWithData.map( ( status ) => ( { + label: status.label, + value: status.value, + valueDisplay: formatMetricValue( status.value, 'number', { + useMultipliers: false, + decimals: 0, + } ), + ...( status.key === 'status_cancelled' && { color: CANCELLED_COLOR } ), + } ) ); + + // Build legend data + const legendData: LegendItem[] = statusesWithData.map( ( status ) => ( { + label: status.label, + value: status.value, + displayValue: formatMetricValue( status.value, 'number', { + useMultipliers: false, + decimals: 0, + } ), + comparison: comparisonBookings ? status.comparisonValue : undefined, + ...( status.key === 'status_cancelled' && { color: CANCELLED_COLOR } ), + } ) ); + + return { + chartData, + total: totalBookings, + comparisonTotal: comparisonTotalBookings, + legendData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts new file mode 100644 index 000000000000..d41e0d2a486d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts @@ -0,0 +1,117 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendItem } from '../components'; +import type { DonutChartData } from '../components/chart-donut/donut-chart'; + +export interface CouponUseData { + chartData: DonutChartData; + total: number; + comparisonTotal: number; + legendData: LegendItem[]; +} + +/** + * Builds chart and legend data for the Coupon Use widget. + * + * Uses pre-computed sales_with_coupon / sales_without_coupon from the + * coupons/by-date endpoint so the donut chart shows the correct breakdown + * of sales with vs without coupons across all orders. + * + * @param coupons - Primary period coupon-by-date data + * @param comparisonCoupons - Comparison period coupon-by-date data + * @param hasComparison - Whether comparison period should be included + */ +export function buildCouponUseData( + coupons: ReportDataMap[ 'couponsByDate' ] | null | undefined, + comparisonCoupons: ReportDataMap[ 'couponsByDate' ] | null | undefined, + hasComparison = true +): CouponUseData { + if ( ! coupons?.summary ) { + return { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + }; + } + + const salesWithCoupon = coupons.summary.sales_with_coupon; + const salesWithoutCoupon = coupons.summary.sales_without_coupon; + const totalSales = coupons.summary.total_sales; + + // Pick comparison totals + const comparisonTotalSales = comparisonCoupons?.summary.total_sales || 0; + const comparisonSalesWithCoupon = + comparisonCoupons?.summary.sales_with_coupon || 0; + const comparisonSalesWithoutCoupon = + comparisonCoupons?.summary.sales_without_coupon || 0; + + // If there are no sales, return empty state + if ( totalSales === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal: comparisonTotalSales, + legendData: [], + }; + } + + // Build chart data showing sales breakdown + const chartData: DonutChartData = [ + { + label: __( 'With coupons', 'woocommerce-analytics' ), + value: salesWithCoupon, + valueDisplay: formatMetricValue( salesWithCoupon, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + }, + { + label: __( 'No coupons', 'woocommerce-analytics' ), + value: salesWithoutCoupon, + valueDisplay: formatMetricValue( salesWithoutCoupon, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + }, + ]; + + // Build legend data + const legendData: LegendItem[] = [ + { + label: __( 'With coupons', 'woocommerce-analytics' ), + value: salesWithCoupon, + displayValue: formatMetricValue( salesWithCoupon, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + comparison: hasComparison ? comparisonSalesWithCoupon : undefined, + }, + { + label: __( 'No coupons', 'woocommerce-analytics' ), + value: salesWithoutCoupon, + displayValue: formatMetricValue( salesWithoutCoupon, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + comparison: hasComparison + ? comparisonSalesWithoutCoupon + : undefined, + }, + ]; + + return { + chartData, + total: totalSales, + comparisonTotal: comparisonTotalSales, + legendData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts new file mode 100644 index 000000000000..757b18bc6779 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendItem } from '../components'; +import type { DonutChartData } from '../components/chart-donut/donut-chart'; + +export interface NewVsReturningCustomerData { + chartData: DonutChartData; + total: number; + comparisonTotal: number; + legendData: LegendItem[]; +} + +/** + * Builds chart and legend data for the New vs Returning Customer widget. + * Shows unique customer counts (not revenue) broken down by new vs returning. + * + * @param customers - Primary period customers by date data + * @param comparisonCustomers - Comparison period customers by date data + * @param hasComparison - Whether comparison period should be included + */ +export function buildNewVsReturningCustomerData( + customers: ReportDataMap[ 'customersByDate' ] | null | undefined, + comparisonCustomers: ReportDataMap[ 'customersByDate' ] | null | undefined, + hasComparison = true +): NewVsReturningCustomerData { + if ( ! customers?.summary ) { + return { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + }; + } + + const totalCustomers = customers.summary.total_customers; + const newCustomers = customers.summary.new_customers; + const returningCustomers = customers.summary.returning_customers; + + // Pick comparison totals + const comparisonTotalCustomers = + comparisonCustomers?.summary?.total_customers || 0; + const comparisonNewCustomers = + comparisonCustomers?.summary?.new_customers || 0; + const comparisonReturningCustomers = + comparisonCustomers?.summary?.returning_customers || 0; + + // If there are no customers, return empty state + if ( totalCustomers === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal: comparisonTotalCustomers, + legendData: [], + }; + } + + // Build chart data showing customer counts + // Note: Returning customers first to match design (larger segment first) + const chartData: DonutChartData = [ + { + label: __( 'Returning', 'woocommerce-analytics' ), + value: returningCustomers, + valueDisplay: formatMetricValue( returningCustomers, 'number', { + useMultipliers: true, + decimals: 0, + } ), + }, + { + label: __( 'New', 'woocommerce-analytics' ), + value: newCustomers, + valueDisplay: formatMetricValue( newCustomers, 'number', { + useMultipliers: true, + decimals: 0, + } ), + }, + ]; + + // Build legend data (same order as chart) + const legendData: LegendItem[] = [ + { + label: __( 'Returning', 'woocommerce-analytics' ), + value: returningCustomers, + displayValue: formatMetricValue( returningCustomers, 'number', { + useMultipliers: true, + decimals: 0, + } ), + comparison: hasComparison + ? comparisonReturningCustomers + : undefined, + }, + { + label: __( 'New', 'woocommerce-analytics' ), + value: newCustomers, + displayValue: formatMetricValue( newCustomers, 'number', { + useMultipliers: true, + decimals: 0, + } ), + comparison: hasComparison ? comparisonNewCustomers : undefined, + }, + ]; + + return { + chartData, + total: totalCustomers, + comparisonTotal: comparisonTotalCustomers, + legendData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts new file mode 100644 index 000000000000..69968c2a8df8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendItem } from '../components'; +import type { DonutChartData } from '../components/chart-donut/donut-chart'; + +export interface OrdersFulfillmentData { + chartData: DonutChartData; + total: number; + comparisonTotal: number; + legendData: LegendItem[]; +} + +/** + * Builds chart and legend data for the Orders Fulfillment widget. + * + * Takes separate responses from filtered API calls (one for fulfilled + * orders, one for unfulfilled orders) and combines them into donut chart data. + * + * @param fulfilledOrders - Primary period fulfilled orders data + * @param unfulfilledOrders - Primary period unfulfilled orders data + * @param comparisonFulfilledOrders - Comparison period fulfilled orders data + * @param comparisonUnfulfilledOrders - Comparison period unfulfilled orders data + */ +export function buildOrdersFulfillmentData( + fulfilledOrders: ReportDataMap[ 'orders' ] | undefined, + unfulfilledOrders: ReportDataMap[ 'orders' ] | undefined, + comparisonFulfilledOrders: ReportDataMap[ 'orders' ] | undefined, + comparisonUnfulfilledOrders: ReportDataMap[ 'orders' ] | undefined +): OrdersFulfillmentData { + const fulfilledCount = fulfilledOrders?.summary?.orders_no ?? 0; + const unfulfilledCount = unfulfilledOrders?.summary?.orders_no ?? 0; + const totalOrders = fulfilledCount + unfulfilledCount; + + const comparisonFulfilledCount = + comparisonFulfilledOrders?.summary?.orders_no ?? 0; + const comparisonUnfulfilledCount = + comparisonUnfulfilledOrders?.summary?.orders_no ?? 0; + const comparisonTotalOrders = + comparisonFulfilledCount + comparisonUnfulfilledCount; + + if ( totalOrders === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal: comparisonTotalOrders, + legendData: [], + }; + } + + const formatCount = ( value: number ) => + formatMetricValue( value, 'number', { + useMultipliers: true, + decimals: 0, + } ); + + const chartData: DonutChartData = [ + { + label: __( 'Fulfilled', 'woocommerce-analytics' ), + value: fulfilledCount, + valueDisplay: formatCount( fulfilledCount ), + }, + { + label: __( 'Unfulfilled', 'woocommerce-analytics' ), + value: unfulfilledCount, + valueDisplay: formatCount( unfulfilledCount ), + }, + ]; + + const legendData: LegendItem[] = [ + { + label: __( 'Fulfilled', 'woocommerce-analytics' ), + value: fulfilledCount, + displayValue: formatCount( fulfilledCount ), + comparison: comparisonFulfilledOrders + ? comparisonFulfilledCount + : undefined, + }, + { + label: __( 'Unfulfilled', 'woocommerce-analytics' ), + value: unfulfilledCount, + displayValue: formatCount( unfulfilledCount ), + comparison: comparisonUnfulfilledOrders + ? comparisonUnfulfilledCount + : undefined, + }, + ]; + + return { + chartData, + total: totalOrders, + comparisonTotal: comparisonTotalOrders, + legendData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts new file mode 100644 index 000000000000..23631bc32e64 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendItem } from '../components'; +import type { DonutChartData } from '../components/chart-donut/donut-chart'; + +export interface PaymentStatusData { + chartData: DonutChartData; + total: number; + comparisonTotal: number; + legendData: LegendItem[]; +} + +/** + * Builds chart and legend data for the Payment Status widget. + * + * @param orders - Primary period order data + * @param comparisonOrders - Comparison period order data + */ +export function buildPaymentStatusData( + orders: ReportDataMap[ 'orders' ] | undefined, + comparisonOrders: ReportDataMap[ 'orders' ] | undefined +): PaymentStatusData { + if ( ! orders?.summary ) { + return { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + }; + } + + const { summary } = orders; + const paidNetSales = summary.paid_net_sales; + const unpaidNetSales = summary.unpaid_net_sales; + const totalSales = paidNetSales + unpaidNetSales; + + // Calculate comparison totals + const comparisonPaidNetSales = + comparisonOrders?.summary?.paid_net_sales || 0; + const comparisonUnpaidNetSales = + comparisonOrders?.summary?.unpaid_net_sales || 0; + const comparisonTotalSales = + comparisonPaidNetSales + comparisonUnpaidNetSales; + + // If there are no sales, return empty state + if ( totalSales === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal: comparisonTotalSales, + legendData: [], + }; + } + + // Build chart data + const chartData: DonutChartData = [ + { + label: __( 'Paid', 'woocommerce-analytics' ), + value: paidNetSales, + valueDisplay: formatMetricValue( paidNetSales, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + }, + { + label: __( 'Unpaid', 'woocommerce-analytics' ), + value: unpaidNetSales, + valueDisplay: formatMetricValue( unpaidNetSales, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + }, + ]; + + // Build legend data + const legendData: LegendItem[] = [ + { + label: __( 'Paid', 'woocommerce-analytics' ), + value: paidNetSales, + displayValue: formatMetricValue( paidNetSales, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + comparison: comparisonOrders ? comparisonPaidNetSales : undefined, + }, + { + label: __( 'Unpaid', 'woocommerce-analytics' ), + value: unpaidNetSales, + displayValue: formatMetricValue( unpaidNetSales, 'currency', { + useMultipliers: true, + decimals: 0, + } ), + comparison: comparisonOrders ? comparisonUnpaidNetSales : undefined, + }, + ]; + + return { + chartData, + total: totalSales, + comparisonTotal: comparisonTotalSales, + legendData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts new file mode 100644 index 000000000000..2a2853fd801e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { SeriesData } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import { formatLegendLabels } from './format-legend-labels'; + +export interface RevenueByCustomerTypeData { + chartData: SeriesData[]; +} + +/** + * Builds bar chart data for the Revenue by Customer Type widget. + * + * Shows revenue split between new and returning customers. + * + * @param customers - Primary period customer data + * @param comparisonCustomers - Comparison period customer data + * @param reportParams - Report parameters for generating date range labels + */ +export function buildRevenueByCustomerTypeData( + customers: ReportDataMap[ 'customers' ] | undefined, + comparisonCustomers: ReportDataMap[ 'customers' ] | undefined, + reportParams: ReportParams +): RevenueByCustomerTypeData { + if ( ! customers?.summary ) { + return { + chartData: [], + }; + } + + const { primary: primaryLabel, comparison: comparisonLabel } = + formatLegendLabels( reportParams ); + + const { summary } = customers; + const newCustomerSales = summary.new_customer_sales; + const returningCustomerSales = summary.returning_customer_sales; + + // Build bar chart data - each category is a bar + const chartData: SeriesData[] = [ + { + label: primaryLabel, + data: [ + { + label: __( 'Returning', 'woocommerce-analytics' ), + value: returningCustomerSales, + }, + { + label: __( 'New', 'woocommerce-analytics' ), + value: newCustomerSales, + }, + ], + }, + ]; + + // Add comparison period if available + if ( comparisonCustomers?.summary ) { + const comparisonNewCustomerSales = + comparisonCustomers.summary.new_customer_sales || 0; + const comparisonReturningCustomerSales = + comparisonCustomers.summary.returning_customer_sales || 0; + + chartData.push( { + label: comparisonLabel, + data: [ + { label: 'Returning', value: comparisonReturningCustomerSales }, + { label: 'New', value: comparisonNewCustomerSales }, + ], + } ); + } + + return { + chartData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts new file mode 100644 index 000000000000..080991ae33dc --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts @@ -0,0 +1,109 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { SeriesData } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import { formatLegendLabels } from './format-legend-labels'; + +export interface SalesByCouponData { + chartData: SeriesData[]; +} + +/** + * Builds bar chart data for the Sales by Coupon widget. + * + * Shows revenue distribution by coupon with top coupons plus "Other" segment. + * + * @param coupons - Primary period coupon data + * @param comparisonCoupons - Comparison period coupon data + * @param reportParams - Report parameters for generating date range labels + * @param totalSegments - Number of top coupons to show (rest goes to "Other") + */ +export function buildSalesByCouponData( + coupons: ReportDataMap[ 'coupons' ] | undefined, + comparisonCoupons: ReportDataMap[ 'coupons' ] | undefined, + reportParams: ReportParams, + totalSegments = 3 +): SalesByCouponData { + if ( ! coupons?.summary ) { + return { + chartData: [], + }; + } + + const { primary: primaryLabel, comparison: comparisonLabel } = + formatLegendLabels( reportParams ); + + const { data: items } = coupons; + + // Process coupons and limit to totalSegments + const topCoupons = items.slice( 0, totalSegments ); + + // Create a map of comparison data by coupon code + const comparisonMap = new Map< string, number >(); + if ( comparisonCoupons ) { + comparisonCoupons.data.forEach( ( item ) => { + comparisonMap.set( item.coupon_code, item.total_sales ); + } ); + } + + // Build current period data points + const currentPeriodData = topCoupons.map( ( item ) => ( { + label: item.coupon_code, + value: item.total_sales, + } ) ); + + // Add "Other" segment if there are more coupons than shown + if ( items.length > totalSegments ) { + const otherSales = items + .slice( totalSegments ) + .reduce( ( sum, item ) => sum + item.total_sales, 0 ); + + currentPeriodData.push( { + label: __( 'Other', 'woocommerce-analytics' ), + value: otherSales, + } ); + } + + // Build bar chart data - current period + const chartData: SeriesData[] = [ + { + label: primaryLabel, + data: currentPeriodData, + }, + ]; + + // Add comparison period if available + if ( comparisonCoupons?.summary ) { + const comparisonPeriodData = topCoupons.map( ( item ) => ( { + label: item.coupon_code, + value: comparisonMap.get( item.coupon_code ) || 0, + } ) ); + + // Add "Other" segment for comparison + if ( items.length > totalSegments ) { + const otherComparison = comparisonCoupons.data + .slice( totalSegments ) + .reduce( ( sum, item ) => sum + item.total_sales, 0 ); + + comparisonPeriodData.push( { + label: __( 'Other', 'woocommerce-analytics' ), + value: otherComparison, + } ); + } + + chartData.push( { + label: comparisonLabel, + data: comparisonPeriodData, + } ); + } + + return { + chartData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts new file mode 100644 index 000000000000..8e9164c677c5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { SeriesData } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import { formatLegendLabels } from './format-legend-labels'; + +export interface SalesByDeviceData { + chartData: SeriesData[]; +} + +/** + * Builds bar chart data for the Sales by Device widget. + * + * Shows sales breakdown by device type (Desktop, Mobile, Tablet). + * + * @param orderAttribution - Primary period order attribution data + * @param hasComparison - Whether comparison period should be included + * @param reportParams - Report parameters for generating date range labels + */ +export function buildSalesByDeviceData( + orderAttribution: ReportDataMap[ 'order-attribution' ] | undefined, + hasComparison: boolean, + reportParams: ReportParams +): SalesByDeviceData { + if ( ! orderAttribution?.data || orderAttribution.data.length === 0 ) { + return { + chartData: [], + }; + } + + const { primary: primaryLabel, comparison: comparisonLabel } = + formatLegendLabels( reportParams ); + + const { data } = orderAttribution; + + // Build bar chart data - current period + const chartData: SeriesData[] = [ + { + label: primaryLabel, + data: data.map( ( item ) => ( { + label: item.item || __( 'Unassigned', 'woocommerce-analytics' ), + value: item.current_period?.value ?? 0, + } ) ), + }, + ]; + + // Add comparison period if available + if ( hasComparison ) { + chartData.push( { + label: comparisonLabel, + data: data.map( ( item ) => ( { + label: item.item || __( 'Unassigned', 'woocommerce-analytics' ), + value: item.previous_period?.value ?? 0, + } ) ), + } ); + } + + return { + chartData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts new file mode 100644 index 000000000000..72f7f5396b86 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts @@ -0,0 +1,58 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LeaderboardChartData } from '../components/chart-leaderboard'; +import { calculateDelta } from './calculate-delta'; + +/** + * Builds leaderboard chart data for the Sales by UTM widget. + * + * Transforms order attribution data into the format required by LeaderboardChart. + * + * @param orderAttribution - Primary period order attribution data + * @param maxEntries - Maximum number of entries to include in the leaderboard + * @return Processed data ready for LeaderboardChart component + */ +export function buildSalesByUtmData( + orderAttribution: ReportDataMap[ 'order-attribution' ] | undefined, + maxEntries = 4 +): LeaderboardChartData { + if ( ! orderAttribution?.data || orderAttribution.data.length === 0 ) { + return []; + } + + const { data } = orderAttribution; + + // Find the max value for share calculation + const maxValue = Math.max( + ...data.map( ( item ) => + Math.max( + item.current_period.value || 0, + item.previous_period?.value || 0 + ) + ), + 1 // Prevent division by zero + ); + + return data.slice( 0, maxEntries ).map( ( item, idx ) => { + const currentValue = item.current_period.value || 0; + const previousValue = item.previous_period?.value ?? 0; + const delta = calculateDelta( currentValue, previousValue ); + + return { + id: item.item ? String( item.item ) : String( idx ), + label: item.item || __( 'Unassigned', 'woocommerce-analytics' ), + currentValue, + previousValue, + currentShare: ( currentValue / maxValue ) * 100, + previousShare: ( previousValue / maxValue ) * 100, + delta, + }; + } ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts new file mode 100644 index 000000000000..3e7dfb42bb02 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts @@ -0,0 +1,120 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendItem } from '../components'; +import type { SemiCircleChartData } from '../components/chart-semi-circle/semi-circle-chart'; + +export interface SessionsByDeviceData { + chartData: SemiCircleChartData; + total: number; + comparisonTotal: number; + legendData: LegendItem[]; +} + +/** + * Device type display labels. + * Maps API device_type values to user-friendly labels. + */ +const DEVICE_LABELS: Record< string, string > = { + mobile: __( 'Mobile', 'woocommerce-analytics' ), + desktop: __( 'Desktop', 'woocommerce-analytics' ), + tablet: __( 'Tablet', 'woocommerce-analytics' ), +}; + +/** + * Get the display label for a device type. + * + * @param deviceType - The device type from the API + */ +function getDeviceLabel( deviceType: string ): string { + const normalized = deviceType.toLowerCase(); + return DEVICE_LABELS[ normalized ] || deviceType; +} + +/** + * Builds chart and legend data for the Sessions by Device widget. + * + * @param sessionsByDevice - Primary period sessions by device data + * @param comparisonSessionsByDevice - Comparison period sessions by device data + */ +export function buildSessionsByDeviceData( + sessionsByDevice: ReportDataMap[ 'sessionsByDevice' ] | undefined, + comparisonSessionsByDevice?: ReportDataMap[ 'sessionsByDevice' ] | undefined +): SessionsByDeviceData { + if ( ! sessionsByDevice?.data || sessionsByDevice.data.length === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + }; + } + + const { data, summary } = sessionsByDevice; + const total = summary.total_sessions; + const comparisonTotal = + comparisonSessionsByDevice?.summary?.total_sessions || 0; + + // If there are no sessions, return empty state + if ( total === 0 ) { + return { + chartData: [], + total: 0, + comparisonTotal, + legendData: [], + }; + } + + // Create a map of comparison data by device type + const comparisonMap = new Map< string, number >(); + if ( comparisonSessionsByDevice?.data ) { + comparisonSessionsByDevice.data.forEach( ( item ) => { + comparisonMap.set( + item.device_type.toLowerCase(), + item.active_sessions + ); + } ); + } + + // Build chart data + const chartData: SemiCircleChartData = data.map( ( item ) => ( { + label: getDeviceLabel( item.device_type ), + value: item.active_sessions, + valueDisplay: formatMetricValue( item.active_sessions, 'number', { + useMultipliers: true, + decimals: 0, + } ), + } ) ); + + // Build legend data + const legendData: LegendItem[] = data.map( ( item ) => { + const normalizedType = item.device_type.toLowerCase(); + const comparisonValue = comparisonSessionsByDevice + ? comparisonMap.get( normalizedType ) || 0 + : undefined; + + return { + label: getDeviceLabel( item.device_type ), + value: item.active_sessions, + displayValue: formatMetricValue( item.active_sessions, 'number', { + useMultipliers: true, + decimals: 0, + } ), + comparison: comparisonValue, + }; + } ); + + return { + chartData, + total, + comparisonTotal, + legendData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts new file mode 100644 index 000000000000..47d292291e0d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts @@ -0,0 +1,106 @@ +/** + * External dependencies + */ +import { formatDateRange } from '@wc-analytics/formatters'; +import { localTZDate } from '@next-woo-analytics/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { + ComparativeLineChartSeries, + ComparativeDatePointDate, +} from '../components/chart-comparative-line/types'; + +/** + * Generic type for time series data that has date_start and metric values + */ +export type TimeSeriesData = { + date_start: string; + [ key: string ]: string | number; +}; + +/** + * Generic type for time series response. + * The summary only needs date_start and date_end for chart labels, + * so we use a loose constraint that accepts any summary with those fields. + */ +type TimeSeriesResponse< T extends TimeSeriesData > = { + data: T[]; + summary: { date_start: string; date_end: string }; +}; + +/** + * Map time series items array into chart series data. + */ +function mapTimeSeriesToLineChartData< T extends TimeSeriesData >( + data: T[], + metricKey: keyof T +): ComparativeDatePointDate[] { + if ( ! data ) { + return []; + } + + return data.map( ( item ) => ( { + date: localTZDate( item.date_start ), + value: Number( item[ metricKey ] ), + } ) ); +} + +type BuildTimeSeriesChartOptions< T extends TimeSeriesData > = { + primary: TimeSeriesResponse< T >; + comparison?: TimeSeriesResponse< T >; + metricKey: keyof T; + emptyDataFallback?: 'empty-array' | 'no-data-series'; +}; + +/** + * Generic function to build line chart series from time series data + */ +export function buildTimeSeriesChartData< T extends TimeSeriesData >( { + primary, + comparison, + metricKey, + emptyDataFallback = 'empty-array', +}: BuildTimeSeriesChartOptions< T > ): ComparativeLineChartSeries[] { + if ( ! primary.data?.length ) { + if ( emptyDataFallback === 'no-data-series' ) { + return [ + { + label: __( 'No data available', 'woocommerce-analytics' ), + data: [], + }, + ]; + } + return []; + } + + const primarySeries: ComparativeLineChartSeries = { + label: formatDateRange( { + from: localTZDate( primary.summary.date_start ), + to: localTZDate( primary.summary.date_end ), + } ), + data: mapTimeSeriesToLineChartData( primary.data, metricKey ), + group: 'primary', + options: {}, + }; + + if ( ! comparison?.data?.length ) { + return [ primarySeries ]; + } + + const comparisonSeries: ComparativeLineChartSeries = { + label: formatDateRange( { + from: localTZDate( comparison.summary.date_start ), + to: localTZDate( comparison.summary.date_end ), + } ), + data: mapTimeSeriesToLineChartData( comparison.data, metricKey ), + group: 'primary', + options: { + type: 'comparison', + }, + }; + + return [ primarySeries, comparisonSeries ]; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts new file mode 100644 index 000000000000..a43f5e182e16 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { SeriesData } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import { formatLegendLabels } from './format-legend-labels'; + +export interface TotalReturnsData { + chartData: SeriesData[]; +} + +/** + * Builds bar chart data for the Total Returns widget. + * + * Shows refunds and net sales as a bar chart, which properly + * supports negative values for refunds visualization. + * + * @param orders - Primary period orders data + * @param comparisonOrders - Comparison period orders data + * @param reportParams - Report parameters for generating date range labels + */ +export function buildTotalReturnsData( + orders: ReportDataMap[ 'orders' ] | null | undefined, + comparisonOrders: ReportDataMap[ 'orders' ] | null | undefined, + reportParams: ReportParams +): TotalReturnsData { + if ( ! orders?.data || ! orders?.summary ) { + return { + chartData: [], + }; + } + + const refundsAmount = orders.summary.refunds ?? 0; + const comparisonRefundsAmount = comparisonOrders?.summary?.refunds ?? 0; + + // When there are no refunds in either period, return empty + // data so the widget shows an empty state instead of + // misleadingly displaying total sales as "returns". + if ( refundsAmount === 0 && comparisonRefundsAmount === 0 ) { + return { + chartData: [], + }; + } + + const { primary: primaryLabel, comparison: comparisonLabel } = + formatLegendLabels( reportParams ); + const totalSales = orders.summary.total_sales ?? 0; + + // Net sales (total sales minus refunds) + const salesAmount = Math.max( 0, totalSales - refundsAmount ); + + // Build bar chart data - each category is a bar + const chartData: SeriesData[] = [ + { + label: primaryLabel, + data: [ + { label: 'Total sales', value: salesAmount }, + { + label: __( 'Refunds', 'woocommerce-analytics' ), + value: refundsAmount, + }, + ], + }, + ]; + + // Add comparison period if available + if ( comparisonOrders?.summary ) { + const comparisonTotalRefunds = comparisonOrders.summary.refunds || 0; + const comparisonTotalSales = comparisonOrders.summary.total_sales || 0; + const comparisonSalesAmount = Math.max( + 0, + comparisonTotalSales - comparisonTotalRefunds + ); + + chartData.push( { + label: comparisonLabel, + data: [ + { + label: __( 'Total sales', 'woocommerce-analytics' ), + value: comparisonSalesAmount, + }, + { + label: __( 'Refunds', 'woocommerce-analytics' ), + value: comparisonTotalRefunds, + }, + ], + } ); + } + + return { + chartData, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts new file mode 100644 index 000000000000..8f5586d9ca2e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import type { GeoData } from '@automattic/charts'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { LeaderboardChartData } from '../components/chart-leaderboard/leaderboard-chart'; + +export type Region = 'US' | 'world'; + +export type VisitorsByLocationData = { + geoData: GeoData; + leaderboardData: LeaderboardChartData; +}; + +export type LocationDataEntry = { + id: string; + label: string; + value: number; +}; + +type BuildVisitorsByLocationDataParams = { + primaryData: LocationDataEntry[]; + comparisonData?: LocationDataEntry[]; + region: Region; + limit?: number; +}; + +/** + * Build geo chart and leaderboard data from raw location data. + * + * @param params - Build parameters + * @param params.primaryData - Primary period data + * @param params.comparisonData - Comparison period data (optional) + * @param params.region - The region ('US' or 'world') + * @param params.limit - Maximum number of items for leaderboard (default: 5) + * @return Geo chart data and leaderboard data + */ +export function buildVisitorsByLocationData( { + primaryData, + comparisonData, + region, + limit = 5, +}: BuildVisitorsByLocationDataParams ): VisitorsByLocationData { + const headerLabel = + region === 'US' + ? __( 'State', 'woocommerce-analytics' ) + : __( 'Country', 'woocommerce-analytics' ); + + // Build geo chart data + const geoData: GeoData = [ + [ headerLabel, 'Visitors' ], + ...primaryData.map( + ( item ) => [ item.label, item.value ] as [ string, number ] + ), + ]; + + // Find max values for bar width scaling (largest value = 100% width) + const maxPrimaryValue = Math.max( + ...primaryData.map( ( d ) => d.value ), + 0 + ); + const maxComparisonValue = comparisonData + ? Math.max( ...comparisonData.map( ( d ) => d.value ), 0 ) + : 0; + + // Build leaderboard data (top N items) + const leaderboardData: LeaderboardChartData = primaryData + .slice( 0, limit ) + .map( ( item ) => { + const comparisonItem = comparisonData?.find( + ( c ) => c.id === item.id + ); + const previousValue = comparisonItem?.value ?? 0; + const currentShare = + maxPrimaryValue > 0 + ? ( item.value / maxPrimaryValue ) * 100 + : 0; + const previousShare = + maxComparisonValue > 0 + ? ( previousValue / maxComparisonValue ) * 100 + : 0; + const delta = + previousValue > 0 + ? ( ( item.value - previousValue ) / previousValue ) * 100 + : 0; + + return { + id: item.id, + label: item.label, + currentValue: item.value, + previousValue, + currentShare, + previousShare, + delta, + }; + } ); + + return { geoData, leaderboardData }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts new file mode 100644 index 000000000000..8669e6bac847 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts @@ -0,0 +1,33 @@ +/** + * Calculates the percentage change (delta) between two values. + * + * Handles edge cases where the previous value is zero: + * - 0 → 0: Returns 0% (no change) + * - 0 → positive: Returns 100% (instead of infinity, representing "new/appeared") + * - 0 → negative: Returns 0% (no meaningful decrease from zero) + * + * @param currentValue - Current period value + * @param previousValue - Previous period value + * @return Percentage change as a number (e.g., 50 for 50% increase, -25 for 25% decrease) + * + * @example + * calculateDelta(150, 100) // Returns 50 (50% increase) + * calculateDelta(75, 100) // Returns -25 (25% decrease) + * calculateDelta(100, 0) // Returns 100 (new item, instead of infinity) + * calculateDelta(0, 0) // Returns 0 (no change) + * calculateDelta(0, 100) // Returns -100 (complete disappearance) + */ +export function calculateDelta( + currentValue: number, + previousValue: number +): number { + // Handle the case where previous value is zero + if ( previousValue === 0 ) { + // If previous was 0 and current is positive, show 100% increase + // If both are 0, show 0% change + return currentValue > 0 ? 100 : 0; + } + + // Standard percentage change calculation + return ( ( currentValue - previousValue ) / previousValue ) * 100; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts new file mode 100644 index 000000000000..b5bf5c8ed333 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts @@ -0,0 +1,68 @@ +/** + * Shared utilities for handling empty chart data states. + * Used by BarChart, ComparativeLineChart, DonutChart, and SemiCircleChart. + */ + +/** + * External dependencies + */ +import type { DataPointPercentage } from '@automattic/charts'; + +/** + * Series data shape for bar and line charts (nested array format). + */ +type SeriesWithData = { + data: Array< { value: number | null } >; +}; + +/** + * Checks if chart data is empty (all values are 0 or null). + * Used to disable tooltips and apply fixed Y-axis domains when there's no meaningful data. + * + * @param series - Array of series data to check + * @return True if all values across all series are 0 or null + */ +export function isEmptyChartData( series: SeriesWithData[] ): boolean { + return series.every( ( s ) => + s.data.every( ( point ) => point.value === 0 || point.value === null ) + ); +} + +/** + * Checks if pie chart data is empty (all values are 0). + * Used for DonutChart and SemiCircleChart. + * + * @param data - Array of DataPointPercentage to check + * @return True if data is empty or all values are 0 + */ +export function isEmptyPieChartData( + data: DataPointPercentage[] | undefined | null +): boolean { + if ( ! data || data.length === 0 ) { + return true; + } + return data.every( ( item ) => item.value === 0 ); +} + +/** + * Returns a sensible Y-axis domain for empty chart data based on metric type. + * Each domain is chosen to produce evenly spaced, readable tick values: + * - currency: 0-4K (ticks: 0, 1K, 2K, 3K, 4K) + * - percentage: 0-1.0 (ticks: 0%, 25%, 50%, 75%, 100%) + * - number: 0-80 (ticks: 0, 20, 40, 60, 80) + * + * @param metricType - The type of data format (currency, number, percentage) + * @return Y-axis domain tuple [min, max] + */ +export function getEmptyChartDomain( metricType: string ): [ number, number ] { + if ( metricType === 'currency' ) { + return [ 0, 4000 ]; + } + + if ( metricType === 'percentage' ) { + return [ 0, 1.0 ]; + } + + // Default for 'number' and other types + return [ 0, 80 ]; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts new file mode 100644 index 000000000000..3d755706c555 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts @@ -0,0 +1,13 @@ +/** + * Given a country code, return a flag SVG URL from CDN. + * @param countryCode A two-letter ISO 3166-1 country code (lowercase) + * @return Flag SVG URL + */ +export function flagUrl( countryCode: string ): string | null { + if ( ! countryCode || countryCode.length !== 2 ) { + return null; + } + + // Use jsDelivr CDN to serve flag-icons package SVGs + return `https://cdn.jsdelivr.net/npm/flag-icons@7.5.0/flags/4x3/${ countryCode.toLowerCase() }.svg`; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts new file mode 100644 index 000000000000..63c35f0bf4f2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { formatDateRange } from '@wc-analytics/formatters'; +import type { ReportParams } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import type { LegendLabels } from '../components/chart-leaderboard'; + +/** + * Formats legend labels from report parameters. + * + * Creates human-readable legend labels for chart comparisons based on the + * date ranges in the report parameters. If date ranges are not available, + * returns default period labels. + * + * @param reportParams - Report parameters containing date ranges + * @return Object with primary and comparison legend labels + * + * @example + * ```ts + * const labels = formatLegendLabels({ + * from: '2024-01-01', + * to: '2024-01-31', + * compare_from: '2023-12-01', + * compare_to: '2023-12-31', + * interval: 'day' + * }); + * // Returns: { primary: 'Jan 1 - 31, 2024', comparison: 'Dec 1 - 31, 2023' } + * ``` + */ +export function formatLegendLabels( reportParams: ReportParams ): LegendLabels { + const primaryLabel = formatDateRange( { + from: new Date( reportParams.from ), + to: new Date( reportParams.to ), + } ); + + const comparisonLabel = + reportParams.compare_from && reportParams.compare_to + ? formatDateRange( { + from: new Date( reportParams.compare_from ), + to: new Date( reportParams.compare_to ), + } ) + : __( 'Previous period', 'woocommerce-analytics' ); + + return { + primary: primaryLabel, + comparison: comparisonLabel, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts new file mode 100644 index 000000000000..a108271b474b --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts @@ -0,0 +1,114 @@ +/** + * External dependencies + */ +import { formatMetricValue } from '@wc-analytics/formatters'; + +/** + * Internal dependencies + */ +import type { MetricKey } from '../types'; + +type FormatMetricOptions = NonNullable< + Parameters< typeof formatMetricValue >[ 2 ] +>; + +type MetricType = NonNullable< Parameters< typeof formatMetricValue >[ 1 ] >; + +const metricFormatMap: Record< + MetricKey, + { metricType: MetricType; format?: FormatMetricOptions } +> = { + orders_no: { + metricType: 'number', + }, + total_sales: { + metricType: 'currency', + }, + average_order_value: { + metricType: 'currency', + }, + avg_items: { + metricType: 'average', + }, + orders_value_net: { + metricType: 'currency', + }, + orders_value_gross: { + metricType: 'currency', + }, + coupons: { + metricType: 'currency', + }, + profit_margin: { + metricType: 'currency', + }, + visitors: { + metricType: 'number', + format: { + useMultipliers: true, + decimals: 0, + }, + }, + conversion_rate: { + metricType: 'percentage', + format: { + decimals: 1, + }, + }, + customers: { + metricType: 'number', + format: { + useMultipliers: true, + decimals: 0, + }, + }, + // Booking status metrics + status_unpaid: { + metricType: 'number', + }, + status_pending_confirmation: { + metricType: 'number', + }, + status_confirmed: { + metricType: 'number', + }, + status_paid: { + metricType: 'number', + }, + status_cancelled: { + metricType: 'number', + }, + status_complete: { + metricType: 'number', + }, + // Booking attendance metrics + attendance_status_booked: { + metricType: 'number', + }, + attendance_status_no_show: { + metricType: 'number', + }, + attendance_status_checked_in: { + metricType: 'number', + }, +}; + +export function formatOrderMetric( + metricKey: MetricKey, + options?: FormatMetricOptions +) { + return ( value: number ) => + formatMetricValue( + value, + metricFormatMap[ metricKey ].metricType, + options ?? {} + ); +} + +export function getFormatByMetricKey( metricKey: MetricKey ) { + const config = metricFormatMap[ metricKey ]; + return { + type: config.metricType, + options: config.format, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts new file mode 100644 index 000000000000..c4048ab1db45 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import type { FilterCondition } from '@next-woo-analytics/data'; + +/** + * Filter for fulfilled orders only. + */ +export const FULFILLED_ORDERS_FILTER: FilterCondition = { + key: 'fulfillment_status', + value: 'fulfilled', + compare: '=', +}; + +/** + * Filter for unfulfilled orders (includes orders with no fulfillments). + */ +export const UNFULFILLED_ORDERS_FILTER: FilterCondition = { + key: 'fulfillment_status', + value: [ 'unfulfilled', 'no_fulfillments' ], + compare: 'IN', +}; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts new file mode 100644 index 000000000000..38d8beb18478 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts @@ -0,0 +1,78 @@ +export { + formatOrderMetric, + getFormatByMetricKey, +} from './format-orders-metrics'; +export { + buildTimeSeriesChartData, + type TimeSeriesData, +} from './build-time-series-chart-data'; +export { + buildSalesByCouponData, + type SalesByCouponData, +} from './build-sales-by-coupon-data'; +export { + PHYSICAL_PRODUCTS_FILTER, + BOOKINGS_FILTER, +} from './product-type-filters'; +export { + FULFILLED_ORDERS_FILTER, + UNFULFILLED_ORDERS_FILTER, +} from './fulfillment-filters'; +export { PAYMENT_STATUS_FILTERS } from './payment-status-filters'; +export { + buildRevenueByCustomerTypeData, + type RevenueByCustomerTypeData, +} from './build-revenue-by-customer-type-data'; +export { + buildNewVsReturningCustomerData, + type NewVsReturningCustomerData, +} from './build-new-vs-returning-customer-data'; +export { + resolveSegmentStyles, + applyStylesToItems, + type SegmentStyle, + type ColorableItem, +} from './segment-styles'; +export { + buildSalesByDeviceData, + type SalesByDeviceData, +} from './build-sales-by-device-data'; +export { + buildSessionsByDeviceData, + type SessionsByDeviceData, +} from './build-sessions-by-device-data'; +export { + buildBookingsByAttendanceData, + type BookingsByAttendanceData, +} from './build-bookings-by-attendance-data'; +export { + buildTotalReturnsData, + type TotalReturnsData, +} from './build-total-returns-data'; +export { buildSalesByUtmData } from './build-sales-by-utm-data'; +export { formatLegendLabels } from './format-legend-labels'; +export { calculateDelta } from './calculate-delta'; +export { + buildCouponUseData, + type CouponUseData, +} from './build-coupon-use-data'; +export { + buildPaymentStatusData, + type PaymentStatusData, +} from './build-payment-status-data'; +export { + buildOrdersFulfillmentData, + type OrdersFulfillmentData, +} from './build-orders-fulfillment-data'; +export { + buildVisitorsByLocationData, + type VisitorsByLocationData, + type LocationDataEntry, + type Region, +} from './build-visitors-by-location-data'; +export { flagUrl } from './flag-url'; +export { + isEmptyChartData, + isEmptyPieChartData, + getEmptyChartDomain, +} from './chart-empty-state'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts new file mode 100644 index 000000000000..124041ad9ce6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import type { FilterCondition } from '@next-woo-analytics/data'; + +/** + * Filter for order statuses relevant to payment tracking. + * + * The Orders API excludes pending/failed/cancelled by default + * (via woocommerce_excluded_report_order_statuses). The payment + * status widget needs pending orders for unpaid_net_sales, so we + * pass an explicit status filter to override the default exclusion. + * + * Includes statuses that represent the payment lifecycle: pending + * (unpaid), processing, on-hold, completed (paid), and refunded. + * Failed, cancelled, and checkout-draft are excluded because they + * don't represent meaningful payment states. + */ +export const PAYMENT_STATUS_FILTERS: FilterCondition[] = [ + { + key: 'status', + value: [ + 'wc-pending', + 'wc-processing', + 'wc-on-hold', + 'wc-completed', + 'wc-refunded', + ], + compare: 'IN', + }, +]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts new file mode 100644 index 000000000000..a86f4482d037 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import type { FilterCondition } from '@next-woo-analytics/data'; + +/** + * Product type filter constants for coupon-based widgets. + * + * These filters are used to segment coupon sales data by product type. + * Each widget instance should specify which filter to use based on + * the product category it targets. + * + * @see https://github.com/woocommerce/woocommerce-analytics/blob/develop/src/Utilities/OrderProductTypeTracker.php + */ + +/** + * Filter for physical products only. + * Includes: simple, variable, and variation product types. + * Excludes: digital/downloadable products and bookings. + */ +export const PHYSICAL_PRODUCTS_FILTER: FilterCondition = { + key: 'product_type', + value: [ 'simple', 'variable', 'variation' ], + compare: 'IN', +}; + +/** + * Filter for booking products only. + * Includes: booking, bookable-event, and bookable-service product types. + * Used by WooCommerce Bookings extension. + * + * @see OrderProductTypeTracker::BOOKINGS_TYPES + */ +export const BOOKINGS_FILTER: FilterCondition = { + key: 'product_type', + value: [ 'booking', 'bookable-event', 'bookable-service' ], + compare: 'IN', +}; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts new file mode 100644 index 000000000000..cd610ffa2c51 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts @@ -0,0 +1,59 @@ +/** + * Style configuration for a single segment. + */ +export type SegmentStyle = { + /** Segment fill color */ + color: string; +}; + +/** + * Item with optional color property. + */ +export type ColorableItem = { color?: string }; + +/** + * Segment data with optional color property. + */ +type SegmentData = { color?: string }; + +/** + * Resolves segment styles from either the explicit styles prop or chartData. + * Priority: styles prop > chartData[].color + * + * @param stylesProp - Explicit styles passed as component prop + * @param chartData - Chart data (may contain color per segment) + * @return Array of resolved styles, one per segment + */ +export function resolveSegmentStyles( + stylesProp: SegmentStyle[] | undefined, + chartData: SegmentData[] +): SegmentStyle[] { + if ( stylesProp?.length ) { + return stylesProp; + } + + return chartData.map( ( segment ) => ( { + color: segment.color ?? '', + } ) ); +} + +/** + * Applies resolved styles (colors) to an array of items. + * Works with any item type that has an optional color property. + * + * @param items - Array of items to style + * @param resolvedStyles - Styles to apply + * @return Items with styles applied + */ +export function applyStylesToItems< T extends ColorableItem >( + items: T[], + resolvedStyles: SegmentStyle[] +): T[] { + return items.map( ( item, index ) => { + const style = resolvedStyles[ index ] ?? resolvedStyles[ 0 ]; + return { + ...item, + color: style?.color || item.color, + }; + } ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/index.ts new file mode 100644 index 000000000000..6151d15e74ff --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/index.ts @@ -0,0 +1,4 @@ +export { useAttributesWithSearchFallback } from './use-attributes-with-search-fallback'; +export { useChartTheme, type WooChartTheme } from './use-chart-theme'; +export { useSeriesStyles } from './use-series-styles'; +export { useWidgetError } from './use-widget-error'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts new file mode 100644 index 000000000000..d12cbc20f53a --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { useSearch } from '@wordpress/route'; + +/** + * Internal dependencies + */ +import type { ReportParamsFieldAttributes } from '../fields'; + +/** + * Hook that provides widget attributes with URL search params as fallback. + * + * When attributes don't contain reportParams (empty or missing), this hook + * will attempt to get them from the URL using useSearch(). This is useful + * for dashboard widgets that can work in two contexts: + * - Dashboard-v2: No attributes, needs URL params + * - Post-Launch: Has attributes, ignores URL + * + * @param { Partial< ReportParamsFieldAttributes > } attributes - The widget attributes (may be empty or partial) + * @return { ReportParamsFieldAttributes } Effective attributes with reportParams guaranteed + * + * @example + * ```typescript + * function MyWidgetRender( { attributes } ) { + * const effectiveAttributes = useAttributesWithSearchFallback( attributes ); + * return ; + * } + * ``` + */ +export function useAttributesWithSearchFallback( + attributes: Partial< ReportParamsFieldAttributes > +): ReportParamsFieldAttributes { + /* + * Try to get search params from router. + * This may fail in contexts without router (e.g., Post-Launch). + * We declare the variable and use try/catch to handle both cases. + */ + let search: Record< string, any >; + + try { + // eslint-disable-next-line react-hooks/rules-of-hooks + search = useSearch( { + from: '/wc-analytics/dashboard', + } ); + } catch { + /* + * Not in router context or route doesn't exist. + * This can happen in Post-Launch where widgets are rendered + * outside the Analytics dashboard context. + */ + search = {}; + } + + /* + * Check if reportParams exists and is not empty. + * If it exists, use the provided attributes. + * Otherwise, build attributes from URL search params. + */ + const hasReportParams = + !! attributes?.reportParams && + Object.keys( attributes.reportParams ).length > 0; + + return hasReportParams + ? ( attributes as ReportParamsFieldAttributes ) + : { reportParams: search }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts new file mode 100644 index 000000000000..ef0c633c4c9d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { useColorPreference } from '@automattic/admin-toolkit'; +import type { ChartTheme } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import { WOO_COLORS } from '../constants'; + +/** + * Extended chart theme with WooCommerce-specific properties. + * Extends the base ChartTheme from @automattic/charts. + */ +export type WooChartTheme = ChartTheme & { + leaderboardChart: ChartTheme[ 'leaderboardChart' ] & { + barBorderRadius: string; + }; +}; + +export function useChartTheme(): WooChartTheme { + const { preferences } = useColorPreference(); + + return useMemo( () => { + // If the user is using a custom color theme, use colors generated from the design system accent + // color token, otherwise use the default Woo theme colors. + const colors = + preferences.interfaceTheme === 'custom' + ? [ '--wpds-color-fg-interactive-brand' ] + : WOO_COLORS; + + return { + backgroundColor: 'var(--wpds-color-bg-surface-neutral-strong)', + labelBackgroundColor: 'var(--wpds-color-bg-interactive-neutral)', + labelTextColor: 'var(--wpds-color-fg-interactive-neutral-strong)', + colors, + gridStyles: { + stroke: 'var(--wpds-color-stroke-surface-neutral)', + strokeWidth: 1, + }, + tickLength: 4, + gridColor: '', + gridColorDark: '', + svgLabelSmall: { + fill: 'var(--wpds-color-fg-content-neutral-weak)', + }, + xTickLineStyles: { stroke: '' }, + xAxisLineStyles: { + stroke: 'var(--wpds-color-stroke-surface-neutral)', + strokeWidth: 1, + }, + legend: { + labelStyles: { + fontSize: 'var(--wpds-font-size-sm)', + fontWeight: 400, + color: 'var(--wpds-color-fg-content-neutral)', + }, + containerStyles: { + rowGap: 'var( --wpds-dimension-padding-sm )', + columnGap: 'var( --wpds-dimension-padding-sm )', + }, + shapeStyles: [ + { + transform: 'translate(0, 1px)', + }, + { + transform: 'translate(0, 1px)', + strokeDasharray: '2, 2, 3, 2, 3, 2, 2', + }, + ], + }, + leaderboardChart: { + rowGap: 12, + columnGap: 4, + labelSpacing: 1.5, + barBorderRadius: 'var(--wpds-border-radius-md)', + deltaColors: [ + 'var(--wpds-color-fg-content-error-weak)', + 'var(--wpds-color-fg-content-neutral)', + 'var(--wpds-color-fg-content-success-weak)', + ] as [ string, string, string ], // [ negative, neutral, positive ] + }, + conversionFunnelChart: { + backgroundColor: 'var(--wpds-color-bg-surface-brand)', + positiveChangeColor: + 'var(--wpds-color-fg-content-success-weak)', + negativeChangeColor: 'var(--wpds-color-fg-content-error-weak)', + }, + lineChart: { + lineStyles: { + comparison: { + strokeDasharray: '4 4', + strokeWidth: 1.5, + strokeLinecap: 'square' as const, + strokeOpacity: 0.8, + strokeDashoffset: 2, + }, + }, + }, + seriesLineStyles: [ + { + strokeWidth: 2, + }, + { + strokeDasharray: '4 4', + strokeWidth: 1.5, + strokeLinecap: 'square' as const, + strokeDashoffset: 2, + }, + ], + }; + }, [ preferences.interfaceTheme ] ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts new file mode 100644 index 000000000000..f31d698130d2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { useGlobalChartsContext } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import type { + ComparativeLineChartSeries, + SeriesStyle, +} from '../components/chart-comparative-line/types'; + +/** + * Hook to build series styles from theme. + * Maps each chart series to its color and line styles from the theme provider. + * + * @param series - Array of chart series data + * @return Array of series styles with stroke color and line properties + * + * @example + * ```tsx + * const seriesStyles = useSeriesStyles( chartSeries ); + * return ; + * ``` + */ +export function useSeriesStyles( + series: ComparativeLineChartSeries[] +): SeriesStyle[] { + const { getElementStyles } = useGlobalChartsContext(); + + return useMemo( + () => + series.map( ( seriesData, index ) => { + const { color, lineStyles } = getElementStyles( { + data: seriesData, + index, + } ); + + return { + stroke: color, + ...lineStyles, + }; + } ), + [ series, getElementStyles ] + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts new file mode 100644 index 000000000000..49ac6e61753e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import { useEffect } from 'react'; +import { __ } from '@wordpress/i18n'; +import { useGlobalError } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../components/widget-root'; + +/** + * Hook to report widget errors to the dashboard's error boundary. + * + * This hook manages the error lifecycle: + * - When an error occurs, it logs the error and reports to the dashboard via setError + * - When the error clears, it clears the error state + * - Provides a retry action that clears the error and refetches data + * - Cleans up error state when the widget unmounts + * + * @param isError - Whether the widget is in an error state + * @param error - The error object (used for logging) + * @param refetch - Function to refetch the data (for retry action) + * + * @return true if widget is in error state, false otherwise + * + * @example + * ```tsx + * function MyWidget() { + * const { isError, error, refetch } = useMyData(); + * const hasError = useWidgetError( isError, error, refetch ); + * + * if ( hasError ) { + * return null; // Dashboard shows error UI via WidgetErrorBoundary + * } + * + * return
Widget content
; + * } + * ``` + */ +export function useWidgetError( + isError: boolean, + error: Error | null | undefined, + refetch?: () => void +): boolean { + const { setError } = useWidgetRootContext(); + const { isGlobalError } = useGlobalError(); + + useEffect( () => { + if ( ! isError ) { + setError?.( null ); + return; + } + + if ( ! setError ) { + // Fallback: Log when setError is unavailable (widget outside dashboard context) + // eslint-disable-next-line no-console + console.warn( + '[useWidgetError] setError is undefined - error UI cannot be displayed' + ); + return; + } + + if ( isGlobalError ) { + // Global error: show illustration only + setError( { + message: '', + } ); + return; + } + + // Log error for debugging - captures API errors, network failures, etc. + if ( error ) { + // eslint-disable-next-line no-console + console.error( '[Widget Error]', error.message, error ); + } + + // Widget-specific error: show message + retry + setError( { + message: __( + "We couldn't load this data. Please try again in a moment.", + 'woocommerce-analytics' + ), + action: { + label: __( 'Retry', 'woocommerce-analytics' ), + onClick: () => { + setError?.( null ); + refetch?.(); + }, + }, + } ); + + // No cleanup function needed: error UI is shown by WidgetErrorBoundary, which unmounts this widget. + // Calling setError(null) in a cleanup would wrongly clear the error. + // Error state is handled and cleared by SingleDashboardWidget as needed. + }, [ isError, error, isGlobalError, setError, refetch ] ); + + return isError; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts new file mode 100644 index 000000000000..23bc55e34138 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts @@ -0,0 +1,106 @@ +/** + * Components + */ +export { + MetricDelta, + MetricWithComparison, + ComparativeLineChart, + Legend, + WidgetRoot, + useWidgetRootContext, + type LegendItem, + type SeriesStyle, + LeaderboardChart, + type LeaderboardChartProps, + type LeaderboardChartData, + type LegendLabels, + LeaderboardLabel, + type LeaderboardLabelProps, + BarChart, + type BarChartProps, + type BarChartData, + type BarChartStyle, +} from './components'; + +/** + * Constants + */ +export { WOO_COLORS, COLOR_GRAY_100 } from './constants'; + +/** + * Widget edit fields + */ +export { + ReportParamsField, + type ReportParamsFieldAttributes, + MetricsField, + DEFAULT_METRICS, +} from './fields'; + +/** + * Helpers and utilities + */ +export { + formatOrderMetric, + getFormatByMetricKey, + buildTimeSeriesChartData, + type TimeSeriesData, + calculateDelta, + BOOKINGS_FILTER, + PHYSICAL_PRODUCTS_FILTER, + FULFILLED_ORDERS_FILTER, + UNFULFILLED_ORDERS_FILTER, + PAYMENT_STATUS_FILTERS, +} from './helpers'; + +/** + * Hooks + */ +export { + useAttributesWithSearchFallback, + useChartTheme, + useSeriesStyles, + useWidgetError, +} from './hooks'; + +/** + * Widget components + */ +export { + BookingOrderMetricWidget, + BookingsByAttendanceWidget, + BookingsRevenueByCustomerTypeWidget, + BookingConversionRateWidget, + ConversionRateWidget, + CouponUseWidget, + MetricComparisonWidget, + RevenueByCustomerTypeWidget, + NewVsReturningCustomerWidget, + OrderMetricWidget, + PaymentStatusWidget, + OrdersFulfillmentWidget, + SalesByCouponWidget, + TotalReturnsWidget, + VisitorMetricWidget, + VisitorsByLocationWidget, + SalesByDeviceWidget, + BookingsByDeviceWidget, + SessionsByDeviceWidget, + SalesByUtmWidget, + TopPerformingProductLeaderboardWidget, + type TopPerformingProductLeaderboardWidgetProps, + TopPerformingProductsWidget, + type TopPerformingProductsWidgetProps, + TopPerformingBookingsWidget, + type TopPerformingBookingsWidgetProps, +} from './widgets'; + +/** + * Types + */ +export type { + OrderMetricKey, + OrderMetrics, + OrdersSummary, + DataFormat, +} from './types'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss new file mode 100644 index 000000000000..22cd83d78d04 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss @@ -0,0 +1,70 @@ +/** + * Widget Container Queries + * + * Provides CSS Container Query support for responsive widgets. + * Breakpoints aligned with Tailwind defaults for consistency with Design System. + * + * @see https://linear.app/a8c/issue/ARC-464/design-system-wide-responsiveness + * @see https://tailwindcss.com/docs/responsive-design#container-size-reference + * + * Usage: + * ```scss + * @use '../styles/widget-container' as *; + * + * .myWidget { + * flex-direction: column; // Mobile-first default + * + * @include widget-query( md ) { + * flex-direction: row; // >= 448px + * } + * } + * ``` + */ + +// Container Query Breakpoints (Tailwind-aligned) +// These are for element-based queries, not viewport +$widget-breakpoints: ( + xxs: 16rem, // 256px - Extra extra small widgets + xs: 20rem, // 320px - Extra small widgets + sm: 24rem, // 384px - Small widgets + md: 28rem, // 448px - Medium widgets (common tile size) + lg: 32rem, // 512px - Large widgets + xl: 36rem, // 576px - Extra large widgets + 2xl: 42rem, // 672px - Full-width widgets +) !default; + +/** + * Container query mixin for widgets. + * + * @param {string} $breakpoint - Breakpoint name (xs, sm, md, lg, xl, 2xl) + * @param {string} $type - Query type (min-width or max-width), default: min-width + * + * @example + * // Min-width query (mobile-first) + * @include widget-query( md ) { ... } + * + * // Max-width query + * @include widget-query( sm, max-width ) { ... } + */ +@mixin widget-query( $breakpoint, $type: min-width ) { + $size: map-get( $widget-breakpoints, $breakpoint ); + + @if not $size { + @error "Unknown breakpoint: #{$breakpoint}. Valid: xs, sm, md, lg, xl, 2xl"; + } + + @container widget ( #{ $type }: #{ $size } ) { + @content; + } +} + +/** + * Widget container base class. + * Apply to the widget wrapper element to enable container queries. + */ +.widgetContainer { + container-type: inline-size; + container-name: widget; + width: 100%; + height: 100%; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts new file mode 100644 index 000000000000..2c99e25089fe --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { formatMetricValue } from '@wc-analytics/formatters'; +import type { ReportDataMap } from '@next-woo-analytics/data'; + +export type OrdersSummary = ReportDataMap[ 'orders' ][ 'summary' ]; + +export type OrderMetrics = Pick< + OrdersSummary, + | 'orders_no' + | 'total_sales' + | 'average_order_value' + | 'avg_items' + | 'orders_value_net' + | 'orders_value_gross' + | 'coupons' + | 'profit_margin' +>; + +export type OrderMetricKey = keyof OrderMetrics; + +type BookingsSummary = ReportDataMap[ 'bookings' ][ 'summary' ]; + +type BookingMetrics = Pick< + BookingsSummary, + | 'status_unpaid' + | 'status_pending_confirmation' + | 'status_confirmed' + | 'status_paid' + | 'status_cancelled' + | 'status_complete' + | 'attendance_status_booked' + | 'attendance_status_no_show' + | 'attendance_status_checked_in' +>; + +export type BookingMetricKey = keyof BookingMetrics; + +export type VisitorsMetricKey = 'visitors'; + +export type ConversionMetricKey = 'conversion_rate'; + +export type CustomersMetricKey = 'customers'; + +export type MetricKey = + | OrderMetricKey + | BookingMetricKey + | VisitorsMetricKey + | ConversionMetricKey + | CustomersMetricKey; + +/* + * Inferred types + */ +type MetricFormat = NonNullable< Parameters< typeof formatMetricValue >[ 1 ] >; + +type FormatMetricValueOptions = NonNullable< + Parameters< typeof formatMetricValue >[ 2 ] +>; + +export type DataFormat = { + type: MetricFormat; + options?: FormatMetricValueOptions; +}; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md new file mode 100644 index 000000000000..8053de2e3983 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md @@ -0,0 +1,43 @@ +# Widgets + +Dashboard widget components for WooCommerce Analytics. + +## Available Widgets + +| Widget | Chart Component | Description | +| -------------------------------- | ----------------------------------------------- | ------------------------------------------------- | +| `ConversionRateWidget` | `MetricWithComparison` | Funnel conversion rate metric | +| `MetricComparisonWidget` | `MetricWithComparison` + `ComparativeLineChart` | Generic metric with time series | +| `RevenueByCustomerTypeWidget` | `BarChart` | Revenue breakdown by customer type | +| `NewVsReturningCustomerWidget` | `DonutChart` | Customer counts by new vs returning | +| `OrderMetricWidget` | `ReportMetricWidget` | Order-based metrics (revenue, orders, AOV) | +| `SalesByCouponWidget` | `SemiCircleChart` | Coupon sales for all product types | +| `SalesByDeviceWidget` | `DonutChart` | Sales breakdown by device type | +| `SalesByUtmWidget` | `LeaderboardChart` | Sales by UTM parameters (source/channel/campaign) | +| `TotalReturnsWidget` | `DonutChart` | Returns/refunds for all product types | +| `VisitorMetricWidget` | `ReportMetricWidget` | Visitor-based metrics | +| `TopPerformingProductsWidget` | `LeaderboardChart` | Top products by revenue | +| `TopPerformingBookingsWidget` | `LeaderboardChart` | Top bookings by revenue | + +## Chart Components + +| Component | Type | Use Case | +| ---------------------- | ----------- | ----------------------------------- | +| `DonutChart` | Pie/Donut | Category breakdowns (2-4 segments) | +| `SemiCircleChart` | Half-pie | Top N rankings with "Other" segment | +| `ComparativeLineChart` | Line | Time series with comparison periods | +| `MetricWithComparison` | Metric | Single value with delta indicator | +| `ReportMetricWidget` | Metric | Report-based metrics with sparkline | +| `LeaderboardChart` | Leaderboard | Top N items with bars and labels | + +## Common Utilities + +Shared code is located in `common/`: + +### Styles + +- `donut-widget.module.scss` - Container styles for DonutChart widgets + +### Hooks + +- `useSegmentStyles( chartData )` - Builds segment colors from theme provider diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx new file mode 100644 index 000000000000..065d6871d14e --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx @@ -0,0 +1,97 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportBookings } from '@next-woo-analytics/data'; +import { calendar } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { DonutChart } from '../../components'; +import { buildBookingsByAttendanceData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useSegmentStyles } from '../common'; +import styles from '../common/donut-widget.module.scss'; + +/** + * Bookings by Status Widget Component + * + * Displays a donut chart showing bookings breakdown by status. + * Shows the total bookings count in the center with a breakdown in the legend. + * + * Statuses include: Booked, Checked In, No Show, and Cancelled. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function BookingsByAttendanceWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportBookings( reportParams ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData, total, comparisonTotal, legendData } = useMemo( + () => buildBookingsByAttendanceData( primary.data, comparison.data ), + [ primary.data, comparison.data ] + ); + + const segmentStyles = useSegmentStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/index.ts new file mode 100644 index 000000000000..3215a0b51402 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/index.ts @@ -0,0 +1 @@ +export { BookingsByAttendanceWidget } from './bookings-by-attendance-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/donut-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/donut-widget.module.scss new file mode 100644 index 000000000000..3c4b0cbde9be --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/donut-widget.module.scss @@ -0,0 +1,10 @@ +/** + * Shared styles for DonutChart-based widgets. + * Used by: CouponUseWidget, PaymentStatusWidget, BookingsByAttendanceWidget + */ +.container { + min-width: 120px; + max-width: 240px; + height: 100%; + margin: 0 auto; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/index.ts new file mode 100644 index 000000000000..94c7a827b260 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/index.ts @@ -0,0 +1,2 @@ +export { useSegmentStyles } from './use-segment-styles'; +export { useBarStyles } from './use-bar-styles'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts new file mode 100644 index 000000000000..b5be75706600 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { useGlobalChartsContext } from '@automattic/charts'; +import type { SeriesData } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import type { BarChartStyle } from '../../components'; + +/** + * Hook to build bar chart styles from theme. + * Maps each series to its color from the theme provider. + * + * @param chartData - Array of series data (SeriesData[]) + * @return Array of bar styles with stroke color for each series + * + * @example + * ```tsx + * const barStyles = useBarStyles( chartData ); + * return ; + * ``` + */ +export function useBarStyles( chartData: SeriesData[] ): BarChartStyle[] { + const { getElementStyles } = useGlobalChartsContext(); + + return useMemo( + () => + chartData.map( ( seriesData, index ) => { + const { color } = getElementStyles( { + data: seriesData, + index, + } ); + + return { stroke: color }; + } ), + [ chartData, getElementStyles ] + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts new file mode 100644 index 000000000000..f876a5207a71 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { useGlobalChartsContext } from '@automattic/charts'; + +/** + * Internal dependencies + */ +import type { SegmentStyle } from '../../helpers'; + +type ChartSegment = { + label: string; + value: number; + percentage?: number; +}; + +/** + * Hook to build segment styles from theme. + * Maps each chart segment to its color from the theme provider. + * + * @param chartData - Array of chart segments with label and value + * @return Array of segment styles with color for each segment + * + * @example + * ```tsx + * const segmentStyles = useSegmentStyles( chartData ); + * return ; + * ``` + */ +export function useSegmentStyles( chartData: ChartSegment[] ): SegmentStyle[] { + const { getElementStyles } = useGlobalChartsContext(); + + return useMemo( + () => + chartData.map( ( segment, index ) => { + const { color } = getElementStyles( { + data: { + ...segment, + group: segment.label, // Use label as group for stable color assignment + } as Parameters< typeof getElementStyles >[ 0 ][ 'data' ], + index, + } ); + + return { color }; + } ), + [ chartData, getElementStyles ] + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss new file mode 100644 index 000000000000..a37cb6c1d6f7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss @@ -0,0 +1,11 @@ +.container { + height: 100%; + min-height: 0; +} + +.conversionFunnelChart { + --funnel-font-family: var( --wpds-font-family-body ); + --step-font-family: var( --wpds-font-family-body ); + flex: 1; + min-height: 0; +} \ No newline at end of file diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx new file mode 100644 index 000000000000..fe7faddec0ac --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { ConversionFunnelChart } from '@automattic/charts'; +import { Icon, Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { + FilterCondition, + useReportConversionRate, +} from '@next-woo-analytics/data'; +import { goal } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { MetricWithComparison, ChartEmptyState } from '../../components'; +import { BOOKINGS_FILTER } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import styles from './conversion-rate-widget.module.scss'; + +/** + * ConversionRateWidget Component + * + * Displays a conversion funnel visualization showing the path from + * visitors to completed orders. Shows steps with conversion percentages + * and comparison delta when available. + */ +export function ConversionRateWidget( { + filters = [], + emptyStateIcon = goal, + emptyStateText, +}: { + filters?: FilterCondition[]; + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; + emptyStateText?: string; +} ) { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportConversionRate( { + ...reportParams, + filters, + } ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { data: conversionData } = primary; + const { data: comparisonData } = comparison; + + const { steps, overallRate, comparisonRate } = useMemo( () => { + if ( + ! conversionData || + conversionData.summary.active_sessions === 0 + ) { + return { + steps: [], + overallRate: 0, + comparisonRate: null, + }; + } + + return { + steps: conversionData.steps || [], + // overallRate is a decimal (e.g., 0.0476 for 4.76%) + overallRate: conversionData.overallRate || 0, + // Get comparison rate as decimal + comparisonRate: + hasComparison && comparisonData?.summary + ? comparisonData.summary.conversion_rate + : null, + }; + }, [ conversionData, comparisonData, hasComparison ] ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + // Don't render if no steps data + if ( steps.length === 0 ) { + return ( + <> + + { isRefetching && } + + ); + } + + // Convert to percentage for ConversionFunnelChart (expects 0-100 scale) + const overallRatePercent = overallRate * 100; + + return ( + <> + + + + null } + className={ styles.conversionFunnelChart } + /> + + { isRefetching && } + + ); +} + +/** + * Booking Conversion Rate Widget Component + * + * A widget that displays a conversion funnel visualization showing the path from + * visitors to completed orders for booking products only. + * + * This component automatically filters data to show only booking product types + * (booking, bookable-event, bookable-service). + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function BookingConversionRateWidget() { + return ; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts new file mode 100644 index 000000000000..47e5a57dd17c --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts @@ -0,0 +1,4 @@ +export { + ConversionRateWidget, + BookingConversionRateWidget, +} from './conversion-rate-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx new file mode 100644 index 000000000000..1637d24857b1 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx @@ -0,0 +1,95 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportCouponsByDate } from '@next-woo-analytics/data'; +import { coupon } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { DonutChart } from '../../components'; +import { buildCouponUseData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useSegmentStyles } from '../common'; +import styles from '../common/donut-widget.module.scss'; + +/** + * Coupon Use Widget Component + * + * Displays a donut chart showing total sales with a coupon vs net sales breakdown. + * Shows the total sales in the center with slices in the legend. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function CouponUseWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportCouponsByDate( reportParams ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData, total, comparisonTotal, legendData } = useMemo( + () => + buildCouponUseData( primary.data, comparison.data, hasComparison ), + [ primary.data, comparison.data, hasComparison ] + ); + + const segmentStyles = useSegmentStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/index.ts new file mode 100644 index 000000000000..cb365cd60944 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/index.ts @@ -0,0 +1 @@ +export { CouponUseWidget } from './coupon-use-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/index.ts new file mode 100644 index 000000000000..eb6f788c08a4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/index.ts @@ -0,0 +1,30 @@ +export { MetricComparisonWidget } from './metric-comparison'; +export { OrderMetricWidget, BookingOrderMetricWidget } from './order-metric'; +export { VisitorMetricWidget } from './visitor-metric'; +export { SalesByCouponWidget } from './sales-by-coupon'; +export { + ConversionRateWidget, + BookingConversionRateWidget, +} from './conversion-rate'; +export { + RevenueByCustomerTypeWidget, + BookingsRevenueByCustomerTypeWidget, +} from './revenue-by-customer-type'; +export { NewVsReturningCustomerWidget } from './new-vs-returning-customer'; +export { SalesByDeviceWidget, BookingsByDeviceWidget } from './sales-by-device'; +export { SessionsByDeviceWidget } from './sessions-by-device'; +export { BookingsByAttendanceWidget } from './bookings-by-attendance'; +export { TotalReturnsWidget } from './total-returns'; +export { SalesByUtmWidget } from './sales-by-utm'; +export { + TopPerformingProductLeaderboardWidget, + type TopPerformingProductLeaderboardWidgetProps, + TopPerformingProductsWidget, + type TopPerformingProductsWidgetProps, + TopPerformingBookingsWidget, + type TopPerformingBookingsWidgetProps, +} from './product-leaderboard'; +export { CouponUseWidget } from './coupon-use'; +export { PaymentStatusWidget } from './payment-status'; +export { OrdersFulfillmentWidget } from './orders-fulfillment'; +export { VisitorsByLocationWidget } from './visitors-by-location'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/index.ts new file mode 100644 index 000000000000..2a1f751be5f3 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/index.ts @@ -0,0 +1 @@ +export { MetricComparisonWidget } from './metric-comparison-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.module.scss new file mode 100644 index 000000000000..8c2e3cfd586a --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.module.scss @@ -0,0 +1,4 @@ +.container { + height: 100%; + min-height: 0; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx new file mode 100644 index 000000000000..042cebaf6741 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import { MetricWithComparison, ComparativeLineChart } from '../../components'; +import type { DataFormat } from '../../types'; +import type { + ComparativeLineChartSeries, + SeriesStyle, +} from '../../components/chart-comparative-line/types'; +import styles from './metric-comparison-widget.module.scss'; + +export type MetricComparisonWidgetProps = { + /** + * Primary metric value + */ + value: number; + + /** + * Optional comparison metric (previous period, target, etc.) + */ + comparisonValue?: number | null; + + /** + * Chart display props + */ + series: ComparativeLineChartSeries[]; + + /** + * Explicit styles for chart series. When provided, takes priority + * over styles defined in series[].options. + */ + seriesStyles?: SeriesStyle[]; + + dataFormat: DataFormat; + tickFormat?: string; +}; + +export function MetricComparisonWidget( { + value, + comparisonValue, + series, + seriesStyles, + dataFormat, + tickFormat, +}: MetricComparisonWidgetProps ) { + return ( + + + + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/index.ts new file mode 100644 index 000000000000..67942ea055f2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/index.ts @@ -0,0 +1 @@ +export { NewVsReturningCustomerWidget } from './new-vs-returning-customer-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx new file mode 100644 index 000000000000..186ac05335ee --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportCustomersByDate } from '@next-woo-analytics/data'; +import { customer } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { DonutChart } from '../../components'; +import { buildNewVsReturningCustomerData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useSegmentStyles } from '../common'; +import styles from '../common/donut-widget.module.scss'; + +/** + * New vs Returning Customer Widget Component + * + * Displays a donut chart showing the breakdown of unique customers + * by type (new vs returning) over the selected time period. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function NewVsReturningCustomerWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportCustomersByDate( reportParams ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData, total, comparisonTotal, legendData } = useMemo( + () => + buildNewVsReturningCustomerData( + primary.data, + comparison.data, + hasComparison + ), + [ primary.data, comparison.data, hasComparison ] + ); + + const segmentStyles = useSegmentStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx new file mode 100644 index 000000000000..3b069ca6dc73 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { useReportOrders } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { ReportMetricWidget } from '../../components/report-metric'; +import { useWidgetRootContext } from '../../components/widget-root'; +import { getFormatByMetricKey, BOOKINGS_FILTER } from '../../helpers'; +import type { OrderMetricKey } from '../../types'; + +export type BookingOrderMetricWidgetProps = { + /** + * The metric key to display from the data + */ + metricKey: OrderMetricKey; +}; + +/** + * Booking Order Metric Widget Component + * + * A widget that displays booking order-related metrics over time with comparison support. + * This component automatically filters data to show only booking product types + * (booking, bookable-event, bookable-service). + * + * This component must be used within a WidgetRoot which provides reportParams + * via context. + * + * @param {Object} props - Component props + * @param {OrderMetricKey} props.metricKey - The metric key to display + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function BookingOrderMetricWidget( { + metricKey, +}: BookingOrderMetricWidgetProps ) { + const { reportParams } = useWidgetRootContext(); + + return ( + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/index.ts new file mode 100644 index 000000000000..b12b5ef51038 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/index.ts @@ -0,0 +1,2 @@ +export { OrderMetricWidget } from './widget-order-metric'; +export { BookingOrderMetricWidget } from './booking-order-metric-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx new file mode 100644 index 000000000000..80851426feed --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { useReportOrders } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { ReportMetricWidget } from '../../components/report-metric'; +import { useWidgetRootContext } from '../../components/widget-root'; +import { getFormatByMetricKey } from '../../helpers'; +import type { OrderMetricKey } from '../../types'; + +export type OrderMetricWidgetProps = { + /** + * The metric key to display from the data + */ + metricKey: OrderMetricKey; +}; + +/** + * Order Metric Widget Component + * + * A widget that displays order-related metrics over time with comparison support. + * This component must be used within a WidgetRoot which provides reportParams + * via context. + * + * @param {Object} props - Component props + * @param {OrderMetricKey} props.metricKey - The metric key to display + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function OrderMetricWidget( { metricKey }: OrderMetricWidgetProps ) { + const { reportParams } = useWidgetRootContext(); + + return ( + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/index.ts new file mode 100644 index 000000000000..108292d321dd --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/index.ts @@ -0,0 +1 @@ +export { OrdersFulfillmentWidget } from './orders-fulfillment-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx new file mode 100644 index 000000000000..92da7775219b --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx @@ -0,0 +1,131 @@ +/** + * External dependencies + */ +import { useMemo, useCallback } from 'react'; +import { Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportOrders } from '@next-woo-analytics/data'; +import { reports } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { DonutChart } from '../../components'; +import { + buildOrdersFulfillmentData, + FULFILLED_ORDERS_FILTER, + UNFULFILLED_ORDERS_FILTER, +} from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useSegmentStyles } from '../common'; +import styles from '../common/donut-widget.module.scss'; + +/** + * Orders Fulfillment Widget Component + * + * Displays a donut chart showing the breakdown of fulfilled vs unfulfilled + * order counts over the selected time period. + * + * Makes two separate API calls with different fulfillment status filters + * since fulfillment data is not pre-aggregated in the orders summary. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function OrdersFulfillmentWidget() { + const { reportParams } = useWidgetRootContext(); + + const fulfilled = useReportOrders( { + ...reportParams, + filters: [ FULFILLED_ORDERS_FILTER ], + } ); + + const unfulfilled = useReportOrders( { + ...reportParams, + filters: [ UNFULFILLED_ORDERS_FILTER ], + } ); + + const isLoading = fulfilled.isLoading || unfulfilled.isLoading; + const isFetching = fulfilled.isFetching || unfulfilled.isFetching; + const hasData = fulfilled.hasData && unfulfilled.hasData; + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData, total, comparisonTotal, legendData } = useMemo( + () => + isLoading + ? { + chartData: [], + total: 0, + comparisonTotal: 0, + legendData: [], + } + : buildOrdersFulfillmentData( + fulfilled.primary.data, + unfulfilled.primary.data, + fulfilled.comparison.data, + unfulfilled.comparison.data + ), + [ + isLoading, + fulfilled.primary.data, + unfulfilled.primary.data, + fulfilled.comparison.data, + unfulfilled.comparison.data, + ] + ); + + const segmentStyles = useSegmentStyles( chartData ); + const hasComparison = fulfilled.hasComparison; + + const isError = fulfilled.isError || unfulfilled.isError; + const error = fulfilled.error ?? unfulfilled.error; + const fulfilledRefetch = fulfilled.refetch; + const unfulfilledRefetch = unfulfilled.refetch; + const refetch = useCallback( async () => { + await Promise.all( [ fulfilledRefetch(), unfulfilledRefetch() ] ); + }, [ fulfilledRefetch, unfulfilledRefetch ] ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/index.ts new file mode 100644 index 000000000000..81f8a17dc1df --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/index.ts @@ -0,0 +1 @@ +export { PaymentStatusWidget } from './payment-status-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx new file mode 100644 index 000000000000..8eb87ad4b725 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportOrders } from '@next-woo-analytics/data'; +import { payment } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { DonutChart } from '../../components'; +import { buildPaymentStatusData, PAYMENT_STATUS_FILTERS } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useSegmentStyles } from '../common'; +import styles from '../common/donut-widget.module.scss'; + +/** + * Payment Status Widget Component + * + * Displays a donut chart comparing revenue from paid orders vs unpaid orders. + * Shows the total revenue in the center with a breakdown in the legend. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function PaymentStatusWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportOrders( { + ...reportParams, + filters: PAYMENT_STATUS_FILTERS, + } ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData, total, comparisonTotal, legendData } = useMemo( + () => buildPaymentStatusData( primary.data, comparison.data ), + [ primary.data, comparison.data ] + ); + + const segmentStyles = useSegmentStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/index.ts new file mode 100644 index 000000000000..599126e19cea --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/index.ts @@ -0,0 +1,12 @@ +export { + TopPerformingProductLeaderboardWidget, + type TopPerformingProductLeaderboardWidgetProps, +} from './top-performing-product-leaderboard-widget'; +export { + TopPerformingProductsWidget, + type TopPerformingProductsWidgetProps, +} from './top-performing-products-widget'; +export { + TopPerformingBookingsWidget, + type TopPerformingBookingsWidgetProps, +} from './top-performing-bookings-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx new file mode 100644 index 000000000000..9d2b451b4583 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { calendar } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { TopPerformingProductLeaderboardWidget } from './top-performing-product-leaderboard-widget'; +import { BOOKINGS_FILTER } from '../../helpers'; + +export type TopPerformingBookingsWidgetProps = { + /** + * Maximum number of bookings to display + */ + limit?: number; +}; + +/** + * Top Performing Bookings Widget + * + * Displays the top-performing booking products by net revenue in a leaderboard format. + * Shows product images, names, and revenue with comparison to previous period. + * + * Filters to: booking, bookable-event, and bookable-service product types. + * + * Features: + * - Automatic booking product data fetching + * - Product image loading + * - Revenue-based ranking + * - Comparison support + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @param props - Component props + * @param props.limit - Maximum number of bookings to display (default: 5) + * + * @example + * + * + * + */ +export function TopPerformingBookingsWidget( { + limit = 5, +}: TopPerformingBookingsWidgetProps ) { + return ( + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx new file mode 100644 index 000000000000..0d6a8550e992 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx @@ -0,0 +1,221 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { + useReportProducts, + useProductImages, + type FilterCondition, +} from '@next-woo-analytics/data'; +import { Icon } from '@wordpress/ui'; +import { productBlouse } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { + LeaderboardChart, + LeaderboardLabel, +} from '../../components/chart-leaderboard'; +import { formatLegendLabels, calculateDelta } from '../../helpers'; +import { useWidgetError } from '../../hooks'; + +export type TopPerformingProductLeaderboardWidgetProps = { + /** + * Maximum number of products to display + */ + limit?: number; + + /** + * Optional product type filter to apply when fetching product data. + * + * When provided, filters results to specific product types (e.g., bookings only). + * When omitted, shows data for all product types. + */ + filter?: FilterCondition; + + /** + * Icon to display in the empty state. + * Defaults to productBlouse icon. + */ + emptyStateIcon?: React.ComponentProps< typeof Icon >[ 'icon' ]; +}; + +/** + * Top Performing Product Leaderboard Widget + * + * Displays top-performing products by net revenue in a leaderboard format. + * Shows product images, names, and revenue with comparison to previous period. + * + * This is a reusable component that can be used for any product-based leaderboard + * (regular products, bookings, etc.). + * + * Features: + * - Automatic product data fetching + * - Product image loading + * - Revenue-based ranking + * - Comparison support + * - Product type filtering + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @param props - Component props + * @param props.limit - Maximum number of products to display (default: 5) + * @param props.filter - Optional product type filter + * @param props.emptyStateIcon - Icon to display in empty state (default: productBlouse) + * + * @example + * // All product types + * + * + * + * + * @example + * // Bookings only + * + * + * + */ +export function TopPerformingProductLeaderboardWidget( { + limit = 5, + filter, + emptyStateIcon = productBlouse, +}: TopPerformingProductLeaderboardWidgetProps ) { + const { reportParams } = useWidgetRootContext(); + + const params = useMemo( + () => ( { + ...reportParams, + ...( filter && { filters: [ filter ] } ), + } ), + [ reportParams, filter ] + ); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportProducts( params, limit ); + + const { data } = primary; + const { data: comparisonData } = comparison; + + // Extract product IDs for fetching images + const productIds = useMemo( + () => data?.data?.map( ( item ) => item.product_id ) || [], + [ data?.data ] + ); + + // Fetch product images + const { data: productImages, isLoading: imagesLoading } = useProductImages( + { + productIds, + } + ); + + const isInitialLoading = ( isLoading || imagesLoading ) && ! hasData; + const isRefetching = ( isFetching || imagesLoading ) && hasData; + + const chartData = useMemo( () => { + const comparisonItems = comparisonData?.data || []; + + // Create a map of product_id to comparison data for efficient lookup + const comparisonMap = new Map( + comparisonItems.map( ( item ) => [ item.product_id, item ] ) + ); + + // Calculate maxValue once outside the map + const maxCurrentValue = Math.max( + ...( data?.data?.map( ( p ) => p.product_net_revenue ?? 0 ) || [] ), + 1 // Prevent division by zero + ); + + // Calculate max previous value once outside the map + const maxPreviousValue = Math.max( + ...comparisonItems.map( ( p ) => p.product_net_revenue ?? 0 ), + 1 // Prevent division by zero + ); + + return ( + data?.data?.map( ( product, index: number ) => { + const currentValue = product.product_net_revenue ?? 0; + + const productImage = productImages + ? productImages[ product.product_id ] + : undefined; + + // Match by product_id instead of index + const comparisonProduct = comparisonMap.get( + product.product_id + ); + const previousValue = + comparisonProduct?.product_net_revenue ?? 0; + + const previousShare = + comparisonItems.length > 0 && previousValue > 0 + ? ( previousValue / maxPreviousValue ) * 100 + : 0; + + const label = product.product_name; + const imageUrl = productImage?.imageUrl || ''; + const imageAlt = productImage?.imageAlt || label; + const delta = calculateDelta( currentValue, previousValue ); + + return { + id: String( product.product_id || index ), + label: ( + + ), + currentValue, + currentShare: ( currentValue / maxCurrentValue ) * 100, + previousValue, + previousShare, + delta, + }; + } ) || [] + ); + }, [ data?.data, comparisonData?.data, productImages ] ); + + const legendLabels = useMemo( + () => formatLegendLabels( reportParams ), + [ reportParams ] + ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx new file mode 100644 index 000000000000..2aeec116eaf0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import { TopPerformingProductLeaderboardWidget } from './top-performing-product-leaderboard-widget'; +import { PHYSICAL_PRODUCTS_FILTER } from '../../helpers'; + +export type TopPerformingProductsWidgetProps = { + /** + * Maximum number of products to display + */ + limit?: number; +}; + +/** + * Top Performing Products Widget + * + * Displays the top-performing physical products by net revenue in a leaderboard format. + * Shows product images, names, and revenue with comparison to previous period. + * + * Filters to: simple, variable, and variation product types (physical products only). + * + * Features: + * - Automatic product data fetching + * - Product image loading + * - Revenue-based ranking + * - Comparison support + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @param props - Component props + * @param props.limit - Maximum number of products to display (default: 5) + * + * @example + * + * + * + */ +export function TopPerformingProductsWidget( { + limit = 5, +}: TopPerformingProductsWidgetProps ) { + return ( + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/index.ts new file mode 100644 index 000000000000..5e8082e1ab2d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/index.ts @@ -0,0 +1,4 @@ +export { + RevenueByCustomerTypeWidget, + BookingsRevenueByCustomerTypeWidget, +} from './revenue-by-customer-type-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx new file mode 100644 index 000000000000..6df1e1d779c8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -0,0 +1,124 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { + useReportCustomers, + type FilterCondition, +} from '@next-woo-analytics/data'; +import { customer } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { BarChart } from '../../components'; +import { buildRevenueByCustomerTypeData, BOOKINGS_FILTER } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useBarStyles } from '../common'; + +type CustomerTypeRevenueWidgetProps = { + /** + * Optional product type filter to apply when fetching customer data. + * If not provided, will show data for all product types. + * + * @see PHYSICAL_PRODUCTS_FILTER for physical goods (simple, variable, variation) + * @see BOOKINGS_FILTER for booking products (booking, bookable-event, bookable-service) + */ + filter?: FilterCondition; +}; + +/** + * Customer Type Revenue Widget Component + * + * Displays a bar chart comparing revenue from new customers vs returning customers. + * Optionally supports filtering by product type. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +function CustomerTypeRevenueWidget( { + filter, +}: CustomerTypeRevenueWidgetProps ) { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportCustomers( { + ...reportParams, + filters: filter ? [ filter ] : undefined, + } ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData } = useMemo( + () => + buildRevenueByCustomerTypeData( + primary.data, + comparison.data, + reportParams + ), + [ primary.data, comparison.data, reportParams ] + ); + + const barStyles = useBarStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + { isRefetching && } + + ); +} + +/** + * Revenue by Customer Type Widget + * + * Displays customer revenue data for all product types. + * No product type filtering applied. + */ +export function RevenueByCustomerTypeWidget() { + return ; +} + +/** + * Bookings Revenue by Customer Type Widget + * + * Displays customer revenue data for booking products only. + * Filters to: booking, bookable-event, and bookable-service product types. + */ +export function BookingsRevenueByCustomerTypeWidget() { + return ; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/index.ts new file mode 100644 index 000000000000..231977195cfc --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/index.ts @@ -0,0 +1 @@ +export { SalesByCouponWidget } from './sales-by-coupon-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx new file mode 100644 index 000000000000..e82e410e4cd0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -0,0 +1,87 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportCoupons } from '@next-woo-analytics/data'; +import { coupon } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { BarChart } from '../../components'; +import { buildSalesByCouponData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useBarStyles } from '../common'; + +/** + * Sales by Coupon Widget Component + * + * Displays a bar chart showing coupon discount distribution. + * Shows top 3 coupons plus "Other" segment. + * Displays data for all product types. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function SalesByCouponWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportCoupons( reportParams ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData } = useMemo( + () => + buildSalesByCouponData( + primary.data, + comparison.data, + reportParams, + 3 + ), + [ primary.data, comparison.data, reportParams ] + ); + + const barStyles = useBarStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts new file mode 100644 index 000000000000..1d452bd9937a --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts @@ -0,0 +1,4 @@ +export { + SalesByDeviceWidget, + BookingsByDeviceWidget, +} from './sales-by-device-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx new file mode 100644 index 000000000000..3bc94384cbab --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -0,0 +1,135 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { + useReportOrderAttribution, + type FilterCondition, +} from '@next-woo-analytics/data'; +import { device } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { BarChart } from '../../components'; +import { buildSalesByDeviceData, BOOKINGS_FILTER } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useBarStyles } from '../common'; + +type SalesByDeviceWidgetProps = { + /** + * Optional product type filter to apply when fetching order attribution data. + * + * When provided, filters results to specific product types (e.g., bookings only). + * When omitted, shows data for all product types. + */ + filter?: FilterCondition; +}; + +/** + * Sales by Device Widget Component + * + * Displays a bar chart showing sales breakdown by device type (Desktop, Mobile, Tablet). + * + * Features: + * - Optional product type filtering (e.g., bookings only) + * - Comparison support (current vs previous period) + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @param props - Component props + * @param props.filter - Optional product type filter + * + * @example + * // All product types + * + * + * + * + * @example + * // Bookings only + * + * + * + */ +export function SalesByDeviceWidget( { filter }: SalesByDeviceWidgetProps ) { + const { reportParams } = useWidgetRootContext(); + + // Add the device view to params + const paramsWithView = useMemo( + () => ( { + ...reportParams, + view: 'device' as const, + ...( filter && { filters: [ filter ] } ), + } ), + [ reportParams, filter ] + ); + + const { + primary, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportOrderAttribution( paramsWithView ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData } = useMemo( + () => + buildSalesByDeviceData( primary.data, hasComparison, reportParams ), + [ primary.data, hasComparison, reportParams ] + ); + + const barStyles = useBarStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + { isRefetching && } + + ); +} + +/** + * Bookings by Device Widget Component + * + * Displays device breakdown data for booking products only. + * This component automatically filters data to show only booking product types + * (booking, bookable-event, bookable-service). + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function BookingsByDeviceWidget() { + return ; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/index.ts new file mode 100644 index 000000000000..28a1e191a8dd --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/index.ts @@ -0,0 +1 @@ +export { SalesByUtmWidget } from './sales-by-utm-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx new file mode 100644 index 000000000000..d48657be6e04 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { + useReportOrderAttribution, + ORDER_ATTRIBUTION_VIEWS, +} from '@next-woo-analytics/data'; +import { megaphone, search, channel } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { LeaderboardChart } from '../../components/chart-leaderboard'; +import { buildSalesByUtmData, formatLegendLabels } from '../../helpers'; +import { useWidgetError } from '../../hooks'; + +type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ]; + +type SalesByUtmWidgetProps = { + /** + * The order attribution view to display (source, channel, campaign, etc.) + */ + view: OrderAttributionView; +}; + +/** + * Sales by UTM Widget Component + * + * Displays order attribution data in a leaderboard chart, showing how sales are + * distributed across different UTM parameters (source, channel, or campaign). + * + * Features: + * - Multiple views: source, channel, campaign + * - Displays data for all product types + * - Comparison support (current vs previous period) + * - Formatted legend labels with date ranges + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @param props - Component props + * @param props.view - The order attribution view (source, channel, campaign) + * + * @example + * + * + * + */ +export function SalesByUtmWidget( { view }: SalesByUtmWidgetProps ) { + const { reportParams } = useWidgetRootContext(); + + const params = useMemo( + () => ( { + ...reportParams, + view, + } ), + [ reportParams, view ] + ); + + const { + primary, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportOrderAttribution( params ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const chartData = useMemo( + () => buildSalesByUtmData( primary.data ), + [ primary.data ] + ); + + const legendLabels = useMemo( + () => formatLegendLabels( reportParams ), + [ reportParams ] + ); + + const emptyStateIcon = useMemo( () => { + switch ( view ) { + case 'source': + return search; + case 'channel': + return channel; + case 'campaign': + return megaphone; + default: + return search; + } + }, [ view ] ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/index.ts new file mode 100644 index 000000000000..5c11324e2400 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/index.ts @@ -0,0 +1 @@ +export { SessionsByDeviceWidget } from './sessions-by-device-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.module.scss new file mode 100644 index 000000000000..77f9c81fda33 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.module.scss @@ -0,0 +1,3 @@ +.container { + height: 100%; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx new file mode 100644 index 000000000000..28a979b87e43 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { Stack } from '@wordpress/ui'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportSessionsByDevice } from '@next-woo-analytics/data'; +import { device } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { useWidgetError } from '../../hooks'; +import { SemiCircleChart } from '../../components'; +import { buildSessionsByDeviceData } from '../../helpers'; +import { useSegmentStyles } from '../common'; +import styles from './sessions-by-device-widget.module.scss'; + +/** + * Sessions by Device Type Widget Component + * + * Displays a semi-circle chart showing the breakdown of website sessions + * by device category: Mobile, Desktop, and Tablet. + * + * Features: + * - Shows total sessions in the center with comparison delta + * - Legend with individual device counts and comparison deltas + * - Supports comparison periods + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function SessionsByDeviceWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportSessionsByDevice( reportParams ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData, total, comparisonTotal, legendData } = useMemo( + () => buildSessionsByDeviceData( primary.data, comparison.data ), + [ primary.data, comparison.data ] + ); + + const segmentStyles = useSegmentStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/index.ts new file mode 100644 index 000000000000..ea562eb4bb17 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/index.ts @@ -0,0 +1 @@ +export { TotalReturnsWidget } from './total-returns-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx new file mode 100644 index 000000000000..d256d91382df --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { useReportOrders } from '@next-woo-analytics/data'; +import { paymentReturn } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { BarChart } from '../../components'; +import { buildTotalReturnsData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; +import { useBarStyles } from '../common'; + +/** + * Total Returns Widget Component + * + * A widget that displays total returns (refunds) as a bar chart + * showing refunds and net sales side by side. + * + * Must be used within a WidgetRoot which provides reportParams via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function TotalReturnsWidget() { + const { reportParams } = useWidgetRootContext(); + + const { + primary, + comparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + } = useReportOrders( reportParams ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const { chartData } = useMemo( + () => + buildTotalReturnsData( + primary.data, + comparison.data, + reportParams + ), + [ primary.data, comparison.data, reportParams ] + ); + + const barStyles = useBarStyles( chartData ); + + const hasError = useWidgetError( isError, error, refetch ); + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + return ( + <> + + { isRefetching && } + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/index.ts new file mode 100644 index 000000000000..9ee968c1e3bc --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/index.ts @@ -0,0 +1 @@ +export { VisitorMetricWidget } from './widget-visitor-metric'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx new file mode 100644 index 000000000000..53f4c8d45c01 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { useReportVisitors } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { ReportMetricWidget } from '../../components/report-metric'; +import { useWidgetRootContext } from '../../components/widget-root'; + +/** + * Visitor Metric Widget Component + * + * A widget that displays visitor metrics over time with comparison support. + * This component must be used within a WidgetRoot which provides reportParams + * via context. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function VisitorMetricWidget() { + const { reportParams } = useWidgetRootContext(); + + return ( + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/index.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/index.tsx new file mode 100644 index 000000000000..84201cec7527 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/index.tsx @@ -0,0 +1 @@ +export { VisitorsByLocationWidget } from './visitors-by-location-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts new file mode 100644 index 000000000000..8ea22f5f4175 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { + type ReportParams, + useReportVisitorsByLocation, +} from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { + buildVisitorsByLocationData, + type Region, + type LocationDataEntry, +} from '../../helpers/build-visitors-by-location-data'; + +export type { Region }; + +type LocationRawData = { + primary: LocationDataEntry[]; + comparison: LocationDataEntry[]; +}; + +/** + * Hook to fetch and build visitors by location chart data. + * + * @param reportParams - Report parameters from widget context + * @param region - The region to get data for ('US' or 'world') + * @return Geo chart data and leaderboard data for the selected region + */ +export function useVisitorsByLocation( + reportParams: ReportParams, + region: Region +) { + const usReport = useReportVisitorsByLocation( reportParams, { + enabled: region === 'US', + groupBy: 'region', + countryCode: 'US', + limit: 100, + } ); + + const worldReport = useReportVisitorsByLocation( reportParams, { + enabled: region === 'world', + groupBy: 'country', + limit: 15, + } ); + + const activeReport = region === 'US' ? usReport : worldReport; + const hasComparison = activeReport.hasComparison; + + const rawData: LocationRawData = useMemo( () => { + const primaryItems = activeReport.primary.data?.data ?? []; + const comparisonItems = activeReport.comparison.data?.data ?? []; + + if ( region === 'US' ) { + const mapUsRegions = ( items: typeof primaryItems ) => + items + .filter( ( item ) => Boolean( item.region ) ) + .map( ( item ) => ( { + id: item.region as string, + label: item.region as string, + value: item.visitors, + } ) ); + + return { + primary: mapUsRegions( primaryItems ), + comparison: mapUsRegions( comparisonItems ), + }; + } + + return { + primary: primaryItems.map( ( item ) => ( { + id: item.country_code.toLowerCase(), + label: item.label, + value: item.visitors, + } ) ), + comparison: comparisonItems.map( ( item ) => ( { + id: item.country_code.toLowerCase(), + label: item.label, + value: item.visitors, + } ) ), + }; + }, [ region, activeReport.primary.data, activeReport.comparison.data ] ); + + const chartDataResult = useMemo( + () => + buildVisitorsByLocationData( { + primaryData: rawData.primary, + comparisonData: hasComparison ? rawData.comparison : undefined, + region, + } ), + [ rawData.primary, rawData.comparison, region, hasComparison ] + ); + + const { isLoading, isFetching, hasData, isError, error, refetch } = + activeReport; + + return { + ...chartDataResult, + hasComparison, + isLoading, + isFetching, + hasData, + isError, + error, + refetch, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss new file mode 100644 index 000000000000..892f02edfbf9 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss @@ -0,0 +1,40 @@ +.root { + height: 100%; + overflow: hidden; +} + +.container { + height: 100%; +} + +.geoChart { + min-width: 0; + max-height: 250px; +} + +.toggleControl { + grid-column: 2; + + // The upstream widget content container (.next-admin-dashboard-widget__content) + // sets overflow: auto, which clips the ToggleGroupControl's outward focus ring. + // Add padding to create space for the focus indicator. + padding-block-start: var( + --wpds-dimension-padding-xs, + 4px + ); + padding-inline-end: var( + --wpds-dimension-padding-xs, + 4px + ); + padding-block-end: 0; + padding-inline-start: 0; +} + +.leaderboardChart { + // bar border-radius now comes from chartTheme.leaderboardChart.barBorderRadius + + .leaderboardImage { + height: 20px; + border-radius: var(--wpds-border-radius-sm, 2px); + } +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx new file mode 100644 index 000000000000..3648a7d29947 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx @@ -0,0 +1,293 @@ +/** + * External dependencies + */ +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalGrid as Grid, +} from '@wordpress/components'; +import { useResizeObserver } from '@wordpress/compose'; +import { __, sprintf } from '@wordpress/i18n'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { GeoChart } from '@automattic/charts'; +import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { location } from '@next-woo-analytics/icons'; + +/** + * Internal dependencies + */ +import { useWidgetRootContext } from '../../components/widget-root'; +import { useWidgetError } from '../../hooks'; +import { useVisitorsByLocation, type Region } from './use-visitors-by-location'; +import { + LeaderboardChart, + LeaderboardLabel, +} from '../../components/chart-leaderboard'; +import { flagUrl } from '../../helpers'; +import styles from './visitors-by-location-widget.module.scss'; +import { ChartEmptyState } from '../../components'; +import { RESIZE_DEBOUNCE_MS } from '../../constants'; + +function isRegion( value: unknown ): value is Region { + return value === 'US' || value === 'world'; +} + +function closestHTMLElement( + el: Element | null | undefined, + selector: string +): HTMLElement | null { + const match = el?.closest( selector ); + return match instanceof HTMLElement ? match : null; +} + +function isSingleColumnTileFromGridColumnEnd( gridColumnEnd: string ) { + const raw = ( gridColumnEnd || '' ).trim(); + const match = raw.match( /^span\s+(\d+)$/ ); + if ( ! match ) { + return false; + } + return Number( match[ 1 ] ) === 1; +} + +export function VisitorsByLocationWidget() { + const { reportParams } = useWidgetRootContext(); + const [ region, setRegion ] = useState< Region >( 'US' ); + const [ isMinimized, setIsMinimized ] = useState( false ); + const rootRef = useRef< HTMLDivElement | null >( null ); + const tileButtonRef = useRef< HTMLElement | null >( null ); + const resizeDebounceTimeoutRef = useRef< ReturnType< + typeof setTimeout + > | null >( null ); + + const { + geoData, + leaderboardData, + isLoading, + isFetching, + hasData, + hasComparison, + isError, + error, + refetch, + } = useVisitorsByLocation( reportParams, region ); + + const isInitialLoading = isLoading && ! hasData; + const isRefetching = isFetching && hasData; + + const leaderboardDataWithImages = useMemo( + () => + leaderboardData.map( ( item ) => { + const imageUrl = flagUrl( region === 'US' ? 'us' : item.id ); + const labelText = + typeof item.label === 'string' ? item.label : ''; + const imageAlt = + region === 'US' + ? __( 'United States flag', 'woocommerce-analytics' ) + : sprintf( + /* translators: %s is the country name */ + __( 'Flag of %s', 'woocommerce-analytics' ), + labelText + ); + + return { + ...item, + label: ( + + ), + }; + } ), + [ leaderboardData, region ] + ); + + const updateIsMinimized = useCallback( () => { + const tileButton = tileButtonRef.current; + if ( ! tileButton ) { + return; + } + + const nextIsMinimized = isSingleColumnTileFromGridColumnEnd( + tileButton.style.gridColumnEnd + ); + + // Avoid scheduling React state updates when nothing changes. + setIsMinimized( ( prev ) => + prev === nextIsMinimized ? prev : nextIsMinimized + ); + }, [] ); + + const debouncedResizeUpdate = useCallback( () => { + // ResizeObserver can fire very frequently while the tile is being resized. + // Debounce to reduce rerenders, mirroring GeoChart's internal resize debounce. + if ( resizeDebounceTimeoutRef.current ) { + clearTimeout( resizeDebounceTimeoutRef.current ); + } + resizeDebounceTimeoutRef.current = setTimeout( + updateIsMinimized, + RESIZE_DEBOUNCE_MS + ); + }, [ updateIsMinimized ] ); + + const resizeObserverRef = useResizeObserver( () => { + debouncedResizeUpdate(); + } ); + + useEffect( () => { + const root = rootRef.current; + + // DataViews picker grid: always render the simplified (map-only) tile + // and avoid attaching any observers/listeners. + const dataViewsPickerGrid = closestHTMLElement( + root, + '.dataviews-view-picker-grid' + ); + + if ( dataViewsPickerGrid ) { + tileButtonRef.current = null; + setIsMinimized( true ); + return; + } + + // Dashboard tile: react to changes in the tile's grid span. + const tileButton = closestHTMLElement( + root, + '[role="button"][aria-roledescription="sortable"]' + ); + + if ( ! tileButton ) { + tileButtonRef.current = null; + setIsMinimized( false ); + return; + } + + tileButtonRef.current = tileButton; + + updateIsMinimized(); + + const mutationObserver = new MutationObserver( updateIsMinimized ); + mutationObserver.observe( tileButton, { + attributes: true, + attributeFilter: [ 'style', 'class' ], + } ); + + // `useResizeObserver` returns a ref callback. We can attach it + // programmatically to `tileButton` even though it's outside this component's + // render tree. + resizeObserverRef( tileButton ); + + return () => { + mutationObserver.disconnect(); + resizeObserverRef( null ); + tileButtonRef.current = null; + if ( resizeDebounceTimeoutRef.current ) { + clearTimeout( resizeDebounceTimeoutRef.current ); + resizeDebounceTimeoutRef.current = null; + } + }; + }, [ resizeObserverRef, updateIsMinimized, leaderboardData ] ); + + const geoChartProps = + region === 'US' + ? ( { + region, + resolution: 'provinces', + } as const ) + : {}; + + const geoChart = ( + + ); + + const hasError = useWidgetError( isError, error, refetch ); + + if ( hasError ) { + return null; + } + + if ( isInitialLoading ) { + return ; + } + + if ( ! leaderboardData || leaderboardData.length === 0 ) { + return ( + <> + + { isRefetching && } + + ); + } + + return ( + <> +
+ { isMinimized ? ( +
{ geoChart }
+ ) : ( + +
+ { + if ( isRegion( value ) ) { + setRegion( value ); + } + } } + value={ region } + > + + + +
+ +
{ geoChart }
+ + +
+ ) } +
+ { isRefetching && } + + ); +} From 38707ec51255e3668bef2de28bca42a15136e585 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:21:31 +0800 Subject: [PATCH 23/32] refactor(premium-analytics): adapt widgets-toolkit imports and manifest for monorepo --- .../packages/widgets-toolkit/README.md | 105 ++++++++++-------- .../packages/widgets-toolkit/package.json | 52 +++++---- .../src/components/chart-bar/README.md | 6 +- .../chart-comparative-line/README.md | 6 +- .../comparative-line-chart.tsx | 2 +- .../src/components/chart-donut/README.md | 2 +- .../chart-empty-state/chart-empty-state.tsx | 4 +- .../components/chart-leaderboard/README.md | 4 +- .../chart-leaderboard/leaderboard-chart.tsx | 2 +- .../components/chart-semi-circle/README.md | 2 +- .../src/components/legend/README.md | 4 +- .../components/metric-delta/metric-delta.tsx | 2 +- .../components/metric-value/metric-value.tsx | 2 +- .../src/components/widget-root/README.md | 2 +- .../src/components/widget-root/context.tsx | 2 +- .../components/widget-root/widget-root.tsx | 2 +- .../fields/date-report-params-field/README.md | 2 +- .../date-report-params-field.tsx | 8 +- .../fields/metrics-field/metrics-field.tsx | 4 +- .../src/fields/metrics-field/metrics.ts | 26 ++--- .../__tests__/build-coupon-use-data.test.ts | 2 +- .../build-sales-by-coupon-data.test.ts | 2 +- .../build-bookings-by-attendance-data.ts | 12 +- .../src/helpers/build-coupon-use-data.ts | 12 +- .../build-new-vs-returning-customer-data.ts | 12 +- .../helpers/build-orders-fulfillment-data.ts | 12 +- .../src/helpers/build-payment-status-data.ts | 12 +- .../build-revenue-by-customer-type-data.ts | 6 +- .../src/helpers/build-sales-by-coupon-data.ts | 6 +- .../src/helpers/build-sales-by-device-data.ts | 6 +- .../src/helpers/build-sales-by-utm-data.ts | 4 +- .../helpers/build-sessions-by-device-data.ts | 10 +- .../helpers/build-time-series-chart-data.ts | 6 +- .../src/helpers/build-total-returns-data.ts | 8 +- .../build-visitors-by-location-data.ts | 4 +- .../src/helpers/format-legend-labels.ts | 6 +- .../src/helpers/format-orders-metrics.ts | 2 +- .../src/helpers/fulfillment-filters.ts | 2 +- .../src/helpers/payment-status-filters.ts | 2 +- .../src/helpers/product-type-filters.ts | 2 +- .../src/hooks/use-widget-error.ts | 6 +- .../packages/widgets-toolkit/src/types.ts | 4 +- .../bookings-by-attendance-widget.tsx | 4 +- .../conversion-rate-widget.tsx | 4 +- .../widgets/coupon-use/coupon-use-widget.tsx | 4 +- .../new-vs-returning-customer-widget.tsx | 4 +- .../booking-order-metric-widget.tsx | 2 +- .../order-metric/widget-order-metric.tsx | 2 +- .../orders-fulfillment-widget.tsx | 4 +- .../payment-status/payment-status-widget.tsx | 4 +- .../top-performing-bookings-widget.tsx | 2 +- ...-performing-product-leaderboard-widget.tsx | 4 +- .../revenue-by-customer-type-widget.tsx | 4 +- .../sales-by-coupon-widget.tsx | 4 +- .../sales-by-device-widget.tsx | 4 +- .../sales-by-utm/sales-by-utm-widget.tsx | 4 +- .../sessions-by-device-widget.tsx | 4 +- .../total-returns/total-returns-widget.tsx | 4 +- .../visitor-metric/widget-visitor-metric.tsx | 2 +- .../use-visitors-by-location.ts | 2 +- .../visitors-by-location-widget.tsx | 12 +- 61 files changed, 231 insertions(+), 220 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/README.md index 0d04716eabe8..d4bf7888775d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/README.md @@ -1,13 +1,14 @@ -# @next-woo-analytics/widgets-toolkit +# @automattic/jetpack-premium-analytics-widgets-toolkit -A collection of focused, single-responsibility components for building WooCommerce Analytics widgets. +A collection of focused, single-responsibility components for building analytics widgets. Each component has a clear API and specific purpose, making them easy to understand, test, and compose. ## Installation -```bash -npm install @next-woo-analytics/widgets-toolkit -``` +This is an internal package of Jetpack Premium Analytics — it is never +published to npm and is resolved entirely in-tree. It's automatically +available to routes and other internal packages within +`@automattic/jetpack-premium-analytics`. ## Components @@ -16,6 +17,7 @@ npm install @next-woo-analytics/widgets-toolkit Displays a formatted numeric value. Does NOT handle comparisons or deltas. **Props:** + - `value` (number) - The numeric value to display - `format` ('number' | 'currency' | 'percentage') - How to format the value (default: 'number') - `formatter` ((value: number) => string) - Custom formatter function (overrides format) @@ -26,7 +28,7 @@ Displays a formatted numeric value. Does NOT handle comparisons or deltas. **Examples:** ```tsx -import { MetricValue } from '@next-woo-analytics/widgets-toolkit'; +import { MetricValue } from '@jetpack-premium-analytics/widgets-toolkit'; // Simple number @@ -48,6 +50,7 @@ import { MetricValue } from '@next-woo-analytics/widgets-toolkit'; Displays the change between two values (as percentage or absolute). **Props:** + - `current` (number) - The current/new value - `previous` (number) - The previous/comparison value - `fallback` (string) - Display when calculation fails (default: '—') @@ -60,7 +63,7 @@ Displays the change between two values (as percentage or absolute). **Examples:** ```tsx -import { MetricDelta } from '@next-woo-analytics/widgets-toolkit'; +import { MetricDelta } from '@jetpack-premium-analytics/widgets-toolkit'; // Percentage change: +50% @@ -77,6 +80,7 @@ import { MetricDelta } from '@next-woo-analytics/widgets-toolkit'; ``` **Delta Calculation:** + - Returns percentage change: `( ( current - previous ) / |previous| ) * 100` - Returns `null` if inputs are invalid or previous is zero (displays fallback) - Returns `0` if both current and previous are zero @@ -88,6 +92,7 @@ import { MetricDelta } from '@next-woo-analytics/widgets-toolkit'; Composite component that combines MetricValue and MetricDelta. **Props:** + - `value` (number) - The current value - `previousValue` (number | null) - Previous value for comparison (no delta if null) - `format` ('number' | 'currency' | 'percentage') - How to format the value (default: 'number') @@ -103,7 +108,7 @@ Composite component that combines MetricValue and MetricDelta. **Examples:** ```tsx -import { MetricWithComparison } from '@next-woo-analytics/widgets-toolkit'; +import { MetricWithComparison } from '@jetpack-premium-analytics/widgets-toolkit'; // Simple metric with comparison -): SeriesData[] + series: SeriesData[], + chartTheme: ReturnType< typeof useChartTheme > +): SeriesData[]; ``` **Example:** + ```tsx import { - applyThemeStylesToSeries, - useChartTheme, - ComparativeLineChart, -} from '@next-woo-analytics/widgets-toolkit'; + applyThemeStylesToSeries, + useChartTheme, + ComparativeLineChart, +} from '@jetpack-premium-analytics/widgets-toolkit'; const chartTheme = useChartTheme(); const styledSeries = applyThemeStylesToSeries( series, chartTheme ); - +; ``` **What it does:** + - Maps `chartTheme.seriesLineStyles` to each series - Sets `options.stroke` from `chartTheme.colors[ 0 ]` - Sets `options.seriesLineStyle` with strokeWidth, strokeDasharray, etc. @@ -314,16 +328,18 @@ const styledSeries = applyThemeStylesToSeries( series, chartTheme ); Creates a formatter function for a specific order metric. **Signature:** + ```tsx function formatOrderMetric( - metricKey: MetricKey, - options?: FormatMetricValueOptions -): ( value: number ) => string + metricKey: MetricKey, + options?: FormatMetricValueOptions +): ( value: number ) => string; ``` **Example:** + ```tsx -import { formatOrderMetric } from '@next-woo-analytics/widgets-toolkit'; +import { formatOrderMetric } from '@jetpack-premium-analytics/widgets-toolkit'; const formatter = formatOrderMetric( 'total_sales' ); formatter( 1234.56 ); // Returns: "$1,234.56" @@ -342,11 +358,11 @@ Configuration object for formatting chart values and tooltips. ```tsx type DataFormat = { - type: 'number' | 'currency' | 'percentage' | 'average'; - options?: { - useMultipliers?: boolean; // Use K, M, B suffixes for large numbers - decimals?: number; // Number of decimal places - }; + type: 'number' | 'currency' | 'percentage' | 'average'; + options?: { + useMultipliers?: boolean; // Use K, M, B suffixes for large numbers + decimals?: number; // Number of decimal places + }; }; ``` @@ -356,14 +372,14 @@ Union type of all supported metric keys. ```tsx type OrderMetricKey = - | 'orders_no' - | 'total_sales' - | 'average_order_value' - | 'avg_items' - | 'orders_value_net' - | 'orders_value_gross' - | 'coupons' - | 'profit_margin'; + | 'orders_no' + | 'total_sales' + | 'average_order_value' + | 'avg_items' + | 'orders_value_net' + | 'orders_value_gross' + | 'coupons' + | 'profit_margin'; type VisitorsMetricKey = 'visitors'; @@ -381,10 +397,7 @@ Components use CSS Modules for styling. You can customize appearance by: 3. **Overriding styles**: Use CSS Modules or styled-components Example: + ```tsx - + ``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/package.json b/projects/packages/premium-analytics/packages/widgets-toolkit/package.json index b34c61a23e4b..44af5e35cf4c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/package.json +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/package.json @@ -1,33 +1,31 @@ { - "name": "@next-woo-analytics/widgets-toolkit", - "description": "Widgets Toolkit for WooCommerce Analytics", - "version": "1.0.0", - "wpModule": true, + "name": "@automattic/jetpack-premium-analytics-widgets-toolkit", + "version": "0.1.0", + "private": true, "type": "module", "main": "src/index.ts", - "exports": { - ".": "./build/src/index.js" - }, + "types": "src/index.ts", + "sideEffects": [ + "*.scss" + ], "dependencies": { - "@automattic/dashboard": "*", - "@automattic/design-system": "*", - "@automattic/admin-toolkit": "*", - "@automattic/charts": "*", - "@next-woo-analytics/components": "workspace:*", - "@next-woo-analytics/data": "workspace:*", - "@woocommerce-next/data": "*", - "@next-woo-analytics/datetime": "workspace:*", - "@wc-analytics/formatters": "workspace:*", - "@next-woo-analytics/icons": "workspace:*", - "@next-woo-analytics/routing": "workspace:*", - "@wordpress/components": "*", - "@wordpress/compose": "*", - "@wordpress/icons": "*", - "@ciab/dataviews": "*", - "@tanstack/react-router": "*", - "date-fns": "*", - "@wordpress/theme": "*", - "@wordpress/ui": "*", - "clsx": "*" + "@automattic/charts": "workspace:*", + "@jetpack-premium-analytics/data": "workspace:*", + "@jetpack-premium-analytics/datetime": "workspace:*", + "@jetpack-premium-analytics/formatters": "workspace:*", + "@jetpack-premium-analytics/icons": "workspace:*", + "@jetpack-premium-analytics/routing": "workspace:*", + "@jetpack-premium-analytics/ui": "workspace:*", + "@wordpress/components": "33.1.0", + "@wordpress/compose": "7.46.0", + "@wordpress/dataviews": "16.0.0", + "@wordpress/i18n": "^6.9.0", + "@wordpress/icons": "^13.0.0", + "@wordpress/route": "0.12.0", + "@wordpress/theme": "0.13.0", + "@wordpress/ui": "0.13.0", + "clsx": "2.1.1", + "date-fns": "4.1.0", + "react": "18.3.1" } } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md index 19a23124b322..a596da06b955 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md @@ -7,7 +7,7 @@ A **pure** vertical bar chart component for displaying categorical data. Built o This component is **pure and self-contained**—it receives all styling via props and has no external dependencies on context providers or themes. ```tsx -import { BarChart } from '@next-woo-analytics/widgets-toolkit'; +import { BarChart } from '@jetpack-premium-analytics/widgets-toolkit'; @@ -48,7 +48,7 @@ export type ChartEmptyStateProps = { */ export function ChartEmptyState( { icon = cautionFilled, - text = __( 'No data found for this date range.', 'woocommerce-analytics' ), + text = __( 'No data found for this date range.', 'jetpack-premium-analytics' ), }: ChartEmptyStateProps ) { return ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md index 592c2cad5f2b..12fd9172fb53 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md @@ -28,7 +28,7 @@ import { GlobalChartsProvider } from '@automattic/charts'; ## Usage ```tsx -import { LeaderboardChart } from '@next-woo-analytics/widgets-toolkit'; +import { LeaderboardChart } from '@jetpack-premium-analytics/widgets-toolkit'; const data = [ { @@ -204,7 +204,7 @@ The component automatically retrieves colors from the GlobalChartsProvider conte ```tsx import { GlobalChartsProvider } from '@automattic/charts'; -import { LeaderboardChart } from '@next-woo-analytics/widgets-toolkit'; +import { LeaderboardChart } from '@jetpack-premium-analytics/widgets-toolkit'; function MyWidget() { return ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx index 8918ea3bf652..869bbfd9c6bc 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx @@ -11,7 +11,7 @@ import { import clsx from 'clsx'; import type { ComponentProps, ReactNode } from 'react'; import { useMemo } from 'react'; -import { formatMetricValue } from '@wc-analytics/formatters'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md index 2312b755ff89..4194cea6e225 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md @@ -14,7 +14,7 @@ A responsive semi-circle (half-donut) chart component that fills its parent cont ## Usage ```tsx -import { SemiCircleChart } from '@next-woo-analytics/widgets-toolkit'; +import { SemiCircleChart } from '@jetpack-premium-analytics/widgets-toolkit'; const chartData = [ { label: 'Mobile', value: 4500 }, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md index 65426a7d3b80..2f623e19a31e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md @@ -5,7 +5,7 @@ A pure component for rendering chart legends with optional comparison deltas. ## Usage ```tsx -import { Legend } from '@next-woo-analytics/widgets-toolkit'; +import { Legend } from '@jetpack-premium-analytics/widgets-toolkit'; const items = [ { label: 'Mobile', value: 241950, displayValue: '$241.95K', color: '#3858E9' }, @@ -49,7 +49,7 @@ const items = [ For widgets inside `GlobalChartsProvider`, use `LegendWithTheme` instead. It automatically resolves colors from the chart theme: ```tsx -import { LegendWithTheme as Legend } from '@next-woo-analytics/widgets-toolkit'; +import { LegendWithTheme as Legend } from '@jetpack-premium-analytics/widgets-toolkit'; // Colors are injected from theme - no need to specify them - { __( 'Metrics', 'woocommerce-analytics' ) } + { __( 'Metrics', 'jetpack-premium-analytics' ) } { help } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts index f7370d115782..40a28101b1c6 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { FilterCondition } from '@next-woo-analytics/data'; +import { FilterCondition } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -28,10 +28,10 @@ export type Metric = { const METRIC_NET_SALES: Metric = { id: 'general-orders_value_net', - label: __( 'Net sales', 'woocommerce-analytics' ), + label: __( 'Net sales', 'jetpack-premium-analytics' ), description: __( 'Monitor your total revenue — after any discounts, returns, or adjustments — over a set period of time.', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), category: 'Finances', metricType: 'general', @@ -41,10 +41,10 @@ const METRIC_NET_SALES: Metric = { const METRIC_ORDERS: Metric = { id: 'general-orders_no', - label: __( 'Orders', 'woocommerce-analytics' ), + label: __( 'Orders', 'jetpack-premium-analytics' ), description: __( 'See a breakdown of when orders are placed to identify peak selling periods.', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), category: 'Orders', metricType: 'general', @@ -54,10 +54,10 @@ const METRIC_ORDERS: Metric = { const METRIC_BOOKINGS: Metric = { id: 'booking-orders_no', - label: __( 'Bookings', 'woocommerce-analytics' ), + label: __( 'Bookings', 'jetpack-premium-analytics' ), description: __( 'See a breakdown of when bookings are placed to identify peak selling periods.', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), category: 'Orders', metricKey: 'orders_no', @@ -74,10 +74,10 @@ const METRIC_BOOKINGS: Metric = { const METRIC_VISITORS: Metric = { id: 'visitors-visitors', - label: __( 'Visitors', 'woocommerce-analytics' ), + label: __( 'Visitors', 'jetpack-premium-analytics' ), description: __( 'Track website visitor trends and monitor traffic patterns over time.', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), category: 'Orders', metricType: 'visitors', @@ -87,10 +87,10 @@ const METRIC_VISITORS: Metric = { const METRIC_CONVERSION_RATE: Metric = { id: 'conversion-conversion_rate', - label: __( 'Store conversion rate', 'woocommerce-analytics' ), + label: __( 'Store conversion rate', 'jetpack-premium-analytics' ), description: __( "Track your store's conversion funnel from sessions to completed orders.", - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), category: 'Sales', metricType: 'conversion', @@ -100,10 +100,10 @@ const METRIC_CONVERSION_RATE: Metric = { const METRIC_CUSTOMERS: Metric = { id: 'customers-customers', - label: __( 'Customers', 'woocommerce-analytics' ), + label: __( 'Customers', 'jetpack-premium-analytics' ), description: __( 'Track the total number of customers (new and returning) who placed orders during the selected time period.', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), category: 'Orders', metricType: 'customers', diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts index 024fa02121da..bd1242234398 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts @@ -1,7 +1,7 @@ /** * Mock formatMetricValue to avoid pulling in heavy transitive deps. */ -jest.mock( '@next-woo-analytics/formatters', () => ( { +jest.mock( '@jetpack-premium-analytics/formatters', () => ( { formatMetricValue: ( value: number ) => `$${ value }`, } ) ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts index 8caae1afdbf2..68854e41667a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts @@ -1,7 +1,7 @@ /** * Mock formatters to avoid pulling in heavy transitive deps. */ -jest.mock( '@next-woo-analytics/formatters', () => ( { +jest.mock( '@jetpack-premium-analytics/formatters', () => ( { formatDateRange: () => 'Jan 1 – 31, 2024', } ) ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts index 697676267a55..d1ad76d7db38 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -54,19 +54,19 @@ export function buildBookingsByAttendanceData( const statusMap: Array< { key: AttendanceStatusKey; label: string } > = [ { key: 'attendance_status_booked', - label: __( 'Booked', 'woocommerce-analytics' ), + label: __( 'Booked', 'jetpack-premium-analytics' ), }, { key: 'attendance_status_checked_in', - label: __( 'Checked In', 'woocommerce-analytics' ), + label: __( 'Checked In', 'jetpack-premium-analytics' ), }, { key: 'attendance_status_no_show', - label: __( 'No Show', 'woocommerce-analytics' ), + label: __( 'No Show', 'jetpack-premium-analytics' ), }, { key: 'status_cancelled', - label: __( 'Cancelled', 'woocommerce-analytics' ), + label: __( 'Cancelled', 'jetpack-premium-analytics' ), }, ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts index d41e0d2a486d..ceaa6e53f6a9 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -67,7 +67,7 @@ export function buildCouponUseData( // Build chart data showing sales breakdown const chartData: DonutChartData = [ { - label: __( 'With coupons', 'woocommerce-analytics' ), + label: __( 'With coupons', 'jetpack-premium-analytics' ), value: salesWithCoupon, valueDisplay: formatMetricValue( salesWithCoupon, 'currency', { useMultipliers: true, @@ -75,7 +75,7 @@ export function buildCouponUseData( } ), }, { - label: __( 'No coupons', 'woocommerce-analytics' ), + label: __( 'No coupons', 'jetpack-premium-analytics' ), value: salesWithoutCoupon, valueDisplay: formatMetricValue( salesWithoutCoupon, 'currency', { useMultipliers: true, @@ -87,7 +87,7 @@ export function buildCouponUseData( // Build legend data const legendData: LegendItem[] = [ { - label: __( 'With coupons', 'woocommerce-analytics' ), + label: __( 'With coupons', 'jetpack-premium-analytics' ), value: salesWithCoupon, displayValue: formatMetricValue( salesWithCoupon, 'currency', { useMultipliers: true, @@ -96,7 +96,7 @@ export function buildCouponUseData( comparison: hasComparison ? comparisonSalesWithCoupon : undefined, }, { - label: __( 'No coupons', 'woocommerce-analytics' ), + label: __( 'No coupons', 'jetpack-premium-analytics' ), value: salesWithoutCoupon, displayValue: formatMetricValue( salesWithoutCoupon, 'currency', { useMultipliers: true, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts index 757b18bc6779..a04fa24d5927 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -66,7 +66,7 @@ export function buildNewVsReturningCustomerData( // Note: Returning customers first to match design (larger segment first) const chartData: DonutChartData = [ { - label: __( 'Returning', 'woocommerce-analytics' ), + label: __( 'Returning', 'jetpack-premium-analytics' ), value: returningCustomers, valueDisplay: formatMetricValue( returningCustomers, 'number', { useMultipliers: true, @@ -74,7 +74,7 @@ export function buildNewVsReturningCustomerData( } ), }, { - label: __( 'New', 'woocommerce-analytics' ), + label: __( 'New', 'jetpack-premium-analytics' ), value: newCustomers, valueDisplay: formatMetricValue( newCustomers, 'number', { useMultipliers: true, @@ -86,7 +86,7 @@ export function buildNewVsReturningCustomerData( // Build legend data (same order as chart) const legendData: LegendItem[] = [ { - label: __( 'Returning', 'woocommerce-analytics' ), + label: __( 'Returning', 'jetpack-premium-analytics' ), value: returningCustomers, displayValue: formatMetricValue( returningCustomers, 'number', { useMultipliers: true, @@ -97,7 +97,7 @@ export function buildNewVsReturningCustomerData( : undefined, }, { - label: __( 'New', 'woocommerce-analytics' ), + label: __( 'New', 'jetpack-premium-analytics' ), value: newCustomers, displayValue: formatMetricValue( newCustomers, 'number', { useMultipliers: true, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts index 69968c2a8df8..0e182e0c01fe 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -63,12 +63,12 @@ export function buildOrdersFulfillmentData( const chartData: DonutChartData = [ { - label: __( 'Fulfilled', 'woocommerce-analytics' ), + label: __( 'Fulfilled', 'jetpack-premium-analytics' ), value: fulfilledCount, valueDisplay: formatCount( fulfilledCount ), }, { - label: __( 'Unfulfilled', 'woocommerce-analytics' ), + label: __( 'Unfulfilled', 'jetpack-premium-analytics' ), value: unfulfilledCount, valueDisplay: formatCount( unfulfilledCount ), }, @@ -76,7 +76,7 @@ export function buildOrdersFulfillmentData( const legendData: LegendItem[] = [ { - label: __( 'Fulfilled', 'woocommerce-analytics' ), + label: __( 'Fulfilled', 'jetpack-premium-analytics' ), value: fulfilledCount, displayValue: formatCount( fulfilledCount ), comparison: comparisonFulfilledOrders @@ -84,7 +84,7 @@ export function buildOrdersFulfillmentData( : undefined, }, { - label: __( 'Unfulfilled', 'woocommerce-analytics' ), + label: __( 'Unfulfilled', 'jetpack-premium-analytics' ), value: unfulfilledCount, displayValue: formatCount( unfulfilledCount ), comparison: comparisonUnfulfilledOrders diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts index 23631bc32e64..cc77eedad888 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -63,7 +63,7 @@ export function buildPaymentStatusData( // Build chart data const chartData: DonutChartData = [ { - label: __( 'Paid', 'woocommerce-analytics' ), + label: __( 'Paid', 'jetpack-premium-analytics' ), value: paidNetSales, valueDisplay: formatMetricValue( paidNetSales, 'currency', { useMultipliers: true, @@ -71,7 +71,7 @@ export function buildPaymentStatusData( } ), }, { - label: __( 'Unpaid', 'woocommerce-analytics' ), + label: __( 'Unpaid', 'jetpack-premium-analytics' ), value: unpaidNetSales, valueDisplay: formatMetricValue( unpaidNetSales, 'currency', { useMultipliers: true, @@ -83,7 +83,7 @@ export function buildPaymentStatusData( // Build legend data const legendData: LegendItem[] = [ { - label: __( 'Paid', 'woocommerce-analytics' ), + label: __( 'Paid', 'jetpack-premium-analytics' ), value: paidNetSales, displayValue: formatMetricValue( paidNetSales, 'currency', { useMultipliers: true, @@ -92,7 +92,7 @@ export function buildPaymentStatusData( comparison: comparisonOrders ? comparisonPaidNetSales : undefined, }, { - label: __( 'Unpaid', 'woocommerce-analytics' ), + label: __( 'Unpaid', 'jetpack-premium-analytics' ), value: unpaidNetSales, displayValue: formatMetricValue( unpaidNetSales, 'currency', { useMultipliers: true, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts index 2a2853fd801e..60d97312afb1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; import type { SeriesData } from '@automattic/charts'; /** @@ -47,11 +47,11 @@ export function buildRevenueByCustomerTypeData( label: primaryLabel, data: [ { - label: __( 'Returning', 'woocommerce-analytics' ), + label: __( 'Returning', 'jetpack-premium-analytics' ), value: returningCustomerSales, }, { - label: __( 'New', 'woocommerce-analytics' ), + label: __( 'New', 'jetpack-premium-analytics' ), value: newCustomerSales, }, ], diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts index 080991ae33dc..29e0424af531 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; import type { SeriesData } from '@automattic/charts'; /** @@ -65,7 +65,7 @@ export function buildSalesByCouponData( .reduce( ( sum, item ) => sum + item.total_sales, 0 ); currentPeriodData.push( { - label: __( 'Other', 'woocommerce-analytics' ), + label: __( 'Other', 'jetpack-premium-analytics' ), value: otherSales, } ); } @@ -92,7 +92,7 @@ export function buildSalesByCouponData( .reduce( ( sum, item ) => sum + item.total_sales, 0 ); comparisonPeriodData.push( { - label: __( 'Other', 'woocommerce-analytics' ), + label: __( 'Other', 'jetpack-premium-analytics' ), value: otherComparison, } ); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts index 8e9164c677c5..94b0cbcc3f8a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; import type { SeriesData } from '@automattic/charts'; /** @@ -44,7 +44,7 @@ export function buildSalesByDeviceData( { label: primaryLabel, data: data.map( ( item ) => ( { - label: item.item || __( 'Unassigned', 'woocommerce-analytics' ), + label: item.item || __( 'Unassigned', 'jetpack-premium-analytics' ), value: item.current_period?.value ?? 0, } ) ), }, @@ -55,7 +55,7 @@ export function buildSalesByDeviceData( chartData.push( { label: comparisonLabel, data: data.map( ( item ) => ( { - label: item.item || __( 'Unassigned', 'woocommerce-analytics' ), + label: item.item || __( 'Unassigned', 'jetpack-premium-analytics' ), value: item.previous_period?.value ?? 0, } ) ), } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts index 72f7f5396b86..1e2a68df751e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -47,7 +47,7 @@ export function buildSalesByUtmData( return { id: item.item ? String( item.item ) : String( idx ), - label: item.item || __( 'Unassigned', 'woocommerce-analytics' ), + label: item.item || __( 'Unassigned', 'jetpack-premium-analytics' ), currentValue, previousValue, currentShare: ( currentValue / maxValue ) * 100, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts index 3e7dfb42bb02..f58f8cd08480 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -23,9 +23,9 @@ export interface SessionsByDeviceData { * Maps API device_type values to user-friendly labels. */ const DEVICE_LABELS: Record< string, string > = { - mobile: __( 'Mobile', 'woocommerce-analytics' ), - desktop: __( 'Desktop', 'woocommerce-analytics' ), - tablet: __( 'Tablet', 'woocommerce-analytics' ), + mobile: __( 'Mobile', 'jetpack-premium-analytics' ), + desktop: __( 'Desktop', 'jetpack-premium-analytics' ), + tablet: __( 'Tablet', 'jetpack-premium-analytics' ), }; /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts index 47d292291e0d..93465e21282e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts @@ -1,8 +1,8 @@ /** * External dependencies */ -import { formatDateRange } from '@wc-analytics/formatters'; -import { localTZDate } from '@next-woo-analytics/data'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; +import { localTZDate } from '@jetpack-premium-analytics/data'; import { __ } from '@wordpress/i18n'; /** @@ -68,7 +68,7 @@ export function buildTimeSeriesChartData< T extends TimeSeriesData >( { if ( emptyDataFallback === 'no-data-series' ) { return [ { - label: __( 'No data available', 'woocommerce-analytics' ), + label: __( 'No data available', 'jetpack-premium-analytics' ), data: [], }, ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts index a43f5e182e16..ebb60dc57ba8 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@next-woo-analytics/data'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; import type { SeriesData } from '@automattic/charts'; /** @@ -61,7 +61,7 @@ export function buildTotalReturnsData( data: [ { label: 'Total sales', value: salesAmount }, { - label: __( 'Refunds', 'woocommerce-analytics' ), + label: __( 'Refunds', 'jetpack-premium-analytics' ), value: refundsAmount, }, ], @@ -81,11 +81,11 @@ export function buildTotalReturnsData( label: comparisonLabel, data: [ { - label: __( 'Total sales', 'woocommerce-analytics' ), + label: __( 'Total sales', 'jetpack-premium-analytics' ), value: comparisonSalesAmount, }, { - label: __( 'Refunds', 'woocommerce-analytics' ), + label: __( 'Refunds', 'jetpack-premium-analytics' ), value: comparisonTotalRefunds, }, ], diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts index 8f5586d9ca2e..ea18235302a5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts @@ -47,8 +47,8 @@ export function buildVisitorsByLocationData( { }: BuildVisitorsByLocationDataParams ): VisitorsByLocationData { const headerLabel = region === 'US' - ? __( 'State', 'woocommerce-analytics' ) - : __( 'Country', 'woocommerce-analytics' ); + ? __( 'State', 'jetpack-premium-analytics' ) + : __( 'Country', 'jetpack-premium-analytics' ); // Build geo chart data const geoData: GeoData = [ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts index 63c35f0bf4f2..77552a7defd3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts @@ -2,8 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { formatDateRange } from '@wc-analytics/formatters'; -import type { ReportParams } from '@next-woo-analytics/data'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; +import type { ReportParams } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -44,7 +44,7 @@ export function formatLegendLabels( reportParams: ReportParams ): LegendLabels { from: new Date( reportParams.compare_from ), to: new Date( reportParams.compare_to ), } ) - : __( 'Previous period', 'woocommerce-analytics' ); + : __( 'Previous period', 'jetpack-premium-analytics' ); return { primary: primaryLabel, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts index a108271b474b..dc378d873219 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { formatMetricValue } from '@wc-analytics/formatters'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts index c4048ab1db45..0b01177c8622 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/fulfillment-filters.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { FilterCondition } from '@next-woo-analytics/data'; +import type { FilterCondition } from '@jetpack-premium-analytics/data'; /** * Filter for fulfilled orders only. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts index 124041ad9ce6..4f1d843b9307 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { FilterCondition } from '@next-woo-analytics/data'; +import type { FilterCondition } from '@jetpack-premium-analytics/data'; /** * Filter for order statuses relevant to payment tracking. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts index a86f4482d037..88f5241dcc22 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/product-type-filters.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { FilterCondition } from '@next-woo-analytics/data'; +import type { FilterCondition } from '@jetpack-premium-analytics/data'; /** * Product type filter constants for coupon-based widgets. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts index 49ac6e61753e..211501dbc999 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts @@ -3,7 +3,7 @@ */ import { useEffect } from 'react'; import { __ } from '@wordpress/i18n'; -import { useGlobalError } from '@next-woo-analytics/data'; +import { useGlobalError } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -80,10 +80,10 @@ export function useWidgetError( setError( { message: __( "We couldn't load this data. Please try again in a moment.", - 'woocommerce-analytics' + 'jetpack-premium-analytics' ), action: { - label: __( 'Retry', 'woocommerce-analytics' ), + label: __( 'Retry', 'jetpack-premium-analytics' ), onClick: () => { setError?.( null ); refetch?.(); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts index 2c99e25089fe..761e18d69cb9 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts @@ -1,8 +1,8 @@ /** * External dependencies */ -import { formatMetricValue } from '@wc-analytics/formatters'; -import type { ReportDataMap } from '@next-woo-analytics/data'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; export type OrdersSummary = ReportDataMap[ 'orders' ][ 'summary' ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx index 065d6871d14e..50572440f21b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportBookings } from '@next-woo-analytics/data'; -import { calendar } from '@next-woo-analytics/icons'; +import { useReportBookings } from '@jetpack-premium-analytics/data'; +import { calendar } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx index fe7faddec0ac..976ee905eaac 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx @@ -8,8 +8,8 @@ import { WidgetLoadingOverlay } from '@automattic/dashboard'; import { FilterCondition, useReportConversionRate, -} from '@next-woo-analytics/data'; -import { goal } from '@next-woo-analytics/icons'; +} from '@jetpack-premium-analytics/data'; +import { goal } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx index 1637d24857b1..318fd6eb9c92 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportCouponsByDate } from '@next-woo-analytics/data'; -import { coupon } from '@next-woo-analytics/icons'; +import { useReportCouponsByDate } from '@jetpack-premium-analytics/data'; +import { coupon } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx index 186ac05335ee..1799b8b1bf9f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportCustomersByDate } from '@next-woo-analytics/data'; -import { customer } from '@next-woo-analytics/icons'; +import { useReportCustomersByDate } from '@jetpack-premium-analytics/data'; +import { customer } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx index 3b069ca6dc73..da7d36e38859 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useReportOrders } from '@next-woo-analytics/data'; +import { useReportOrders } from '@jetpack-premium-analytics/data'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx index 80851426feed..9e5373783d08 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useReportOrders } from '@next-woo-analytics/data'; +import { useReportOrders } from '@jetpack-premium-analytics/data'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx index 92da7775219b..32a09cea2878 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx @@ -4,8 +4,8 @@ import { useMemo, useCallback } from 'react'; import { Stack } from '@wordpress/ui'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportOrders } from '@next-woo-analytics/data'; -import { reports } from '@next-woo-analytics/icons'; +import { useReportOrders } from '@jetpack-premium-analytics/data'; +import { reports } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx index 8eb87ad4b725..70beaa651cef 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportOrders } from '@next-woo-analytics/data'; -import { payment } from '@next-woo-analytics/icons'; +import { useReportOrders } from '@jetpack-premium-analytics/data'; +import { payment } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx index 9d2b451b4583..543c76af14c1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { calendar } from '@next-woo-analytics/icons'; +import { calendar } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx index 0d6a8550e992..6ba442b071b9 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx @@ -7,9 +7,9 @@ import { useReportProducts, useProductImages, type FilterCondition, -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; import { Icon } from '@wordpress/ui'; -import { productBlouse } from '@next-woo-analytics/icons'; +import { productBlouse } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx index 6df1e1d779c8..1585f5607f69 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -6,8 +6,8 @@ import { WidgetLoadingOverlay } from '@automattic/dashboard'; import { useReportCustomers, type FilterCondition, -} from '@next-woo-analytics/data'; -import { customer } from '@next-woo-analytics/icons'; +} from '@jetpack-premium-analytics/data'; +import { customer } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx index e82e410e4cd0..7b47b48ae46b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -3,8 +3,8 @@ */ import { useMemo } from 'react'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportCoupons } from '@next-woo-analytics/data'; -import { coupon } from '@next-woo-analytics/icons'; +import { useReportCoupons } from '@jetpack-premium-analytics/data'; +import { coupon } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx index 3bc94384cbab..c4ebd5e8e987 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -6,8 +6,8 @@ import { WidgetLoadingOverlay } from '@automattic/dashboard'; import { useReportOrderAttribution, type FilterCondition, -} from '@next-woo-analytics/data'; -import { device } from '@next-woo-analytics/icons'; +} from '@jetpack-premium-analytics/data'; +import { device } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx index d48657be6e04..9966fc5e05d6 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx @@ -6,8 +6,8 @@ import { WidgetLoadingOverlay } from '@automattic/dashboard'; import { useReportOrderAttribution, ORDER_ATTRIBUTION_VIEWS, -} from '@next-woo-analytics/data'; -import { megaphone, search, channel } from '@next-woo-analytics/icons'; +} from '@jetpack-premium-analytics/data'; +import { megaphone, search, channel } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx index 28a979b87e43..8cc4f54c11e6 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportSessionsByDevice } from '@next-woo-analytics/data'; -import { device } from '@next-woo-analytics/icons'; +import { useReportSessionsByDevice } from '@jetpack-premium-analytics/data'; +import { device } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx index d256d91382df..4d71062da4d0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx @@ -3,8 +3,8 @@ */ import { useMemo } from 'react'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { useReportOrders } from '@next-woo-analytics/data'; -import { paymentReturn } from '@next-woo-analytics/icons'; +import { useReportOrders } from '@jetpack-premium-analytics/data'; +import { paymentReturn } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx index 53f4c8d45c01..64f52c36e1e2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useReportVisitors } from '@next-woo-analytics/data'; +import { useReportVisitors } from '@jetpack-premium-analytics/data'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts index 8ea22f5f4175..3871d73b5c2d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts @@ -5,7 +5,7 @@ import { useMemo } from 'react'; import { type ReportParams, useReportVisitorsByLocation, -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx index 3648a7d29947..8fff540d95bc 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx @@ -11,7 +11,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { GeoChart } from '@automattic/charts'; import { WidgetLoadingOverlay } from '@automattic/dashboard'; -import { location } from '@next-woo-analytics/icons'; +import { location } from '@jetpack-premium-analytics/icons'; /** * Internal dependencies @@ -82,10 +82,10 @@ export function VisitorsByLocationWidget() { typeof item.label === 'string' ? item.label : ''; const imageAlt = region === 'US' - ? __( 'United States flag', 'woocommerce-analytics' ) + ? __( 'United States flag', 'jetpack-premium-analytics' ) : sprintf( /* translators: %s is the country name */ - __( 'Flag of %s', 'woocommerce-analytics' ), + __( 'Flag of %s', 'jetpack-premium-analytics' ), labelText ); @@ -245,7 +245,7 @@ export function VisitorsByLocationWidget() { hideLabelFromVision label={ __( 'Location', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ) } onChange={ ( value ) => { if ( isRegion( value ) ) { @@ -258,14 +258,14 @@ export function VisitorsByLocationWidget() { value="US" label={ __( 'United States', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ) } /> From 397f668d7b9bb4e8b592c8b9aa89df5ae533530d Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:25:56 +0800 Subject: [PATCH 24/32] refactor(premium-analytics): decouple widgets-toolkit from unpublished ciab-admin packages --- .../chart-empty-state/chart-empty-state.tsx | 3 +-- .../report-metric/report-metric.tsx | 2 +- .../widget-loading-overlay/index.ts | 1 + .../widget-loading-overlay.module.scss | 7 +++++ .../widget-loading-overlay.tsx | 26 +++++++++++++++++++ .../src/components/widget-root/context.tsx | 2 +- .../components/widget-root/widget-root.tsx | 4 +-- .../fields/date-report-params-field/README.md | 18 ++++++------- .../date-report-params-field.tsx | 4 +-- .../fields/metrics-field/metrics-field.tsx | 2 +- .../widgets-toolkit/src/helpers/store-info.ts | 21 +++++++++++++++ .../src/hooks/use-chart-theme.ts | 2 +- .../src/hooks/use-color-preference.ts | 24 +++++++++++++++++ .../packages/widgets-toolkit/src/types.ts | 20 +++++++++++--- .../bookings-by-attendance-widget.tsx | 2 +- .../conversion-rate-widget.tsx | 2 +- .../widgets/coupon-use/coupon-use-widget.tsx | 2 +- .../new-vs-returning-customer-widget.tsx | 2 +- .../orders-fulfillment-widget.tsx | 2 +- .../payment-status/payment-status-widget.tsx | 2 +- ...-performing-product-leaderboard-widget.tsx | 2 +- .../revenue-by-customer-type-widget.tsx | 2 +- .../sales-by-coupon-widget.tsx | 2 +- .../sales-by-device-widget.tsx | 2 +- .../sales-by-utm/sales-by-utm-widget.tsx | 2 +- .../sessions-by-device-widget.tsx | 2 +- .../total-returns/total-returns-widget.tsx | 2 +- .../visitors-by-location-widget.tsx | 2 +- 28 files changed, 128 insertions(+), 36 deletions(-) create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/store-info.ts create mode 100644 projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-color-preference.ts diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx index f0b72fe0f21b..7e7a4736ddd4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx @@ -1,8 +1,7 @@ /** * External dependencies */ -import { EmptyState } from '@automattic/design-system'; -import { Icon } from '@wordpress/ui'; +import { EmptyState, Icon } from '@wordpress/ui'; import { __ } from '@wordpress/i18n'; import { cautionFilled } from '@wordpress/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx index 320db90f4889..48231123990c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../widget-loading-overlay'; import { useGlobalChartsContext } from '@automattic/charts'; import { useMemo } from 'react'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/index.ts new file mode 100644 index 000000000000..5b5ee9f5c947 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/index.ts @@ -0,0 +1 @@ +export { WidgetLoadingOverlay } from './widget-loading-overlay'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss new file mode 100644 index 000000000000..c80a0707f40d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss @@ -0,0 +1,7 @@ +.overlay { + position: absolute; + inset: 0; + z-index: 1; + height: 100%; + background: color-mix( in srgb, var( --wpds-color-bg-surface-neutral-strong ) 60%, transparent ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx new file mode 100644 index 000000000000..0a3d6d9545d5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { Spinner } from '@wordpress/components'; +import { Stack } from '@wordpress/ui'; + +/** + * Internal dependencies + */ +import styles from './widget-loading-overlay.module.scss'; + +/** + * Local stand-in for `WidgetLoadingOverlay` from `@automattic/dashboard` + * (CIAB Admin), which is not published to npm. Renders a centered spinner + * that overlays the widget content while data is loading or refetching. + * + * TODO: Replace with the `@automattic/dashboard` component once it is + * available in the monorepo or published to npm. + */ +export function WidgetLoadingOverlay() { + return ( + + + + ); +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx index e712aa0d160a..e0aa8a914210 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx @@ -3,7 +3,7 @@ */ import { createContext, useContext } from 'react'; import type { ReportParams } from '@jetpack-premium-analytics/data'; -import type { WidgetErrorConfig } from '@automattic/dashboard'; +import type { WidgetErrorConfig } from '../../types'; export type WidgetRootContextValue = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx index cf473d8002a3..cb80b18b6047 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx @@ -6,11 +6,11 @@ import { getDefaultPreset, normalizeReportParams, } from '@jetpack-premium-analytics/data'; -import { getStoreInfo } from '@woocommerce-next/data'; +import { getStoreInfo } from '../../helpers/store-info'; import { GlobalChartsProvider } from '@automattic/charts'; import { useSearch } from '@wordpress/route'; import { useMemo, type ReactNode } from 'react'; -import type { WidgetErrorConfig } from '@automattic/dashboard'; +import type { WidgetErrorConfig } from '../../types'; import '@automattic/charts/style.css'; /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md index a005efa09820..35fbbed8293c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/README.md @@ -7,10 +7,10 @@ Form control for editing a widget's date-range parameters This field depends on two external data providers: -| Provider | Package | Purpose | -|----------|---------|---------| -| `getStoreInfo()` | `@woocommerce-next/data` | Reads `launchedDate` from the store profile | -| `getDefaultPreset()` | `@jetpack-premium-analytics/data` | Resolves a smart date-range preset based on store age | +| Provider | Package | Purpose | +| -------------------- | ------------------------------------------------------------------ | ----------------------------------------------------- | +| `getStoreInfo()` | `helpers/store-info` (local stand-in for `@woocommerce-next/data`) | Reads `launchedDate` from the store profile | +| `getDefaultPreset()` | `@jetpack-premium-analytics/data` | Resolves a smart date-range preset based on store age | ### Why the coupling exists @@ -26,9 +26,9 @@ show dates that don't match the widget's actual data range. ### Alternatives considered -| Approach | Why we didn't use it | -|----------|---------------------| -| WidgetRoot context | Modal renders outside the widget tree — context not accessible | +| Approach | Why we didn't use it | +| ------------------------- | -------------------------------------------------------------------- | +| WidgetRoot context | Modal renders outside the widget tree — context not accessible | | Prop via attribute config | `@ciab/dataviews` `DataFormControlProps` doesn't support extra props | -| Global/singleton | Adds indirection for a problem scoped to one component | -| Attribute initialization | Side-effect on render, risk of re-render loops | +| Global/singleton | Adds indirection for a problem scoped to one component | +| Attribute initialization | Side-effect on render, risk of re-render loops | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx index 3cf8017545d2..a00bdc3aa490 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx @@ -9,14 +9,14 @@ import { localTZDate, getSiteTimezone, } from '@jetpack-premium-analytics/data'; -import { getStoreInfo } from '@woocommerce-next/data'; +import { getStoreInfo } from '../../helpers/store-info'; import { endOfDay } from 'date-fns'; import { deriveComparisonRange, encodeDateToSearchParam, } from '@jetpack-premium-analytics/routing'; import { useCallback, useMemo, useState, useEffect } from 'react'; -import type { DataFormControlProps } from '@ciab/dataviews'; +import type { DataFormControlProps } from '@wordpress/dataviews'; import { type ComparisonPresetId, isPrimaryPreset, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx index 02d4ce359832..f2df81d90b28 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx @@ -5,7 +5,7 @@ import { Fieldset, Stack } from '@wordpress/ui'; import { CheckboxControl } from '@wordpress/components'; import { __, _n, sprintf } from '@wordpress/i18n'; import { useCallback, useEffect } from 'react'; -import type { DataFormControlProps } from '@ciab/dataviews'; +import type { DataFormControlProps } from '@wordpress/dataviews'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/store-info.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/store-info.ts new file mode 100644 index 000000000000..ff5103005ee7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/store-info.ts @@ -0,0 +1,21 @@ +type StoreInfo = { + /** + * ISO 8601 date string of when the store was launched, if known. + */ + launchedDate?: string; +}; + +/** + * Local stand-in for `getStoreInfo` from `@woocommerce-next/data` (next-admin), + * which is not published to npm. Only `launchedDate` is consumed by this + * package, where it feeds `getDefaultPreset( launchedDate )` — that helper + * falls back to its default preset when `launchedDate` is undefined, so + * returning an empty object keeps the behavior correct until real store info + * is available. + * + * TODO: Source store info from the analytics boot/localized settings once the + * host exposes it. + */ +export function getStoreInfo(): StoreInfo { + return {}; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts index ef0c633c4c9d..698db18620b0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { useColorPreference } from '@automattic/admin-toolkit'; +import { useColorPreference } from './use-color-preference'; import type { ChartTheme } from '@automattic/charts'; /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-color-preference.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-color-preference.ts new file mode 100644 index 000000000000..a3905f43c667 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-color-preference.ts @@ -0,0 +1,24 @@ +type ColorPreference = { + preferences: { + interfaceTheme: 'default' | 'custom'; + }; +}; + +/** + * Local stand-in for `useColorPreference` from `@automattic/admin-toolkit` + * (CIAB Admin), which is not published to npm. Jetpack Premium Analytics has + * no interface-theme preference yet, so this always reports the default + * theme, which maps to the standard WOO_COLORS chart palette in + * `useChartTheme`. + * + * TODO: Wire this to a real interface-theme preference once the host + * dashboard exposes one (or replace with the `@automattic/admin-toolkit` + * hook if it becomes available). + */ +export function useColorPreference(): ColorPreference { + return { + preferences: { + interfaceTheme: 'default', + }, + }; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts index 761e18d69cb9..e9a1ac8c80fa 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/types.ts @@ -55,11 +55,25 @@ export type MetricKey = */ type MetricFormat = NonNullable< Parameters< typeof formatMetricValue >[ 1 ] >; -type FormatMetricValueOptions = NonNullable< - Parameters< typeof formatMetricValue >[ 2 ] ->; +type FormatMetricValueOptions = NonNullable< Parameters< typeof formatMetricValue >[ 2 ] >; export type DataFormat = { type: MetricFormat; options?: FormatMetricValueOptions; }; + +/** + * Local stand-in for the `WidgetErrorConfig` type from `@automattic/dashboard` + * (CIAB Admin), which is not published to npm. Mirrors the documented shape of + * the dashboard's widget error contract: a message plus an optional action + * (e.g. a retry button). + * + * TODO: Replace with the `@automattic/dashboard` type once it is available. + */ +export type WidgetErrorConfig = { + message: string; + action?: { + label: string; + onClick: () => void; + }; +}; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx index 50572440f21b..9b6fa62723eb 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx @@ -3,7 +3,7 @@ */ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportBookings } from '@jetpack-premium-analytics/data'; import { calendar } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx index 976ee905eaac..96c280b24f54 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { ConversionFunnelChart } from '@automattic/charts'; import { Icon, Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { FilterCondition, useReportConversionRate, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx index 318fd6eb9c92..1e9d00f942a9 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx @@ -3,7 +3,7 @@ */ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCouponsByDate } from '@jetpack-premium-analytics/data'; import { coupon } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx index 1799b8b1bf9f..0ff01b43b411 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx @@ -3,7 +3,7 @@ */ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCustomersByDate } from '@jetpack-premium-analytics/data'; import { customer } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx index 32a09cea2878..1fcf945289d0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx @@ -3,7 +3,7 @@ */ import { useMemo, useCallback } from 'react'; import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrders } from '@jetpack-premium-analytics/data'; import { reports } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx index 70beaa651cef..b33a465e9bd0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx @@ -3,7 +3,7 @@ */ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrders } from '@jetpack-premium-analytics/data'; import { payment } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx index 6ba442b071b9..68249c2d234f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportProducts, useProductImages, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx index 1585f5607f69..d0288e2afde0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCustomers, type FilterCondition, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx index 7b47b48ae46b..225ab34d0e7c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCoupons } from '@jetpack-premium-analytics/data'; import { coupon } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx index c4ebd5e8e987..fed9f2fe599d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrderAttribution, type FilterCondition, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx index 9966fc5e05d6..938d95e4d101 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrderAttribution, ORDER_ATTRIBUTION_VIEWS, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx index 8cc4f54c11e6..5896e3e2a085 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx @@ -3,7 +3,7 @@ */ import { useMemo } from 'react'; import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportSessionsByDevice } from '@jetpack-premium-analytics/data'; import { device } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx index 4d71062da4d0..73203c38da30 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrders } from '@jetpack-premium-analytics/data'; import { paymentReturn } from '@jetpack-premium-analytics/icons'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx index 8fff540d95bc..537b9812bf2d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx @@ -10,7 +10,7 @@ import { useResizeObserver } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { GeoChart } from '@automattic/charts'; -import { WidgetLoadingOverlay } from '@automattic/dashboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { location } from '@jetpack-premium-analytics/icons'; /** From 36147f2411da2b260d81cf278419f3426c1bd36d Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:26:40 +0800 Subject: [PATCH 25/32] style(premium-analytics): align ported widgets-toolkit with jetpack prettier --- .../src/components/chart-bar/README.md | 42 +++----- .../chart-bar/bar-chart.module.scss | 16 ++- .../src/components/chart-bar/bar-chart.tsx | 15 +-- .../src/components/chart-bar/index.ts | 7 +- .../chart-comparative-line/README.md | 17 +--- .../comparative-line-chart.module.scss | 8 +- .../comparative-line-chart.tsx | 24 ++--- .../chart-comparative-line/types.ts | 6 +- .../utils/align-series-dates.test.ts | 97 +++++-------------- .../utils/align-series-dates.ts | 13 +-- .../src/components/chart-donut/README.md | 65 ++++++------- .../components/chart-donut/donut-chart.tsx | 31 ++---- .../src/components/chart-donut/index.ts | 6 +- .../chart-empty-state.module.scss | 4 +- .../chart-empty-state/chart-empty-state.tsx | 4 +- .../src/components/chart-empty-state/index.ts | 5 +- .../components/chart-leaderboard/README.md | 60 ++++++------ .../leaderboard-chart.module.scss | 4 +- .../chart-leaderboard/leaderboard-chart.tsx | 17 +--- .../leaderboard-label.module.scss | 6 +- .../chart-leaderboard/leaderboard-label.tsx | 7 +- .../components/chart-semi-circle/README.md | 68 ++++++------- .../chart-semi-circle/semi-circle-chart.tsx | 23 +---- .../src/components/chart-tooltip/README.md | 46 ++++----- .../chart-tooltip/chart-tooltip.module.scss | 2 +- .../chart-tooltip/chart-tooltip.tsx | 9 +- .../src/components/chart-tooltip/index.ts | 11 +-- .../chart-tooltip/pie-chart-tooltip.tsx | 11 +-- .../components/chart-tooltip/tooltip-row.tsx | 7 +- .../src/components/chart-tooltip/utils.ts | 4 +- .../widgets-toolkit/src/components/index.ts | 17 +--- .../src/components/legend/README.md | 54 ++++++----- .../components/legend/legend-with-theme.tsx | 13 +-- .../src/components/legend/legend.tsx | 19 +--- .../src/components/legend/row/legend-row.tsx | 24 +---- .../metric-delta/metric-delta.module.scss | 10 +- .../components/metric-delta/metric-delta.tsx | 25 ++--- .../metric-value/metric-value.module.scss | 6 +- .../components/metric-value/metric-value.tsx | 9 +- .../metric-with-comparison.tsx | 16 +-- .../report-metric/report-metric.tsx | 10 +- .../src/components/widget-root/README.md | 71 +++++++------- .../src/components/widget-root/context.tsx | 8 +- .../components/widget-root/widget-root.tsx | 15 +-- .../src/constants/color-palette.ts | 8 +- .../date-report-params-field.tsx | 47 +++------ .../fields/date-report-params-field/index.ts | 5 +- .../widgets-toolkit/src/fields/index.ts | 5 +- .../fields/metrics-field/metrics-field.tsx | 8 +- .../src/fields/metrics-field/metrics.ts | 8 +- .../build-sales-by-coupon-data.test.ts | 34 ++----- .../build-bookings-by-attendance-data.ts | 19 ++-- .../src/helpers/build-coupon-use-data.ts | 10 +- .../build-new-vs-returning-customer-data.ts | 13 +-- .../helpers/build-orders-fulfillment-data.ts | 17 +--- .../src/helpers/build-payment-status-data.ts | 9 +- .../build-revenue-by-customer-type-data.ts | 6 +- .../src/helpers/build-sales-by-coupon-data.ts | 9 +- .../src/helpers/build-sales-by-device-data.ts | 7 +- .../src/helpers/build-sales-by-utm-data.ts | 7 +- .../helpers/build-sessions-by-device-data.ts | 14 +-- .../helpers/build-time-series-chart-data.ts | 2 +- .../src/helpers/build-total-returns-data.ts | 8 +- .../build-visitors-by-location-data.ts | 56 ++++------- .../src/helpers/calculate-delta.ts | 5 +- .../src/helpers/chart-empty-state.ts | 10 +- .../src/helpers/format-orders-metrics.ts | 15 +-- .../widgets-toolkit/src/helpers/index.ts | 51 ++-------- .../src/helpers/payment-status-filters.ts | 8 +- .../src/helpers/segment-styles.ts | 2 +- .../use-attributes-with-search-fallback.ts | 7 +- .../src/hooks/use-chart-theme.ts | 3 +- .../src/hooks/use-series-styles.ts | 4 +- .../src/hooks/use-widget-error.ts | 4 +- .../packages/widgets-toolkit/src/index.ts | 7 +- .../src/styles/_widget-container.scss | 21 ++-- .../widgets-toolkit/src/widgets/README.md | 32 +++--- .../bookings-by-attendance-widget.tsx | 7 +- .../conversion-rate-widget.module.scss | 2 +- .../conversion-rate-widget.tsx | 19 +--- .../src/widgets/conversion-rate/index.ts | 5 +- .../widgets/coupon-use/coupon-use-widget.tsx | 10 +- .../widgets-toolkit/src/widgets/index.ts | 5 +- .../new-vs-returning-customer-widget.tsx | 14 +-- .../booking-order-metric-widget.tsx | 4 +- .../orders-fulfillment-widget.tsx | 7 +- .../payment-status/payment-status-widget.tsx | 7 +- .../top-performing-bookings-widget.tsx | 4 +- ...-performing-product-leaderboard-widget.tsx | 47 +++------ .../top-performing-products-widget.tsx | 9 +- .../revenue-by-customer-type-widget.tsx | 34 ++----- .../sales-by-coupon-widget.tsx | 20 +--- .../src/widgets/sales-by-device/index.ts | 5 +- .../sales-by-device-widget.tsx | 20 +--- .../sales-by-utm/sales-by-utm-widget.tsx | 25 +---- .../sessions-by-device-widget.tsx | 7 +- .../total-returns/total-returns-widget.tsx | 19 +--- .../use-visitors-by-location.ts | 21 ++-- .../visitors-by-location-widget.module.scss | 12 +-- .../visitors-by-location-widget.tsx | 55 +++-------- 100 files changed, 541 insertions(+), 1210 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md index a596da06b955..fa1a8bc72dd3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/README.md @@ -9,11 +9,7 @@ This component is **pure and self-contained**—it receives all styling via prop ```tsx import { BarChart } from '@jetpack-premium-analytics/widgets-toolkit'; -; +; ``` **Why this matters:** @@ -31,9 +27,7 @@ The cleanest approach is to pass styles as a separate prop. Styles are applied t ```tsx import { BarChart, type BarChartStyle } from '@jetpack-premium-analytics/widgets-toolkit'; -const styles: BarChartStyle[] = [ - { stroke: '#3858E9' }, -]; +const styles: BarChartStyle[] = [ { stroke: '#3858E9' } ]; const chartData = [ { @@ -46,11 +40,7 @@ const chartData = [ }, ]; -; +; ``` ### With styles in chartData (fallback) @@ -92,7 +82,7 @@ const revenueData = [ ; ``` @@ -123,11 +113,7 @@ const styles: BarChartStyle[] = [ { stroke: '#66BDFF' }, // Comparison - Blue 30 ]; -; +; ``` ## Using with Theme Providers @@ -150,23 +136,19 @@ function MyWidget( { chartData } ) { } ); return ( - + ); } ``` ## Props -| Prop | Type | Required | Description | -| ------------ | ----------------- | -------- | ----------------------------------------------------- | -| `chartData` | `BarChartData` | Yes | Array of series with categorical data points | +| Prop | Type | Required | Description | +| ------------ | ----------------- | -------- | ---------------------------------------------------------- | +| `chartData` | `BarChartData` | Yes | Array of series with categorical data points | | `dataFormat` | `DataFormat` | Yes | Format for values (tooltips): currency, number, percentage | -| `styles` | `BarChartStyle[]` | No | Styles for each series (by index) | -| `className` | `string` | No | CSS class for the chart container | +| `styles` | `BarChartStyle[]` | No | Styles for each series (by index) | +| `className` | `string` | No | CSS class for the chart container | ## BarChartStyle Type @@ -193,6 +175,7 @@ When all values are zero, the chart: 2. **Shows a fixed Y-axis domain** — so 0 appears at the bottom with meaningful tick values Default domains by data format: + - `currency`: 0 - 4K - `number`: 0 - 80 - `percentage`: 0% - 100% @@ -202,6 +185,7 @@ Default domains by data format: ### ChartTooltip The tooltip displays data points when hovering over bars. It uses: + - Rectangle indicators (matching bar shape) - WPDS design tokens for consistent styling - `MetricValue` component for formatted values diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss index 6da45934886d..6202e0fad9b3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss @@ -1,17 +1,15 @@ .chart { - // Override visx-bar default styles that break the layout // Todo: address upstream in Charts package. /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ - :global(.visx-bar) { + :global( .visx-bar ) { // All corners rounded to handle both positive and negative bar values consistently. - clip-path: inset(0 round 4px); + clip-path: inset( 0 round 4px ); } - .legend { - height: var(--wpds-font-line-height-lg); - min-height: var(--wpds-font-line-height-lg); + height: var( --wpds-font-line-height-lg ); + min-height: var( --wpds-font-line-height-lg ); flex-wrap: nowrap; // Vertically center the legend shape. @@ -19,14 +17,14 @@ // from the label baseline in this layout. Reset the transform here so the circle is // aligned with the accompanying text. TODO: address this default upstream in the Charts package. circle { - transform: translate(0, 0) !important; + transform: translate( 0, 0 ) !important; } } .legendItem { min-width: 0; - gap: var(--wpds-dimension-gap-sm); - padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px + gap: var( --wpds-dimension-gap-sm ); + padding: var( --wpds-dimension-padding-xs ) var( --wpds-dimension-padding-sm ); // 4px 6px->8px } .legendLabel { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx index ecbf1482c415..4c081526c228 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx @@ -23,9 +23,7 @@ export type BarChartData = ComponentProps< typeof BarChartBase >[ 'data' ]; * Inferred types from BarChart (BarChartBase) */ type BarChartBaseProps = ComponentProps< typeof BarChartBase >; -type RenderTooltipParams = Parameters< - NonNullable< BarChartBaseProps[ 'renderTooltip' ] > ->[ 0 ]; +type RenderTooltipParams = Parameters< NonNullable< BarChartBaseProps[ 'renderTooltip' ] > >[ 0 ]; /** * Style configuration for bar chart. @@ -98,7 +96,7 @@ function resolveSeriesStyles( // Fallback: extract styles from chartData options return ( - chartData?.map( ( series ) => ( { + chartData?.map( series => ( { stroke: series.options?.stroke ?? 'currentColor', } ) ) ?? [ { stroke: 'currentColor' } ] ); @@ -176,10 +174,7 @@ export function BarChart( { * Detect if chart data is empty (all values are 0). * Used to disable tooltips when there's no meaningful data to display. */ - const isEmptyData = useMemo( - () => isEmptyChartData( styledChartData ), - [ styledChartData ] - ); + const isEmptyData = useMemo( () => isEmptyChartData( styledChartData ), [ styledChartData ] ); /** * Chart options for empty data state. @@ -233,9 +228,7 @@ export function BarChart( { ); if ( isEmptyData ) { - return ( - - ); + return ; } return ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts index e2b31a36db21..189c42822bf7 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/index.ts @@ -1,6 +1 @@ -export { - BarChart, - type BarChartProps, - type BarChartData, - type BarChartStyle, -} from './bar-chart'; +export { BarChart, type BarChartProps, type BarChartData, type BarChartStyle } from './bar-chart'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md index ac016de47c3d..9f048c37a9bb 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/README.md @@ -29,10 +29,7 @@ import { ComparativeLineChart } from '@jetpack-premium-analytics/widgets-toolkit The cleanest approach is to pass styles as a separate prop. Styles are applied to series by index: ```tsx -import { - ComparativeLineChart, - type SeriesStyle, -} from '@jetpack-premium-analytics/widgets-toolkit'; +import { ComparativeLineChart, type SeriesStyle } from '@jetpack-premium-analytics/widgets-toolkit'; const styles: SeriesStyle[] = [ { stroke: '#3858E9', strokeWidth: 2 }, @@ -60,11 +57,7 @@ const series = [ }, ]; -; +; ``` ### With styles in series (fallback) @@ -122,11 +115,7 @@ function MyWidget( { series } ) { } ); return ( - + ); } ``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss index fa2581da88ea..2dfe1093d311 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss @@ -3,15 +3,15 @@ .legend { flex: 0 0 auto; - height: var(--wpds-font-line-height-lg); - min-height: var(--wpds-font-line-height-lg); + height: var( --wpds-font-line-height-lg ); + min-height: var( --wpds-font-line-height-lg ); flex-wrap: nowrap; } .legendItem { min-width: 0; - gap: var(--wpds-dimension-gap-sm); - padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px + gap: var( --wpds-dimension-gap-sm ); + padding: var( --wpds-dimension-padding-xs ) var( --wpds-dimension-padding-sm ); // 4px 6px->8px } .legendLabel { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx index 66d8a2f2a77a..56044df77c2e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx @@ -36,7 +36,7 @@ function resolveSeriesStyles( } // Fallback: extract styles from series options - return series.map( ( s ) => { + return series.map( s => { const lineStyle = s.options?.seriesLineStyle; return { @@ -92,9 +92,7 @@ function applyStylesToSeries( * Inferred types */ type LineChartProps = ComponentProps< typeof LineChart >; -type RenderTooltipParams = Parameters< - NonNullable< LineChartProps[ 'renderTooltip' ] > ->[ 0 ]; +type RenderTooltipParams = Parameters< NonNullable< LineChartProps[ 'renderTooltip' ] > >[ 0 ]; /** * Props for the ComparativeLineChart component. @@ -170,9 +168,7 @@ export function ComparativeLineChart( { const getTooltipLabel = useCallback( ( datum: { date: Date; realDate?: Date }, index: number ): string => { const isComparison = index > 0; - const displayDate = isComparison - ? datum.realDate ?? datum.date - : datum.date; + const displayDate = isComparison ? datum.realDate ?? datum.date : datum.date; return formatDate( displayDate ); }, [] @@ -223,10 +219,7 @@ export function ComparativeLineChart( { * Align comparison series dates to primary series for X-axis display. * Original dates are preserved in realDate for tooltip display. */ - const alignedSeries = useMemo( - () => alignSeriesDates( series ), - [ series ] - ); + const alignedSeries = useMemo( () => alignSeriesDates( series ), [ series ] ); /** * Apply resolved styles to series data for the internal LineChart. @@ -243,10 +236,7 @@ export function ComparativeLineChart( { /** * Detect if chart data is empty and apply special props for empty state */ - const isEmptyData = useMemo( - () => isEmptyChartData( styledSeries ), - [ styledSeries ] - ); + const isEmptyData = useMemo( () => isEmptyChartData( styledSeries ), [ styledSeries ] ); /** * For percentage metrics, always use a fixed domain [0, 1.0] (0% to 100%) @@ -332,9 +322,7 @@ export function ComparativeLineChart( { className={ clsx( styles.chart, className ) } data={ styledSeries } options={ chartOptions } - margin={ - percentageMargin ?? emptyChartProps.margin ?? DEFAULT_MARGIN - } + margin={ percentageMargin ?? emptyChartProps.margin ?? DEFAULT_MARGIN } maxWidth={ maxWidth } resizeDebounceTime={ RESIZE_DEBOUNCE_MS } withLegendGlyph={ false } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts index 415cdb54ab87..b7a121e1811c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/types.ts @@ -1,11 +1,7 @@ /** * External dependencies */ -import { - type SeriesData, - type DataPointDate, - type LineStyles, -} from '@automattic/charts'; +import { type SeriesData, type DataPointDate, type LineStyles } from '@automattic/charts'; /** * Types diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts index 97f849830886..8b64b6779abf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.test.ts @@ -30,18 +30,13 @@ describe( 'alignSeriesDates', () => { it( 'returns single series unchanged', () => { const series = [ - createSeries( 'Primary', [ - new Date( '2024-01-01' ), - new Date( '2024-01-02' ), - ] ), + createSeries( 'Primary', [ new Date( '2024-01-01' ), new Date( '2024-01-02' ) ] ), ]; const result = alignSeriesDates( series ); expect( result ).toBe( series ); // Same reference - expect( result[ 0 ].data[ 0 ].date ).toEqual( - new Date( '2024-01-01' ) - ); + expect( result[ 0 ].data[ 0 ].date ).toEqual( new Date( '2024-01-01' ) ); } ); it( 'handles series with empty data arrays', () => { @@ -89,20 +84,12 @@ describe( 'alignSeriesDates', () => { const result = alignSeriesDates( [ primary, comparison ] ); // Primary should be unchanged - expect( result[ 0 ].data[ 0 ].date ).toEqual( - new Date( '2024-01-08' ) - ); + expect( result[ 0 ].data[ 0 ].date ).toEqual( new Date( '2024-01-08' ) ); // Comparison dates should match primary dates by index - expect( result[ 1 ].data[ 0 ].date ).toEqual( - new Date( '2024-01-08' ) - ); - expect( result[ 1 ].data[ 1 ].date ).toEqual( - new Date( '2024-01-09' ) - ); - expect( result[ 1 ].data[ 2 ].date ).toEqual( - new Date( '2024-01-10' ) - ); + expect( result[ 1 ].data[ 0 ].date ).toEqual( new Date( '2024-01-08' ) ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( new Date( '2024-01-09' ) ); + expect( result[ 1 ].data[ 2 ].date ).toEqual( new Date( '2024-01-10' ) ); } ); it( 'handles weekly intervals with different start days', () => { @@ -124,20 +111,12 @@ describe( 'alignSeriesDates', () => { const result = alignSeriesDates( [ primary, comparison ] ); // Comparison should get primary's dates for perfect alignment - expect( result[ 1 ].data[ 0 ].date ).toEqual( - new Date( '2024-09-12' ) - ); - expect( result[ 1 ].data[ 1 ].date ).toEqual( - new Date( '2024-09-16' ) - ); - expect( result[ 1 ].data[ 2 ].date ).toEqual( - new Date( '2024-09-23' ) - ); + expect( result[ 1 ].data[ 0 ].date ).toEqual( new Date( '2024-09-12' ) ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( new Date( '2024-09-16' ) ); + expect( result[ 1 ].data[ 2 ].date ).toEqual( new Date( '2024-09-23' ) ); // Original dates preserved for tooltip - expect( result[ 1 ].data[ 0 ].realDate ).toEqual( - new Date( '2024-06-14' ) - ); + expect( result[ 1 ].data[ 0 ].realDate ).toEqual( new Date( '2024-06-14' ) ); } ); it( 'preserves original dates in realDate property', () => { @@ -154,21 +133,13 @@ describe( 'alignSeriesDates', () => { const result = alignSeriesDates( [ primary, comparison ] ); // Original dates preserved in realDate - expect( result[ 1 ].data[ 0 ].realDate ).toEqual( - new Date( '2024-01-01' ) - ); - expect( result[ 1 ].data[ 1 ].realDate ).toEqual( - new Date( '2024-01-02' ) - ); + expect( result[ 1 ].data[ 0 ].realDate ).toEqual( new Date( '2024-01-01' ) ); + expect( result[ 1 ].data[ 1 ].realDate ).toEqual( new Date( '2024-01-02' ) ); } ); it( 'does not add realDate to primary series', () => { - const primary = createSeries( 'This Week', [ - new Date( '2024-01-08' ), - ] ); - const comparison = createSeries( 'Last Week', [ - new Date( '2024-01-01' ), - ] ); + const primary = createSeries( 'This Week', [ new Date( '2024-01-08' ) ] ); + const comparison = createSeries( 'Last Week', [ new Date( '2024-01-01' ) ] ); const result = alignSeriesDates( [ primary, comparison ] ); @@ -209,16 +180,10 @@ describe( 'alignSeriesDates', () => { const result = alignSeriesDates( [ primary, comparison ] ); // First two points align by index - expect( result[ 1 ].data[ 0 ].date ).toEqual( - new Date( '2024-01-08' ) - ); - expect( result[ 1 ].data[ 1 ].date ).toEqual( - new Date( '2024-01-09' ) - ); + expect( result[ 1 ].data[ 0 ].date ).toEqual( new Date( '2024-01-08' ) ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( new Date( '2024-01-09' ) ); // Extra point gets last primary date - expect( result[ 1 ].data[ 2 ].date ).toEqual( - new Date( '2024-01-09' ) - ); + expect( result[ 1 ].data[ 2 ].date ).toEqual( new Date( '2024-01-09' ) ); } ); it( 'handles comparison with fewer points than primary', () => { @@ -236,12 +201,8 @@ describe( 'alignSeriesDates', () => { const result = alignSeriesDates( [ primary, comparison ] ); // Both comparison points align to their corresponding primary dates - expect( result[ 1 ].data[ 0 ].date ).toEqual( - new Date( '2024-01-08' ) - ); - expect( result[ 1 ].data[ 1 ].date ).toEqual( - new Date( '2024-01-09' ) - ); + expect( result[ 1 ].data[ 0 ].date ).toEqual( new Date( '2024-01-08' ) ); + expect( result[ 1 ].data[ 1 ].date ).toEqual( new Date( '2024-01-09' ) ); } ); } ); @@ -265,23 +226,13 @@ describe( 'alignSeriesDates', () => { const result = alignSeriesDates( [ primary, lastMonth, lastYear ] ); // All series should now use primary's dates - expect( result[ 0 ].data[ 0 ].date ).toEqual( - new Date( '2024-03-01' ) - ); - expect( result[ 1 ].data[ 0 ].date ).toEqual( - new Date( '2024-03-01' ) - ); - expect( result[ 2 ].data[ 0 ].date ).toEqual( - new Date( '2024-03-01' ) - ); + expect( result[ 0 ].data[ 0 ].date ).toEqual( new Date( '2024-03-01' ) ); + expect( result[ 1 ].data[ 0 ].date ).toEqual( new Date( '2024-03-01' ) ); + expect( result[ 2 ].data[ 0 ].date ).toEqual( new Date( '2024-03-01' ) ); // Original dates preserved - expect( result[ 1 ].data[ 0 ].realDate ).toEqual( - new Date( '2024-02-01' ) - ); - expect( result[ 2 ].data[ 0 ].realDate ).toEqual( - new Date( '2023-03-01' ) - ); + expect( result[ 1 ].data[ 0 ].realDate ).toEqual( new Date( '2024-02-01' ) ); + expect( result[ 2 ].data[ 0 ].realDate ).toEqual( new Date( '2023-03-01' ) ); } ); } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts index 0c807c70d9bf..05a2ecf65e33 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/utils/align-series-dates.ts @@ -31,7 +31,7 @@ export function alignSeriesDates( return series; } - const alignedRest = rest.map( ( comparisonSeries ) => { + const alignedRest = rest.map( comparisonSeries => { if ( ! comparisonSeries.data.length ) { return comparisonSeries; } @@ -41,14 +41,10 @@ export function alignSeriesDates( const comparisonFirstDate = comparisonSeries.data[ 0 ]?.date; const primaryFirstMs = - primaryFirstDate instanceof Date - ? primaryFirstDate.getTime() - : primaryFirstDate; + primaryFirstDate instanceof Date ? primaryFirstDate.getTime() : primaryFirstDate; const comparisonFirstMs = - comparisonFirstDate instanceof Date - ? comparisonFirstDate.getTime() - : comparisonFirstDate; + comparisonFirstDate instanceof Date ? comparisonFirstDate.getTime() : comparisonFirstDate; // If dates already align, return as-is if ( primaryFirstMs === comparisonFirstMs ) { @@ -61,8 +57,7 @@ export function alignSeriesDates( data: comparisonSeries.data.map( ( point, index ) => { // Use corresponding primary date, or last primary date if comparison has more points const primaryDate = - primary.data[ index ]?.date ?? - primary.data[ primary.data.length - 1 ]?.date; + primary.data[ index ]?.date ?? primary.data[ primary.data.length - 1 ]?.date; return { ...point, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md index 71eb3bd88264..c4c940363158 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/README.md @@ -17,40 +17,36 @@ A responsive donut (pie) chart component that automatically adapts to its contai import { DonutChart } from '@jetpack-premium-analytics/widgets-toolkit'; const chartData = [ - { label: 'Completed', value: 45, percentage: 56.25 }, - { label: 'Pending', value: 25, percentage: 31.25 }, - { label: 'Cancelled', value: 10, percentage: 12.5 }, + { label: 'Completed', value: 45, percentage: 56.25 }, + { label: 'Pending', value: 25, percentage: 31.25 }, + { label: 'Cancelled', value: 10, percentage: 12.5 }, ]; -const styles = [ - { color: '#3858E9' }, - { color: '#66BDFF' }, - { color: '#A77EFF' }, -]; +const styles = [ { color: '#3858E9' }, { color: '#66BDFF' }, { color: '#A77EFF' } ]; + chartData={ chartData } + styles={ styles } + value={ 80 } + comparisonValue={ 72 } + showLegend={ true } + legendData={ legendItems } + dataFormat={ { type: 'number' } } +/>; ``` ## Props -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `chartData` | `DonutChartData` | required | Array of segments with `label`, `value`, and `percentage` | -| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | -| `value` | `number` | required | Primary metric value displayed in center | -| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | -| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | -| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | -| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | -| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | +| Prop | Type | Default | Description | +| ----------------- | ---------------- | -------------------- | ------------------------------------------------------------------ | +| `chartData` | `DonutChartData` | required | Array of segments with `label`, `value`, and `percentage` | +| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | +| `value` | `number` | required | Primary metric value displayed in center | +| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | +| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | +| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | +| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | +| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | ## Data Validation @@ -90,6 +86,7 @@ The component uses a reference/wrapper pattern to achieve fluid sizing: ### Default dimensions Before the first resize observation, the chart uses sensible defaults: + - Size: 164px (width and height) ## Storybook @@ -128,8 +125,8 @@ const styles = [ ```tsx const chartData = [ - { label: 'Completed', value: 45, percentage: 56, color: '#3858E9' }, - { label: 'Pending', value: 25, percentage: 31, color: '#66BDFF' }, + { label: 'Completed', value: 45, percentage: 56, color: '#3858E9' }, + { label: 'Pending', value: 25, percentage: 31, color: '#66BDFF' }, ]; ``` @@ -152,9 +149,9 @@ const segmentStyles = chartData.map( ( segment, index ) => { ## Comparison with SemiCircleChart -| Feature | DonutChart | SemiCircleChart | -|---------|------------|-----------------| -| Shape | Full circle | Half circle | -| Use case | Status distribution | Two-segment comparison | -| Default size | 164px | 220x100px | -| Metric position | Center | Bottom center | +| Feature | DonutChart | SemiCircleChart | +| --------------- | ------------------- | ---------------------- | +| Shape | Full circle | Half circle | +| Use case | Status distribution | Two-segment comparison | +| Default size | 164px | 220x100px | +| Metric position | Center | Bottom center | diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx index 1e4420992a52..e12434cafdc4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx @@ -139,8 +139,7 @@ export function DonutChart( { tooltipOffsetY, tooltipDataFormat, }: DonutChartProps ) { - const hasComparison = - comparisonValue !== null && comparisonValue !== undefined; + const hasComparison = comparisonValue !== null && comparisonValue !== undefined; const [ widgetHeight, setWidgetHeight ] = useState< number >( 0 ); @@ -150,7 +149,7 @@ export function DonutChart( { */ const [ chartWidth, setChartWidth ] = useState< number >( 0 ); - const ref = useResizeObserver( ( entries ) => { + const ref = useResizeObserver( entries => { const entry = entries?.[ 0 ]; if ( ! entry?.contentRect ) { return; @@ -196,24 +195,13 @@ export function DonutChart( { // Render empty state when no data is available if ( isEmptyData ) { - return ( - - ); + return ; } return ( -
+
- + @@ -252,10 +238,7 @@ export function DonutChart( { { showLegend && styledLegendData && (
- +
) }
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts index d57f81465c1f..dca9e0328af7 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/index.ts @@ -1,5 +1 @@ -export { - DonutChart, - type DonutChartProps, - type DonutChartData, -} from './donut-chart'; +export { DonutChart, type DonutChartProps, type DonutChartData } from './donut-chart'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss index b06aad82338f..9f0076b968a1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss @@ -4,9 +4,9 @@ justify-content: center; height: 100%; width: 100%; - gap: var(--wpds-dimension-gap-lg); + gap: var( --wpds-dimension-gap-lg ); } .icon { - color: var(--wpds-color-stroke-surface-neutral-weak, #E0E0E0); + color: var( --wpds-color-stroke-surface-neutral-weak, #e0e0e0 ); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx index 7e7a4736ddd4..9fe852dc7f5e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx @@ -51,9 +51,7 @@ export function ChartEmptyState( { }: ChartEmptyStateProps ) { return ( - { icon && ( - - ) } + { icon && } { text } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts index e0bdf02935d9..9e6519d64689 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/index.ts @@ -1,4 +1 @@ -export { - ChartEmptyState, - type ChartEmptyStateProps, -} from './chart-empty-state'; +export { ChartEmptyState, type ChartEmptyStateProps } from './chart-empty-state'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md index 12fd9172fb53..0ca44ba61216 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/README.md @@ -4,14 +4,14 @@ A responsive leaderboard (horizontal bar) chart component for displaying ranking ## Features -- **Context-aware styling**: Integrates with GlobalChartsProvider for consistent theming -- **Comparison mode**: Shows current vs. previous period data with delta indicators -- **Flexible formatting**: Supports currency, number, percentage, and custom formats -- **Empty state handling**: Built-in empty state with customizable content -- **Legend support**: Optional legend with customizable labels -- **Overlay labels**: Alternative styling with labels on top of bars -- **Loading states**: Skeleton loaders during data fetch -- **Long label handling**: Automatic truncation and tooltips for long labels +- **Context-aware styling**: Integrates with GlobalChartsProvider for consistent theming +- **Comparison mode**: Shows current vs. previous period data with delta indicators +- **Flexible formatting**: Supports currency, number, percentage, and custom formats +- **Empty state handling**: Built-in empty state with customizable content +- **Legend support**: Optional legend with customizable labels +- **Overlay labels**: Alternative styling with labels on top of bars +- **Loading states**: Skeleton loaders during data fetch +- **Long label handling**: Automatic truncation and tooltips for long labels ## Requirements @@ -166,11 +166,7 @@ type DataFormat = { ### With Overlay Labels ```tsx - + ``` ### Custom Empty State @@ -208,9 +204,7 @@ import { LeaderboardChart } from '@jetpack-premium-analytics/widgets-toolkit'; function MyWidget() { return ( - + ); @@ -219,9 +213,9 @@ function MyWidget() { The component uses `getElementStyles()` from the context to: -- Retrieve primary and secondary colors for bars -- Apply consistent theming across all charts -- Support both current period (index 0) and comparison period (index 1) colors +- Retrieve primary and secondary colors for bars +- Apply consistent theming across all charts +- Support both current period (index 0) and comparison period (index 1) colors ## Empty State Behavior @@ -239,25 +233,25 @@ When `loading={true}`, the component displays skeleton loaders that match the st The LeaderboardChart automatically adapts to its container width. For optimal display: -- **Minimum width**: 280px recommended -- **Ideal width**: 400px+ for comfortable reading -- **Label truncation**: Long labels automatically truncate with ellipsis -- **Bar scaling**: Bars scale proportionally to container width +- **Minimum width**: 280px recommended +- **Ideal width**: 400px+ for comfortable reading +- **Label truncation**: Long labels automatically truncate with ellipsis +- **Bar scaling**: Bars scale proportionally to container width ## Storybook Run `pnpm storybook` and navigate to **Widgets Toolkit / Components / LeaderboardChart** to see: -- **Default** - Basic leaderboard without comparison -- **WithComparison** - Current vs. previous period -- **Loading** - Loading skeleton state -- **EmptyState** - No data handling -- **WithOverlayLabel** - Labels on top of bars -- **WithoutLegend** - Chart without legend -- **LongLabels** - Label truncation handling -- **NumberFormat** - Number formatting (not currency) -- **PercentageFormat** - Percentage values -- **Container size variants** - Small (280px), Medium (400px), Large (600px) +- **Default** - Basic leaderboard without comparison +- **WithComparison** - Current vs. previous period +- **Loading** - Loading skeleton state +- **EmptyState** - No data handling +- **WithOverlayLabel** - Labels on top of bars +- **WithoutLegend** - Chart without legend +- **LongLabels** - Label truncation handling +- **NumberFormat** - Number formatting (not currency) +- **PercentageFormat** - Percentage values +- **Container size variants** - Small (280px), Medium (400px), Large (600px) ## Comparison with Other Chart Components diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss index 31919b013c4b..c306a3f338a2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss @@ -30,13 +30,13 @@ } .emptyStateIcon { - color: var(--wpds-color-fg-content-neutral-weak); + color: var( --wpds-color-fg-content-neutral-weak ); opacity: 0.5; } .emptyStateText { margin: 0; - color: var(--wpds-color-fg-content-neutral); + color: var( --wpds-color-fg-content-neutral ); font-size: 14px; text-align: center; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx index 869bbfd9c6bc..6edecdcc802e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx @@ -21,9 +21,7 @@ import type { WooChartTheme } from '../../hooks/use-chart-theme'; import { ChartEmptyState } from '../chart-empty-state'; import styles from './leaderboard-chart.module.scss'; -type LeaderboardChartData = ComponentProps< - typeof BaseLeaderboardChart ->[ 'data' ]; +type LeaderboardChartData = ComponentProps< typeof BaseLeaderboardChart >[ 'data' ]; export type { LeaderboardChartData }; @@ -134,8 +132,7 @@ export function LeaderboardChart( { * Create value formatter from dataFormat configuration */ const valueFormatter = useMemo( - () => ( value: number ) => - formatMetricValue( value, dataFormat.type, dataFormat.options ), + () => ( value: number ) => formatMetricValue( value, dataFormat.type, dataFormat.options ), [ dataFormat ] ); @@ -171,9 +168,7 @@ export function LeaderboardChart( { const isEmptyData = ! data || data.length === 0; if ( isEmptyData ) { - return ( - - ); + return ; } return ( @@ -189,11 +184,7 @@ export function LeaderboardChart( { withComparison={ withComparison } valueFormatter={ valueFormatter } legendLabels={ legendLabels } - primaryColor={ - withOverlayLabel - ? hexToRgba( chartColors.primaryColor, 0.08 ) - : undefined - } + primaryColor={ withOverlayLabel ? hexToRgba( chartColors.primaryColor, 0.08 ) : undefined } withOverlayLabel={ withOverlayLabel } showLegend={ false } style={ chartStyle } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss index d1bc27cbfb8a..61b2c1c5a20e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss @@ -1,15 +1,15 @@ .container { - padding: var(--wpds-dimension-padding-sm); + padding: var( --wpds-dimension-padding-sm ); } .label { - font-size: var(--wpds-font-size-sm); + font-size: var( --wpds-font-size-sm ); } .labelImage { width: 28px; height: 28px; vertical-align: middle; - border-radius: var(--wpds-border-radius-md); + border-radius: var( --wpds-border-radius-md ); object-fit: cover; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx index 13fffb7e3b4e..9d70da3192f5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx @@ -59,12 +59,7 @@ export function LeaderboardLabel( { const finalImageUrl = imageUrl || DEFAULT_IMAGE_URL; return ( - + ) => { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md index 4194cea6e225..b044f4399632 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/README.md @@ -17,48 +17,44 @@ A responsive semi-circle (half-donut) chart component that fills its parent cont import { SemiCircleChart } from '@jetpack-premium-analytics/widgets-toolkit'; const chartData = [ - { label: 'Mobile', value: 4500 }, - { label: 'Desktop', value: 2500 }, - { label: 'Tablet', value: 1000 }, + { label: 'Mobile', value: 4500 }, + { label: 'Desktop', value: 2500 }, + { label: 'Tablet', value: 1000 }, ]; -const styles = [ - { color: '#3858E9' }, - { color: '#66BDFF' }, - { color: '#A77EFF' }, -]; +const styles = [ { color: '#3858E9' }, { color: '#66BDFF' }, { color: '#A77EFF' } ]; + chartData={ chartData } + styles={ styles } + value={ 8000 } + comparisonValue={ 7450 } + showLegend={ true } + legendData={ legendItems } + dataFormat={ { type: 'number', options: { useMultipliers: true } } } + withTooltips +/>; ``` ## Props -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `chartData` | `SemiCircleChartData` | required | Array of segments with `label` and `value` (percentage is auto-calculated) | -| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | -| `value` | `number` | required | Primary metric value displayed in center | -| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | -| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | -| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | -| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | -| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | -| `maxWidth` | `number` | `Infinity` | Maximum width constraint for the chart | -| `withTooltips` | `boolean` | `false` | Enable tooltips on hover | -| `tooltipOffsetX` | `number` | - | Horizontal offset for tooltip positioning | -| `tooltipOffsetY` | `number` | - | Vertical offset for tooltip positioning | -| `tooltipDataFormat` | `DataFormat` | - | Format for tooltip values (falls back to `dataFormat`) | -| `emptyStateIcon` | `IconProps['icon']` | - | Icon for empty state | -| `emptyStateText` | `string` | - | Text for empty state | +| Prop | Type | Default | Description | +| ------------------- | --------------------- | -------------------- | -------------------------------------------------------------------------- | +| `chartData` | `SemiCircleChartData` | required | Array of segments with `label` and `value` (percentage is auto-calculated) | +| `styles` | `SegmentStyle[]` | - | Explicit colors per segment (takes priority over chartData colors) | +| `value` | `number` | required | Primary metric value displayed in center | +| `comparisonValue` | `number \| null` | - | Previous period value for delta calculation | +| `dataFormat` | `DataFormat` | `{ type: 'number' }` | Format configuration for the metric display | +| `legendData` | `LegendItem[]` | - | Legend items with labels and comparison values | +| `showLegend` | `boolean` | `true` | Whether to show the legend below the chart | +| `thickness` | `number` | `0.3` | Arc thickness as ratio (0-1) | +| `maxWidth` | `number` | `Infinity` | Maximum width constraint for the chart | +| `withTooltips` | `boolean` | `false` | Enable tooltips on hover | +| `tooltipOffsetX` | `number` | - | Horizontal offset for tooltip positioning | +| `tooltipOffsetY` | `number` | - | Vertical offset for tooltip positioning | +| `tooltipDataFormat` | `DataFormat` | - | Format for tooltip values (falls back to `dataFormat`) | +| `emptyStateIcon` | `IconProps['icon']` | - | Icon for empty state | +| `emptyStateText` | `string` | - | Text for empty state | ## Responsive Layout @@ -104,8 +100,8 @@ const styles = [ ```tsx const chartData = [ - { label: 'Mobile', value: 4500, color: '#3858E9' }, - { label: 'Desktop', value: 2500, color: '#66BDFF' }, + { label: 'Mobile', value: 4500, color: '#3858E9' }, + { label: 'Desktop', value: 2500, color: '#66BDFF' }, ]; ``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx index 374195855490..6f639b6e8110 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx @@ -27,9 +27,7 @@ import { RESIZE_DEBOUNCE_MS } from '../../constants'; // Default chart configuration const DEFAULT_THICKNESS = 0.3; -export type SemiCircleChartData = ComponentProps< - typeof PieSemiCircleChart ->[ 'data' ]; +export type SemiCircleChartData = ComponentProps< typeof PieSemiCircleChart >[ 'data' ]; export type SemiCircleChartProps = { /** @@ -144,8 +142,7 @@ export function SemiCircleChart( { tooltipOffsetY, tooltipDataFormat, }: SemiCircleChartProps ) { - const hasComparison = - comparisonValue !== null && comparisonValue !== undefined; + const hasComparison = comparisonValue !== null && comparisonValue !== undefined; /** * Resolve styles: prop takes priority, fallback to chartData colors. @@ -179,18 +176,11 @@ export function SemiCircleChart( { // Render empty state when no data is available if ( isEmptyData ) { - return ( - - ); + return ; } return ( - + { showLegend && styledLegendData && ( - + ) } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md index cfd2d4a0c417..9f3bd613f552 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/README.md @@ -17,7 +17,7 @@ A **shared** tooltip component for chart visualizations. Supports both line char ```tsx import { ChartTooltip } from '../chart-tooltip'; -const renderTooltip = ( params ) => ( +const renderTooltip = params => ( ( ```tsx import { ChartTooltip } from '../chart-tooltip'; -const renderTooltip = ( params ) => ( +const renderTooltip = params => ( ( ## Props -| Prop | Type | Required | Description | -|-----------------|-------------------------------------------|----------|-----------------------------------------------------------| -| `tooltipData` | `{ datumByKey?: Record }`| No | Tooltip data from visx chart | -| `dataFormat` | `DataFormat` | Yes | Format for values: currency, number, percentage | -| `seriesStyles` | `TooltipStyle[]` | Yes | Styles for each series (color, stroke properties) | -| `indicatorType` | `'line' \| 'rect'` | Yes | Shape indicator: line for line charts, rect for bars | -| `getLabel` | `(datum, index, key) => string` | No | Custom label extractor. `key` is the series key/label (default: `datum.label`) | -| `getValue` | `(datum) => number` | No | Custom value extractor (default: `datum.value`) | - +| Prop | Type | Required | Description | +| --------------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------ | +| `tooltipData` | `{ datumByKey?: Record }` | No | Tooltip data from visx chart | +| `dataFormat` | `DataFormat` | Yes | Format for values: currency, number, percentage | +| `seriesStyles` | `TooltipStyle[]` | Yes | Styles for each series (color, stroke properties) | +| `indicatorType` | `'line' \| 'rect'` | Yes | Shape indicator: line for line charts, rect for bars | +| `getLabel` | `(datum, index, key) => string` | No | Custom label extractor. `key` is the series key/label (default: `datum.label`) | +| `getValue` | `(datum) => number` | No | Custom value extractor (default: `datum.value`) | ## TooltipStyle Type @@ -153,19 +152,16 @@ Reuses the same SCSS module as `ChartTooltip` so styling (box-shadow, padding, v import { PieChartTooltip } from '../chart-tooltip'; const renderTooltip = ( { tooltipData } ) => ( - + ); ``` ## Props -| Prop | Type | Required | Description | -|---------------|-----------------------|----------|----------------------------------------------------| +| Prop | Type | Required | Description | +| ------------- | --------------------- | -------- | ------------------------------------------------------- | | `tooltipData` | `DataPointPercentage` | Yes | Tooltip data from pie chart hover (label, value, color) | -| `dataFormat` | `DataFormat` | Yes | Format for values: currency, number, percentage | +| `dataFormat` | `DataFormat` | Yes | Format for values: currency, number, percentage | ## Used By @@ -189,17 +185,17 @@ import { RectShape } from '@automattic/charts/visx/legend'; label="Revenue" value={ 1234.56 } dataFormat={ { type: 'currency' } } -/> +/>; ``` ## Props -| Prop | Type | Required | Description | -|-------------|-------------------|----------|-----------------------------------------------------| -| `indicator` | `React.ReactNode` | Yes | Pre-rendered indicator element (LineShape, RectShape, etc.) | -| `label` | `string` | Yes | Row label text | -| `value` | `number` | Yes | Numeric value to format | -| `dataFormat`| `DataFormat` | Yes | Format configuration (currency, number, percentage) | +| Prop | Type | Required | Description | +| ------------ | ----------------- | -------- | ----------------------------------------------------------- | +| `indicator` | `React.ReactNode` | Yes | Pre-rendered indicator element (LineShape, RectShape, etc.) | +| `label` | `string` | Yes | Row label text | +| `value` | `number` | Yes | Numeric value to format | +| `dataFormat` | `DataFormat` | Yes | Format configuration (currency, number, percentage) | ## Used By diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss index e45308e68118..133e474b7e81 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss @@ -18,7 +18,7 @@ // Override visx-tooltip ONLY when our custom tooltip components are used. // This applies to ChartTooltip (line/bar) and PieChartTooltip (pie/semi-circle). /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ -:global(.visx-tooltip):has( .tooltip ) { +:global( .visx-tooltip ):has( .tooltip ) { max-width: none !important; box-shadow: none !important; margin: 0 !important; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx index dce120f41b30..2bc3566f432f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx @@ -129,8 +129,7 @@ export function ChartTooltip< TDatum >( { return null; } - const { stroke, ...lineShapeStyle } = - seriesStyles[ index ] || seriesStyles[ 0 ]; + const { stroke, ...lineShapeStyle } = seriesStyles[ index ] || seriesStyles[ 0 ]; const label = getLabel( entry.datum, index, entry.key ); const value = getValue( entry.datum ); @@ -146,11 +145,7 @@ export function ChartTooltip< TDatum >( { style={ lineShapeStyle } /> ) : ( - + ) } label={ label } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts index c3e6abcb721c..9f4d39696756 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/index.ts @@ -1,11 +1,4 @@ -export { - ChartTooltip, - type ChartTooltipProps, - type TooltipStyle, -} from './chart-tooltip'; -export { - PieChartTooltip, - type PieChartTooltipProps, -} from './pie-chart-tooltip'; +export { ChartTooltip, type ChartTooltipProps, type TooltipStyle } from './chart-tooltip'; +export { PieChartTooltip, type PieChartTooltipProps } from './pie-chart-tooltip'; export { TooltipRow, type TooltipRowProps } from './tooltip-row'; export { isChartDatumEntry, type ChartDatumEntry } from './utils'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx index 7c39b24f40e0..31a194156181 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx @@ -31,19 +31,12 @@ export type PieChartTooltipProps = { * Reuses the same SCSS module as ChartTooltip so styling (box-shadow, padding, * the `:global(.visx-tooltip):has(.tooltip)` override) is shared. */ -export function PieChartTooltip( { - tooltipData, - dataFormat, -}: PieChartTooltipProps ) { +export function PieChartTooltip( { tooltipData, dataFormat }: PieChartTooltipProps ) { return ( + } label={ tooltipData.label } value={ tooltipData.value } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx index 526d49631833..6c134546c375 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx @@ -21,12 +21,7 @@ export type TooltipRowProps = { dataFormat: DataFormat; }; -export function TooltipRow( { - indicator, - label, - value, - dataFormat, -}: TooltipRowProps ) { +export function TooltipRow( { indicator, label, value, dataFormat }: TooltipRowProps ) { return ( = { * @param entry The entry to check. * @return True if the entry has the expected structure. */ -export const isChartDatumEntry = < T >( - entry: unknown -): entry is ChartDatumEntry< T > => { +export const isChartDatumEntry = < T >( entry: unknown ): entry is ChartDatumEntry< T > => { return ( typeof entry === 'object' && entry !== null && diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts index ea68110f259e..121c13e7227b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts @@ -1,10 +1,7 @@ export { MetricDelta } from './metric-delta'; export { MetricValue } from './metric-value'; export { MetricWithComparison } from './metric-with-comparison'; -export { - ComparativeLineChart, - type SeriesStyle, -} from './chart-comparative-line'; +export { ComparativeLineChart, type SeriesStyle } from './chart-comparative-line'; export { Legend, type LegendItem } from './legend'; export { WidgetRoot, useWidgetRootContext } from './widget-root'; @@ -19,13 +16,5 @@ export { LeaderboardLabel, type LeaderboardLabelProps, } from './chart-leaderboard'; -export { - BarChart, - type BarChartProps, - type BarChartData, - type BarChartStyle, -} from './chart-bar'; -export { - ChartEmptyState, - type ChartEmptyStateProps, -} from './chart-empty-state'; +export { BarChart, type BarChartProps, type BarChartData, type BarChartStyle } from './chart-bar'; +export { ChartEmptyState, type ChartEmptyStateProps } from './chart-empty-state'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md index 2f623e19a31e..243cc32d174b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/README.md @@ -8,40 +8,52 @@ A pure component for rendering chart legends with optional comparison deltas. import { Legend } from '@jetpack-premium-analytics/widgets-toolkit'; const items = [ - { label: 'Mobile', value: 241950, displayValue: '$241.95K', color: '#3858E9' }, - { label: 'Desktop', value: 148130, displayValue: '$148.13K', color: '#66BDFF' }, - { label: 'Tablet', value: 44740, displayValue: '$44.74K', color: '#A77EFF' }, + { label: 'Mobile', value: 241950, displayValue: '$241.95K', color: '#3858E9' }, + { label: 'Desktop', value: 148130, displayValue: '$148.13K', color: '#66BDFF' }, + { label: 'Tablet', value: 44740, displayValue: '$44.74K', color: '#A77EFF' }, ]; - +; ``` ## Props -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| `items` | `LegendItem[]` | required | Array of legend items to display | -| `withComparison` | `boolean` | `false` | Show comparison deltas | +| Prop | Type | Default | Description | +| ---------------- | -------------- | -------- | -------------------------------- | +| `items` | `LegendItem[]` | required | Array of legend items to display | +| `withComparison` | `boolean` | `false` | Show comparison deltas | ### LegendItem -| Property | Type | Required | Description | -|----------|------|----------|-------------| -| `label` | `string` | yes | Item label text | -| `value` | `number` | yes | Current numeric value | -| `displayValue` | `string` | yes | Display-ready formatted value | -| `color` | `string` | no | Bullet color (hex, rgb, etc.) | -| `comparison` | `number` | no | Previous value for delta calculation | +| Property | Type | Required | Description | +| -------------- | -------- | -------- | ------------------------------------ | +| `label` | `string` | yes | Item label text | +| `value` | `number` | yes | Current numeric value | +| `displayValue` | `string` | yes | Display-ready formatted value | +| `color` | `string` | no | Bullet color (hex, rgb, etc.) | +| `comparison` | `number` | no | Previous value for delta calculation | ## With Comparison ```tsx const items = [ - { label: 'Mobile', value: 241950, displayValue: '$241.95K', color: '#3858E9', comparison: 200000 }, - { label: 'Desktop', value: 148130, displayValue: '$148.13K', color: '#66BDFF', comparison: 160000 }, + { + label: 'Mobile', + value: 241950, + displayValue: '$241.95K', + color: '#3858E9', + comparison: 200000, + }, + { + label: 'Desktop', + value: 148130, + displayValue: '$148.13K', + color: '#66BDFF', + comparison: 160000, + }, ]; - +; ``` ## Theme Integration @@ -53,10 +65,8 @@ import { LegendWithTheme as Legend } from '@jetpack-premium-analytics/widgets-to // Colors are injected from theme - no need to specify them ( - - ) } -/> + render={ chartItems => } +/>; ``` ## Architecture diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx index 6d5f55c9bf2f..df56c2229486 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx @@ -1,10 +1,7 @@ /** * External dependencies */ -import { - type BaseLegendItem, - useGlobalChartsContext, -} from '@automattic/charts'; +import { type BaseLegendItem, useGlobalChartsContext } from '@automattic/charts'; /** * Internal dependencies @@ -33,9 +30,7 @@ function resolveItemColor( return item.color; } - const correspondingChartItem = chartItems?.find( - ( chartItem ) => chartItem.label === item.label - ); + const correspondingChartItem = chartItems?.find( chartItem => chartItem.label === item.label ); if ( correspondingChartItem?.color ) { return correspondingChartItem.color; @@ -66,7 +61,5 @@ export function LegendWithTheme( { color: resolveItemColor( item, index, chartItems, getElementStyles ), } ) ); - return ( - - ); + return ; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx index fe3e96b9a8eb..79fe63740322 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx @@ -45,21 +45,14 @@ type LegendProps = { /** * Determines the number of grid columns based on visibility options. */ -function getTemplateColumns( - hideValue: boolean, - withComparison: boolean -): string { +function getTemplateColumns( hideValue: boolean, withComparison: boolean ): string { if ( hideValue ) { return withComparison ? '1fr auto' : '1fr'; } return withComparison ? '1fr auto auto' : '1fr auto'; } -export function Legend( { - items, - withComparison = false, - hideValue = false, -}: LegendProps ) { +export function Legend( { items, withComparison = false, hideValue = false }: LegendProps ) { return ( - { items.map( ( item ) => ( + { items.map( item => ( + ) : null } color={ item.color } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx index c35f9a6bff4c..a7f8d7e65bb2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx @@ -37,32 +37,16 @@ export type LegendRowProps = { title?: string; }; -export function LegendRow( { - children, - value, - comparison, - color, - title, -}: LegendRowProps ) { +export function LegendRow( { children, value, comparison, color, title }: LegendRowProps ) { return ( <> - -
+ +
{ children } - { value !== false && ( - { value } - ) } + { value !== false && { value } } { comparison } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss index 304a53577c06..d31aff2462ef 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss @@ -1,18 +1,18 @@ .delta { - font-size: var(--wpds-font-size-md); + font-size: var( --wpds-font-size-md ); font-weight: 400; - line-height: var(--wpds-font-size-lg); + line-height: var( --wpds-font-size-lg ); &.invalid, &.neutral { - color: var(--wpds-color-fg-content-neutral-weak); + color: var( --wpds-color-fg-content-neutral-weak ); } &.positive { - color: var(--wpds-color-stroke-surface-success-strong); + color: var( --wpds-color-stroke-surface-success-strong ); } &.negative { - color: var(--wpds-color-stroke-surface-error-strong); + color: var( --wpds-color-stroke-surface-error-strong ); } } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx index da5f09ef8170..6fa96f4a9403 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx @@ -64,10 +64,7 @@ export type MetricDeltaProps = { absoluteFormat?: 'number' | 'currency'; }; -function calculatePercentageChange( - current: number, - previous: number -): number | null { +function calculatePercentageChange( current: number, previous: number ): number | null { // Handle invalid inputs if ( ! Number.isFinite( current ) || ! Number.isFinite( previous ) ) { return null; @@ -79,9 +76,7 @@ function calculatePercentageChange( } // Calculate percentage change, rounded to integer - return Math.round( - ( ( current - previous ) / Math.abs( previous ) ) * 100 - ); + return Math.round( ( ( current - previous ) / Math.abs( previous ) ) * 100 ); } export function MetricDelta( { @@ -102,10 +97,7 @@ export function MetricDelta( { // Handle edge cases if ( percentageChange === null ) { return ( - + { fallback } ); @@ -123,19 +115,14 @@ export function MetricDelta( { displayValue = `+${ displayValue }`; } } else { - displayValue = formatMetricValue( - percentageChange / 100, - 'percentage' - ); + displayValue = formatMetricValue( percentageChange / 100, 'percentage' ); } // Determine color based on direction and inversion const isPositive = - ( percentageChange > 0 && ! invertColors ) || - ( percentageChange < 0 && invertColors ); + ( percentageChange > 0 && ! invertColors ) || ( percentageChange < 0 && invertColors ); const isNegative = - ( percentageChange < 0 && ! invertColors ) || - ( percentageChange > 0 && invertColors ); + ( percentageChange < 0 && ! invertColors ) || ( percentageChange > 0 && invertColors ); return ( { displayValue } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx index f6772f110c4a..c233b0424efa 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx @@ -94,21 +94,11 @@ export function MetricWithComparison( { /** * Determine absolute format for delta based on data type */ - const absoluteFormat = - dataFormat.type === 'currency' ? 'currency' : 'number'; + const absoluteFormat = dataFormat.type === 'currency' ? 'currency' : 'number'; return ( - - + + { showDelta && ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md index a09834f9d1a8..532fb64175ea 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/README.md @@ -24,11 +24,11 @@ Dashboard widgets are ES Modules loaded asynchronously via lazy-load. This means import { WidgetRoot, MyWidget } from '@jetpack-premium-analytics/widgets-toolkit'; export default function MyWidgetRender( { attributes } ) { - return ( - - - - ); + return ( + + + + ); } ``` @@ -39,12 +39,12 @@ export default function MyWidgetRender( { attributes } ) { import { useWidgetRootContext } from '../../components/widget-root'; export function MyWidget() { - const { reportParams } = useWidgetRootContext(); + const { reportParams } = useWidgetRootContext(); - // Use reportParams for data fetching - const { data } = useReportOrders( reportParams ); + // Use reportParams for data fetching + const { data } = useReportOrders( reportParams ); - return
{ /* render widget */ }
; + return
{ /* render widget */ }
; } ``` @@ -52,11 +52,11 @@ export function MyWidget() { ### WidgetRoot Props -| Prop | Type | Description | -|------|------|-------------| -| `attributes` | `Partial` | Widget attributes, may include `reportParams` | -| `children` | `ReactNode` | Child components (widgets) | -| `options.from` | `string` | Router path for URL params (default: `/wc-analytics/dashboard`) | +| Prop | Type | Description | +| -------------- | -------------------------------------- | --------------------------------------------------------------- | +| `attributes` | `Partial` | Widget attributes, may include `reportParams` | +| `children` | `ReactNode` | Child components (widgets) | +| `options.from` | `string` | Router path for URL params (default: `/wc-analytics/dashboard`) | ### useWidgetRootContext @@ -64,7 +64,7 @@ Returns the resolved context value: ```typescript type WidgetRootContextValue = { - reportParams: ReportParams; + reportParams: ReportParams; }; ``` @@ -78,6 +78,7 @@ type WidgetRootContextValue = { 2. **From URL** - Falls back to URL search params via `@wordpress/route` This allows widgets to work both: + - In the Analytics dashboard (params from URL) - Other contexts (params from attributes) @@ -103,15 +104,15 @@ Dashboard widgets live in a resizable grid. Users can change tile sizes, so widg Aligned with [Tailwind container query defaults](https://tailwindcss.com/docs/responsive-design#container-size-reference) and [ARC-464](https://linear.app/a8c/issue/ARC-464). -| Token | Size | Use Case | -|-------|------|----------| +| Token | Size | Use Case | +| ----- | ------------- | ----------------------- | | `xxs` | 256px (16rem) | Extra extra small tiles | -| `xs` | 320px (20rem) | Extra small tiles | -| `sm` | 384px (24rem) | Small tiles | -| `md` | 448px (28rem) | Standard tile size | -| `lg` | 512px (32rem) | Large tiles | -| `xl` | 576px (36rem) | Extra large tiles | -| `2xl` | 672px (42rem) | Full-width widgets | +| `xs` | 320px (20rem) | Extra small tiles | +| `sm` | 384px (24rem) | Small tiles | +| `md` | 448px (28rem) | Standard tile size | +| `lg` | 512px (32rem) | Large tiles | +| `xl` | 576px (36rem) | Extra large tiles | +| `2xl` | 672px (42rem) | Full-width widgets | ### Usage in Widget SCSS @@ -119,18 +120,18 @@ Aligned with [Tailwind container query defaults](https://tailwindcss.com/docs/re @use '../../styles/widget-container' as *; .myWidget { - // Mobile-first: vertical layout for small containers - flex-direction: column; - - // >= 448px: switch to horizontal layout - @include widget-query( md ) { - flex-direction: row; - } - - // >= 576px: add more spacing - @include widget-query( xl ) { - gap: var( --wpds-dimension-base ); - } + // Mobile-first: vertical layout for small containers + flex-direction: column; + + // >= 448px: switch to horizontal layout + @include widget-query( md ) { + flex-direction: row; + } + + // >= 576px: add more spacing + @include widget-query( xl ) { + gap: var( --wpds-dimension-base ); + } } ``` diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx index e0aa8a914210..ebe151f8d80c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx @@ -30,9 +30,7 @@ export type WidgetRootContextValue = { setError?: ( error: WidgetErrorConfig | true | null ) => void; }; -const WidgetRootContext = createContext< WidgetRootContextValue | null >( - null -); +const WidgetRootContext = createContext< WidgetRootContextValue | null >( null ); /** * Hook to access the WidgetRoot context. @@ -54,9 +52,7 @@ export function useWidgetRootContext(): WidgetRootContextValue { const context = useContext( WidgetRootContext ); if ( ! context ) { - throw new Error( - 'useWidgetRootContext must be used within a WidgetRoot component' - ); + throw new Error( 'useWidgetRootContext must be used within a WidgetRoot component' ); } return context; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx index cb80b18b6047..b51eaf7902e5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx @@ -75,8 +75,7 @@ function useResolveReportParams( * Otherwise, use URL search params as reportParams. */ const hasReportParams = - !! attributes?.reportParams && - Object.keys( attributes.reportParams ).length > 0; + !! attributes?.reportParams && Object.keys( attributes.reportParams ).length > 0; return hasReportParams ? attributes.reportParams : search; } @@ -105,12 +104,7 @@ function useResolveReportParams( * } * ``` */ -export function WidgetRoot( { - attributes, - children, - setError, - options, -}: WidgetRootProps ) { +export function WidgetRoot( { attributes, children, setError, options }: WidgetRootProps ) { const chartTheme = useChartTheme(); const rawReportParams = useResolveReportParams( attributes, options?.from ); @@ -122,10 +116,7 @@ export function WidgetRoot( { [ rawReportParams, defaultPreset ] ); - const contextValue = useMemo( - () => ( { reportParams, setError } ), - [ reportParams, setError ] - ); + const contextValue = useMemo( () => ( { reportParams, setError } ), [ reportParams, setError ] ); return ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts index 4d72d523f4e6..6c3a9e8f8238 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/constants/color-palette.ts @@ -12,10 +12,4 @@ const COLOR_PRIMARY = COLOR_BLUEBERRY; const COLOR_SECONDARY = COLOR_BLUE_30; // Theme -export const WOO_COLORS = [ - COLOR_PRIMARY, - COLOR_SECONDARY, - COLOR_PURPLE_30, - '#7B90FF', - '#EB6594', -]; +export const WOO_COLORS = [ COLOR_PRIMARY, COLOR_SECONDARY, COLOR_PURPLE_30, '#7B90FF', '#EB6594' ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx index a00bdc3aa490..adb3cbb860cf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx @@ -11,10 +11,7 @@ import { } from '@jetpack-premium-analytics/data'; import { getStoreInfo } from '../../helpers/store-info'; import { endOfDay } from 'date-fns'; -import { - deriveComparisonRange, - encodeDateToSearchParam, -} from '@jetpack-premium-analytics/routing'; +import { deriveComparisonRange, encodeDateToSearchParam } from '@jetpack-premium-analytics/routing'; import { useCallback, useMemo, useState, useEffect } from 'react'; import type { DataFormControlProps } from '@wordpress/dataviews'; import { @@ -26,9 +23,7 @@ import { /** * Inferred types */ -type ReportParams = NonNullable< - Parameters< typeof normalizeReportParams >[ 0 ] ->; +type ReportParams = NonNullable< Parameters< typeof normalizeReportParams >[ 0 ] >; export type ReportParamsFieldAttributes = { reportParams: ReportParams; @@ -38,16 +33,14 @@ export function ReportParamsField( { data: attributes, onChange, }: DataFormControlProps< ReportParamsFieldAttributes > ) { - const [ stagedReportParams, setStagedReportParams ] = - useState< ReportParams >( attributes?.reportParams ); + const [ stagedReportParams, setStagedReportParams ] = useState< ReportParams >( + attributes?.reportParams + ); const { launchedDate } = getStoreInfo(); const defaultPreset = getDefaultPreset( launchedDate ); - const reportParams = normalizeReportParams( - stagedReportParams, - defaultPreset - ); + const reportParams = normalizeReportParams( stagedReportParams, defaultPreset ); const range = { from: localTZDate( reportParams.from ), @@ -59,12 +52,8 @@ export function ReportParamsField( { const nextReportParams = { ...stagedReportParams }; if ( nextRange?.from && nextRange?.to ) { - nextReportParams.from = encodeDateToSearchParam( - nextRange.from - ); - nextReportParams.to = encodeDateToSearchParam( - endOfDay( nextRange.to ) - ); + nextReportParams.from = encodeDateToSearchParam( nextRange.from ); + nextReportParams.to = encodeDateToSearchParam( endOfDay( nextRange.to ) ); } if ( nextPresetId && isPrimaryPreset( nextPresetId ) ) { @@ -107,19 +96,12 @@ export function ReportParamsField( { ] ); const commitComparisonRange = useCallback( - ( - nextComparisonRange?: DateRange, - nextComparisonPresetId?: ComparisonPresetId - ) => { + ( nextComparisonRange?: DateRange, nextComparisonPresetId?: ComparisonPresetId ) => { onChange( { reportParams: { ...reportParams, - compare_from: encodeDateToSearchParam( - nextComparisonRange?.from - ), - compare_to: encodeDateToSearchParam( - nextComparisonRange?.to - ), + compare_from: encodeDateToSearchParam( nextComparisonRange?.from ), + compare_to: encodeDateToSearchParam( nextComparisonRange?.to ), compare_preset: nextComparisonPresetId, comp: '1' as const, }, @@ -141,13 +123,10 @@ export function ReportParamsField( { * This is a temporary workaround until @automattic/dashboard exposes * a Context provider. See WOOA7S-1008 for the upstream solution. */ - const [ containerElement, setContainerElement ] = - useState< HTMLElement | null >( null ); + const [ containerElement, setContainerElement ] = useState< HTMLElement | null >( null ); useEffect( () => { - const node = document.querySelector< HTMLElement >( - '.next-admin-layout__surface' - ); + const node = document.querySelector< HTMLElement >( '.next-admin-layout__surface' ); setContainerElement( node ); }, [] ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts index 18be99c32fc4..8cc8a10736f0 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/index.ts @@ -1,4 +1 @@ -export { - ReportParamsField, - type ReportParamsFieldAttributes, -} from './date-report-params-field'; +export { ReportParamsField, type ReportParamsFieldAttributes } from './date-report-params-field'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts index 46abc93ff479..c048f329edde 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/index.ts @@ -1,8 +1,5 @@ /** * Widget edit fields */ -export { - ReportParamsField, - type ReportParamsFieldAttributes, -} from './date-report-params-field'; +export { ReportParamsField, type ReportParamsFieldAttributes } from './date-report-params-field'; export { MetricsField, DEFAULT_METRICS } from './metrics-field'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx index f2df81d90b28..52c683e111b5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx @@ -32,7 +32,7 @@ export function MetricsField( { const updateMetrics = useCallback( ( id: string ) => onChange( { - metrics: attributes.metrics.map( ( m ) => { + metrics: attributes.metrics.map( m => { return m.id === id ? { ...m, enabled: ! m.enabled } : m; } ), } ), @@ -52,12 +52,10 @@ export function MetricsField( { return ( - - { __( 'Metrics', 'jetpack-premium-analytics' ) } - + { __( 'Metrics', 'jetpack-premium-analytics' ) } { help } - { attributes?.metrics?.map( ( metric ) => ( + { attributes?.metrics?.map( metric => ( = {} ) { return { - data: items.map( ( item ) => ( { + data: items.map( item => ( { ...item, coupon_id: 1, orders_count: 1, @@ -43,11 +43,7 @@ function makeCouponsData( describe( 'buildSalesByCouponData', () => { it( 'returns empty chartData when coupons is undefined', () => { - const result = buildSalesByCouponData( - undefined, - undefined, - defaultReportParams - ); + const result = buildSalesByCouponData( undefined, undefined, defaultReportParams ); expect( result.chartData ).toEqual( [] ); } ); @@ -79,11 +75,7 @@ describe( 'buildSalesByCouponData', () => { { total_sales: 300 } ); - const result = buildSalesByCouponData( - coupons as any, - undefined, - defaultReportParams - ); + const result = buildSalesByCouponData( coupons as any, undefined, defaultReportParams ); expect( result.chartData ).toHaveLength( 1 ); expect( result.chartData[ 0 ].data ).toEqual( [ @@ -104,12 +96,7 @@ describe( 'buildSalesByCouponData', () => { { total_sales: 725 } ); - const result = buildSalesByCouponData( - coupons as any, - undefined, - defaultReportParams, - 3 - ); + const result = buildSalesByCouponData( coupons as any, undefined, defaultReportParams, 3 ); const currentPeriod = result.chartData[ 0 ].data; expect( currentPeriod ).toHaveLength( 4 ); // 3 top + Other @@ -155,11 +142,7 @@ describe( 'buildSalesByCouponData', () => { { total_sales: 500 } ); - const result = buildSalesByCouponData( - coupons as any, - undefined, - defaultReportParams - ); + const result = buildSalesByCouponData( coupons as any, undefined, defaultReportParams ); expect( result.chartData[ 0 ].data[ 0 ].value ).toBe( 500 ); } ); @@ -174,12 +157,7 @@ describe( 'buildSalesByCouponData', () => { { total_sales: 600 } ); - const result = buildSalesByCouponData( - coupons as any, - undefined, - defaultReportParams, - 2 - ); + const result = buildSalesByCouponData( coupons as any, undefined, defaultReportParams, 2 ); const currentPeriod = result.chartData[ 0 ].data; expect( currentPeriod ).toHaveLength( 3 ); // 2 top + Other diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts index d1ad76d7db38..0a0e31d15f04 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts @@ -71,11 +71,9 @@ export function buildBookingsByAttendanceData( ]; // Calculate values for each status - const statusValues = statusMap.map( ( status ) => { + const statusValues = statusMap.map( status => { const value = summary[ status.key ] || 0; - const comparisonValue = comparisonSummary - ? comparisonSummary[ status.key ] || 0 - : 0; + const comparisonValue = comparisonSummary ? comparisonSummary[ status.key ] || 0 : 0; return { ...status, @@ -85,10 +83,7 @@ export function buildBookingsByAttendanceData( } ); // Calculate total bookings across all statuses - const totalBookings = statusValues.reduce( - ( sum, status ) => sum + status.value, - 0 - ); + const totalBookings = statusValues.reduce( ( sum, status ) => sum + status.value, 0 ); const comparisonTotalBookings = statusValues.reduce( ( sum, status ) => sum + status.comparisonValue, @@ -106,12 +101,10 @@ export function buildBookingsByAttendanceData( } // Filter out statuses with zero bookings - const statusesWithData = statusValues.filter( - ( status ) => status.value > 0 - ); + const statusesWithData = statusValues.filter( status => status.value > 0 ); // Build chart data - const chartData: DonutChartData = statusesWithData.map( ( status ) => ( { + const chartData: DonutChartData = statusesWithData.map( status => ( { label: status.label, value: status.value, valueDisplay: formatMetricValue( status.value, 'number', { @@ -122,7 +115,7 @@ export function buildBookingsByAttendanceData( } ) ); // Build legend data - const legendData: LegendItem[] = statusesWithData.map( ( status ) => ( { + const legendData: LegendItem[] = statusesWithData.map( status => ( { label: status.label, value: status.value, displayValue: formatMetricValue( status.value, 'number', { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts index ceaa6e53f6a9..d34d8865a14d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts @@ -49,10 +49,8 @@ export function buildCouponUseData( // Pick comparison totals const comparisonTotalSales = comparisonCoupons?.summary.total_sales || 0; - const comparisonSalesWithCoupon = - comparisonCoupons?.summary.sales_with_coupon || 0; - const comparisonSalesWithoutCoupon = - comparisonCoupons?.summary.sales_without_coupon || 0; + const comparisonSalesWithCoupon = comparisonCoupons?.summary.sales_with_coupon || 0; + const comparisonSalesWithoutCoupon = comparisonCoupons?.summary.sales_without_coupon || 0; // If there are no sales, return empty state if ( totalSales === 0 ) { @@ -102,9 +100,7 @@ export function buildCouponUseData( useMultipliers: true, decimals: 0, } ), - comparison: hasComparison - ? comparisonSalesWithoutCoupon - : undefined, + comparison: hasComparison ? comparisonSalesWithoutCoupon : undefined, }, ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts index a04fa24d5927..c14a9b19e5e1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts @@ -45,12 +45,9 @@ export function buildNewVsReturningCustomerData( const returningCustomers = customers.summary.returning_customers; // Pick comparison totals - const comparisonTotalCustomers = - comparisonCustomers?.summary?.total_customers || 0; - const comparisonNewCustomers = - comparisonCustomers?.summary?.new_customers || 0; - const comparisonReturningCustomers = - comparisonCustomers?.summary?.returning_customers || 0; + const comparisonTotalCustomers = comparisonCustomers?.summary?.total_customers || 0; + const comparisonNewCustomers = comparisonCustomers?.summary?.new_customers || 0; + const comparisonReturningCustomers = comparisonCustomers?.summary?.returning_customers || 0; // If there are no customers, return empty state if ( totalCustomers === 0 ) { @@ -92,9 +89,7 @@ export function buildNewVsReturningCustomerData( useMultipliers: true, decimals: 0, } ), - comparison: hasComparison - ? comparisonReturningCustomers - : undefined, + comparison: hasComparison ? comparisonReturningCustomers : undefined, }, { label: __( 'New', 'jetpack-premium-analytics' ), diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts index 0e182e0c01fe..954a0900233d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts @@ -39,12 +39,9 @@ export function buildOrdersFulfillmentData( const unfulfilledCount = unfulfilledOrders?.summary?.orders_no ?? 0; const totalOrders = fulfilledCount + unfulfilledCount; - const comparisonFulfilledCount = - comparisonFulfilledOrders?.summary?.orders_no ?? 0; - const comparisonUnfulfilledCount = - comparisonUnfulfilledOrders?.summary?.orders_no ?? 0; - const comparisonTotalOrders = - comparisonFulfilledCount + comparisonUnfulfilledCount; + const comparisonFulfilledCount = comparisonFulfilledOrders?.summary?.orders_no ?? 0; + const comparisonUnfulfilledCount = comparisonUnfulfilledOrders?.summary?.orders_no ?? 0; + const comparisonTotalOrders = comparisonFulfilledCount + comparisonUnfulfilledCount; if ( totalOrders === 0 ) { return { @@ -79,17 +76,13 @@ export function buildOrdersFulfillmentData( label: __( 'Fulfilled', 'jetpack-premium-analytics' ), value: fulfilledCount, displayValue: formatCount( fulfilledCount ), - comparison: comparisonFulfilledOrders - ? comparisonFulfilledCount - : undefined, + comparison: comparisonFulfilledOrders ? comparisonFulfilledCount : undefined, }, { label: __( 'Unfulfilled', 'jetpack-premium-analytics' ), value: unfulfilledCount, displayValue: formatCount( unfulfilledCount ), - comparison: comparisonUnfulfilledOrders - ? comparisonUnfulfilledCount - : undefined, + comparison: comparisonUnfulfilledOrders ? comparisonUnfulfilledCount : undefined, }, ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts index cc77eedad888..d0d9feac37f3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts @@ -43,12 +43,9 @@ export function buildPaymentStatusData( const totalSales = paidNetSales + unpaidNetSales; // Calculate comparison totals - const comparisonPaidNetSales = - comparisonOrders?.summary?.paid_net_sales || 0; - const comparisonUnpaidNetSales = - comparisonOrders?.summary?.unpaid_net_sales || 0; - const comparisonTotalSales = - comparisonPaidNetSales + comparisonUnpaidNetSales; + const comparisonPaidNetSales = comparisonOrders?.summary?.paid_net_sales || 0; + const comparisonUnpaidNetSales = comparisonOrders?.summary?.unpaid_net_sales || 0; + const comparisonTotalSales = comparisonPaidNetSales + comparisonUnpaidNetSales; // If there are no sales, return empty state if ( totalSales === 0 ) { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts index 60d97312afb1..b37202af69ee 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts @@ -34,8 +34,7 @@ export function buildRevenueByCustomerTypeData( }; } - const { primary: primaryLabel, comparison: comparisonLabel } = - formatLegendLabels( reportParams ); + const { primary: primaryLabel, comparison: comparisonLabel } = formatLegendLabels( reportParams ); const { summary } = customers; const newCustomerSales = summary.new_customer_sales; @@ -60,8 +59,7 @@ export function buildRevenueByCustomerTypeData( // Add comparison period if available if ( comparisonCustomers?.summary ) { - const comparisonNewCustomerSales = - comparisonCustomers.summary.new_customer_sales || 0; + const comparisonNewCustomerSales = comparisonCustomers.summary.new_customer_sales || 0; const comparisonReturningCustomerSales = comparisonCustomers.summary.returning_customer_sales || 0; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts index 29e0424af531..169ca630f6f1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts @@ -36,8 +36,7 @@ export function buildSalesByCouponData( }; } - const { primary: primaryLabel, comparison: comparisonLabel } = - formatLegendLabels( reportParams ); + const { primary: primaryLabel, comparison: comparisonLabel } = formatLegendLabels( reportParams ); const { data: items } = coupons; @@ -47,13 +46,13 @@ export function buildSalesByCouponData( // Create a map of comparison data by coupon code const comparisonMap = new Map< string, number >(); if ( comparisonCoupons ) { - comparisonCoupons.data.forEach( ( item ) => { + comparisonCoupons.data.forEach( item => { comparisonMap.set( item.coupon_code, item.total_sales ); } ); } // Build current period data points - const currentPeriodData = topCoupons.map( ( item ) => ( { + const currentPeriodData = topCoupons.map( item => ( { label: item.coupon_code, value: item.total_sales, } ) ); @@ -80,7 +79,7 @@ export function buildSalesByCouponData( // Add comparison period if available if ( comparisonCoupons?.summary ) { - const comparisonPeriodData = topCoupons.map( ( item ) => ( { + const comparisonPeriodData = topCoupons.map( item => ( { label: item.coupon_code, value: comparisonMap.get( item.coupon_code ) || 0, } ) ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts index 94b0cbcc3f8a..551b3609d89a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts @@ -34,8 +34,7 @@ export function buildSalesByDeviceData( }; } - const { primary: primaryLabel, comparison: comparisonLabel } = - formatLegendLabels( reportParams ); + const { primary: primaryLabel, comparison: comparisonLabel } = formatLegendLabels( reportParams ); const { data } = orderAttribution; @@ -43,7 +42,7 @@ export function buildSalesByDeviceData( const chartData: SeriesData[] = [ { label: primaryLabel, - data: data.map( ( item ) => ( { + data: data.map( item => ( { label: item.item || __( 'Unassigned', 'jetpack-premium-analytics' ), value: item.current_period?.value ?? 0, } ) ), @@ -54,7 +53,7 @@ export function buildSalesByDeviceData( if ( hasComparison ) { chartData.push( { label: comparisonLabel, - data: data.map( ( item ) => ( { + data: data.map( item => ( { label: item.item || __( 'Unassigned', 'jetpack-premium-analytics' ), value: item.previous_period?.value ?? 0, } ) ), diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts index 1e2a68df751e..5aa5a44247c2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts @@ -31,11 +31,8 @@ export function buildSalesByUtmData( // Find the max value for share calculation const maxValue = Math.max( - ...data.map( ( item ) => - Math.max( - item.current_period.value || 0, - item.previous_period?.value || 0 - ) + ...data.map( item => + Math.max( item.current_period.value || 0, item.previous_period?.value || 0 ) ), 1 // Prevent division by zero ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts index f58f8cd08480..2f579588c5e1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts @@ -59,8 +59,7 @@ export function buildSessionsByDeviceData( const { data, summary } = sessionsByDevice; const total = summary.total_sessions; - const comparisonTotal = - comparisonSessionsByDevice?.summary?.total_sessions || 0; + const comparisonTotal = comparisonSessionsByDevice?.summary?.total_sessions || 0; // If there are no sessions, return empty state if ( total === 0 ) { @@ -75,16 +74,13 @@ export function buildSessionsByDeviceData( // Create a map of comparison data by device type const comparisonMap = new Map< string, number >(); if ( comparisonSessionsByDevice?.data ) { - comparisonSessionsByDevice.data.forEach( ( item ) => { - comparisonMap.set( - item.device_type.toLowerCase(), - item.active_sessions - ); + comparisonSessionsByDevice.data.forEach( item => { + comparisonMap.set( item.device_type.toLowerCase(), item.active_sessions ); } ); } // Build chart data - const chartData: SemiCircleChartData = data.map( ( item ) => ( { + const chartData: SemiCircleChartData = data.map( item => ( { label: getDeviceLabel( item.device_type ), value: item.active_sessions, valueDisplay: formatMetricValue( item.active_sessions, 'number', { @@ -94,7 +90,7 @@ export function buildSessionsByDeviceData( } ) ); // Build legend data - const legendData: LegendItem[] = data.map( ( item ) => { + const legendData: LegendItem[] = data.map( item => { const normalizedType = item.device_type.toLowerCase(); const comparisonValue = comparisonSessionsByDevice ? comparisonMap.get( normalizedType ) || 0 diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts index 93465e21282e..57dfa4edf666 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts @@ -42,7 +42,7 @@ function mapTimeSeriesToLineChartData< T extends TimeSeriesData >( return []; } - return data.map( ( item ) => ( { + return data.map( item => ( { date: localTZDate( item.date_start ), value: Number( item[ metricKey ] ), } ) ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts index ebb60dc57ba8..cc02fd4fb339 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts @@ -47,8 +47,7 @@ export function buildTotalReturnsData( }; } - const { primary: primaryLabel, comparison: comparisonLabel } = - formatLegendLabels( reportParams ); + const { primary: primaryLabel, comparison: comparisonLabel } = formatLegendLabels( reportParams ); const totalSales = orders.summary.total_sales ?? 0; // Net sales (total sales minus refunds) @@ -72,10 +71,7 @@ export function buildTotalReturnsData( if ( comparisonOrders?.summary ) { const comparisonTotalRefunds = comparisonOrders.summary.refunds || 0; const comparisonTotalSales = comparisonOrders.summary.total_sales || 0; - const comparisonSalesAmount = Math.max( - 0, - comparisonTotalSales - comparisonTotalRefunds - ); + const comparisonSalesAmount = Math.max( 0, comparisonTotalSales - comparisonTotalRefunds ); chartData.push( { label: comparisonLabel, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts index ea18235302a5..8ac0ce709502 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts @@ -53,51 +53,33 @@ export function buildVisitorsByLocationData( { // Build geo chart data const geoData: GeoData = [ [ headerLabel, 'Visitors' ], - ...primaryData.map( - ( item ) => [ item.label, item.value ] as [ string, number ] - ), + ...primaryData.map( item => [ item.label, item.value ] as [ string, number ] ), ]; // Find max values for bar width scaling (largest value = 100% width) - const maxPrimaryValue = Math.max( - ...primaryData.map( ( d ) => d.value ), - 0 - ); + const maxPrimaryValue = Math.max( ...primaryData.map( d => d.value ), 0 ); const maxComparisonValue = comparisonData - ? Math.max( ...comparisonData.map( ( d ) => d.value ), 0 ) + ? Math.max( ...comparisonData.map( d => d.value ), 0 ) : 0; // Build leaderboard data (top N items) - const leaderboardData: LeaderboardChartData = primaryData - .slice( 0, limit ) - .map( ( item ) => { - const comparisonItem = comparisonData?.find( - ( c ) => c.id === item.id - ); - const previousValue = comparisonItem?.value ?? 0; - const currentShare = - maxPrimaryValue > 0 - ? ( item.value / maxPrimaryValue ) * 100 - : 0; - const previousShare = - maxComparisonValue > 0 - ? ( previousValue / maxComparisonValue ) * 100 - : 0; - const delta = - previousValue > 0 - ? ( ( item.value - previousValue ) / previousValue ) * 100 - : 0; + const leaderboardData: LeaderboardChartData = primaryData.slice( 0, limit ).map( item => { + const comparisonItem = comparisonData?.find( c => c.id === item.id ); + const previousValue = comparisonItem?.value ?? 0; + const currentShare = maxPrimaryValue > 0 ? ( item.value / maxPrimaryValue ) * 100 : 0; + const previousShare = maxComparisonValue > 0 ? ( previousValue / maxComparisonValue ) * 100 : 0; + const delta = previousValue > 0 ? ( ( item.value - previousValue ) / previousValue ) * 100 : 0; - return { - id: item.id, - label: item.label, - currentValue: item.value, - previousValue, - currentShare, - previousShare, - delta, - }; - } ); + return { + id: item.id, + label: item.label, + currentValue: item.value, + previousValue, + currentShare, + previousShare, + delta, + }; + } ); return { geoData, leaderboardData }; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts index 8669e6bac847..899000fcdd17 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/calculate-delta.ts @@ -17,10 +17,7 @@ * calculateDelta(0, 0) // Returns 0 (no change) * calculateDelta(0, 100) // Returns -100 (complete disappearance) */ -export function calculateDelta( - currentValue: number, - previousValue: number -): number { +export function calculateDelta( currentValue: number, previousValue: number ): number { // Handle the case where previous value is zero if ( previousValue === 0 ) { // If previous was 0 and current is positive, show 100% increase diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts index b5bf5c8ed333..1094b9b81a3d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/chart-empty-state.ts @@ -23,9 +23,7 @@ type SeriesWithData = { * @return True if all values across all series are 0 or null */ export function isEmptyChartData( series: SeriesWithData[] ): boolean { - return series.every( ( s ) => - s.data.every( ( point ) => point.value === 0 || point.value === null ) - ); + return series.every( s => s.data.every( point => point.value === 0 || point.value === null ) ); } /** @@ -35,13 +33,11 @@ export function isEmptyChartData( series: SeriesWithData[] ): boolean { * @param data - Array of DataPointPercentage to check * @return True if data is empty or all values are 0 */ -export function isEmptyPieChartData( - data: DataPointPercentage[] | undefined | null -): boolean { +export function isEmptyPieChartData( data: DataPointPercentage[] | undefined | null ): boolean { if ( ! data || data.length === 0 ) { return true; } - return data.every( ( item ) => item.value === 0 ); + return data.every( item => item.value === 0 ); } /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts index dc378d873219..2d4685e5a00f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts @@ -8,9 +8,7 @@ import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; */ import type { MetricKey } from '../types'; -type FormatMetricOptions = NonNullable< - Parameters< typeof formatMetricValue >[ 2 ] ->; +type FormatMetricOptions = NonNullable< Parameters< typeof formatMetricValue >[ 2 ] >; type MetricType = NonNullable< Parameters< typeof formatMetricValue >[ 1 ] >; @@ -93,16 +91,9 @@ const metricFormatMap: Record< }, }; -export function formatOrderMetric( - metricKey: MetricKey, - options?: FormatMetricOptions -) { +export function formatOrderMetric( metricKey: MetricKey, options?: FormatMetricOptions ) { return ( value: number ) => - formatMetricValue( - value, - metricFormatMap[ metricKey ].metricType, - options ?? {} - ); + formatMetricValue( value, metricFormatMap[ metricKey ].metricType, options ?? {} ); } export function getFormatByMetricKey( metricKey: MetricKey ) { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts index 38d8beb18478..547a4b0dfd99 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/index.ts @@ -1,23 +1,8 @@ -export { - formatOrderMetric, - getFormatByMetricKey, -} from './format-orders-metrics'; -export { - buildTimeSeriesChartData, - type TimeSeriesData, -} from './build-time-series-chart-data'; -export { - buildSalesByCouponData, - type SalesByCouponData, -} from './build-sales-by-coupon-data'; -export { - PHYSICAL_PRODUCTS_FILTER, - BOOKINGS_FILTER, -} from './product-type-filters'; -export { - FULFILLED_ORDERS_FILTER, - UNFULFILLED_ORDERS_FILTER, -} from './fulfillment-filters'; +export { formatOrderMetric, getFormatByMetricKey } from './format-orders-metrics'; +export { buildTimeSeriesChartData, type TimeSeriesData } from './build-time-series-chart-data'; +export { buildSalesByCouponData, type SalesByCouponData } from './build-sales-by-coupon-data'; +export { PHYSICAL_PRODUCTS_FILTER, BOOKINGS_FILTER } from './product-type-filters'; +export { FULFILLED_ORDERS_FILTER, UNFULFILLED_ORDERS_FILTER } from './fulfillment-filters'; export { PAYMENT_STATUS_FILTERS } from './payment-status-filters'; export { buildRevenueByCustomerTypeData, @@ -33,10 +18,7 @@ export { type SegmentStyle, type ColorableItem, } from './segment-styles'; -export { - buildSalesByDeviceData, - type SalesByDeviceData, -} from './build-sales-by-device-data'; +export { buildSalesByDeviceData, type SalesByDeviceData } from './build-sales-by-device-data'; export { buildSessionsByDeviceData, type SessionsByDeviceData, @@ -45,21 +27,12 @@ export { buildBookingsByAttendanceData, type BookingsByAttendanceData, } from './build-bookings-by-attendance-data'; -export { - buildTotalReturnsData, - type TotalReturnsData, -} from './build-total-returns-data'; +export { buildTotalReturnsData, type TotalReturnsData } from './build-total-returns-data'; export { buildSalesByUtmData } from './build-sales-by-utm-data'; export { formatLegendLabels } from './format-legend-labels'; export { calculateDelta } from './calculate-delta'; -export { - buildCouponUseData, - type CouponUseData, -} from './build-coupon-use-data'; -export { - buildPaymentStatusData, - type PaymentStatusData, -} from './build-payment-status-data'; +export { buildCouponUseData, type CouponUseData } from './build-coupon-use-data'; +export { buildPaymentStatusData, type PaymentStatusData } from './build-payment-status-data'; export { buildOrdersFulfillmentData, type OrdersFulfillmentData, @@ -71,8 +44,4 @@ export { type Region, } from './build-visitors-by-location-data'; export { flagUrl } from './flag-url'; -export { - isEmptyChartData, - isEmptyPieChartData, - getEmptyChartDomain, -} from './chart-empty-state'; +export { isEmptyChartData, isEmptyPieChartData, getEmptyChartDomain } from './chart-empty-state'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts index 4f1d843b9307..6fdd6c085cd4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/payment-status-filters.ts @@ -19,13 +19,7 @@ import type { FilterCondition } from '@jetpack-premium-analytics/data'; export const PAYMENT_STATUS_FILTERS: FilterCondition[] = [ { key: 'status', - value: [ - 'wc-pending', - 'wc-processing', - 'wc-on-hold', - 'wc-completed', - 'wc-refunded', - ], + value: [ 'wc-pending', 'wc-processing', 'wc-on-hold', 'wc-completed', 'wc-refunded' ], compare: 'IN', }, ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts index cd610ffa2c51..80dcb6d9eba3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/segment-styles.ts @@ -32,7 +32,7 @@ export function resolveSegmentStyles( return stylesProp; } - return chartData.map( ( segment ) => ( { + return chartData.map( segment => ( { color: segment.color ?? '', } ) ); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts index d12cbc20f53a..6152bcfd39bf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts @@ -58,10 +58,7 @@ export function useAttributesWithSearchFallback( * Otherwise, build attributes from URL search params. */ const hasReportParams = - !! attributes?.reportParams && - Object.keys( attributes.reportParams ).length > 0; + !! attributes?.reportParams && Object.keys( attributes.reportParams ).length > 0; - return hasReportParams - ? ( attributes as ReportParamsFieldAttributes ) - : { reportParams: search }; + return hasReportParams ? ( attributes as ReportParamsFieldAttributes ) : { reportParams: search }; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts index 698db18620b0..e01f342c65a3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts @@ -84,8 +84,7 @@ export function useChartTheme(): WooChartTheme { }, conversionFunnelChart: { backgroundColor: 'var(--wpds-color-bg-surface-brand)', - positiveChangeColor: - 'var(--wpds-color-fg-content-success-weak)', + positiveChangeColor: 'var(--wpds-color-fg-content-success-weak)', negativeChangeColor: 'var(--wpds-color-fg-content-error-weak)', }, lineChart: { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts index f31d698130d2..427efb2b2b7b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts @@ -25,9 +25,7 @@ import type { * return ; * ``` */ -export function useSeriesStyles( - series: ComparativeLineChartSeries[] -): SeriesStyle[] { +export function useSeriesStyles( series: ComparativeLineChartSeries[] ): SeriesStyle[] { const { getElementStyles } = useGlobalChartsContext(); return useMemo( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts index 211501dbc999..8819f19f605b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts @@ -56,9 +56,7 @@ export function useWidgetError( if ( ! setError ) { // Fallback: Log when setError is unavailable (widget outside dashboard context) // eslint-disable-next-line no-console - console.warn( - '[useWidgetError] setError is undefined - error UI cannot be displayed' - ); + console.warn( '[useWidgetError] setError is undefined - error UI cannot be displayed' ); return; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts index 23bc55e34138..9b561dd55f5b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts @@ -98,9 +98,4 @@ export { /** * Types */ -export type { - OrderMetricKey, - OrderMetrics, - OrdersSummary, - DataFormat, -} from './types'; +export type { OrderMetricKey, OrderMetrics, OrdersSummary, DataFormat } from './types'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss index 22cd83d78d04..ed57843940c7 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss @@ -24,13 +24,20 @@ // Container Query Breakpoints (Tailwind-aligned) // These are for element-based queries, not viewport $widget-breakpoints: ( - xxs: 16rem, // 256px - Extra extra small widgets - xs: 20rem, // 320px - Extra small widgets - sm: 24rem, // 384px - Small widgets - md: 28rem, // 448px - Medium widgets (common tile size) - lg: 32rem, // 512px - Large widgets - xl: 36rem, // 576px - Extra large widgets - 2xl: 42rem, // 672px - Full-width widgets + xxs: 16rem, + // 256px - Extra extra small widgets + xs: 20rem, + // 320px - Extra small widgets + sm: 24rem, + // 384px - Small widgets + md: 28rem, + // 448px - Medium widgets (common tile size) + lg: 32rem, + // 512px - Large widgets + xl: 36rem, + // 576px - Extra large widgets + 2xl: 42rem, + // 672px - Full-width widgets ) !default; /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md index 8053de2e3983..794b5705764b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/README.md @@ -4,20 +4,20 @@ Dashboard widget components for WooCommerce Analytics. ## Available Widgets -| Widget | Chart Component | Description | -| -------------------------------- | ----------------------------------------------- | ------------------------------------------------- | -| `ConversionRateWidget` | `MetricWithComparison` | Funnel conversion rate metric | -| `MetricComparisonWidget` | `MetricWithComparison` + `ComparativeLineChart` | Generic metric with time series | -| `RevenueByCustomerTypeWidget` | `BarChart` | Revenue breakdown by customer type | -| `NewVsReturningCustomerWidget` | `DonutChart` | Customer counts by new vs returning | -| `OrderMetricWidget` | `ReportMetricWidget` | Order-based metrics (revenue, orders, AOV) | -| `SalesByCouponWidget` | `SemiCircleChart` | Coupon sales for all product types | -| `SalesByDeviceWidget` | `DonutChart` | Sales breakdown by device type | -| `SalesByUtmWidget` | `LeaderboardChart` | Sales by UTM parameters (source/channel/campaign) | -| `TotalReturnsWidget` | `DonutChart` | Returns/refunds for all product types | -| `VisitorMetricWidget` | `ReportMetricWidget` | Visitor-based metrics | -| `TopPerformingProductsWidget` | `LeaderboardChart` | Top products by revenue | -| `TopPerformingBookingsWidget` | `LeaderboardChart` | Top bookings by revenue | +| Widget | Chart Component | Description | +| ------------------------------ | ----------------------------------------------- | ------------------------------------------------- | +| `ConversionRateWidget` | `MetricWithComparison` | Funnel conversion rate metric | +| `MetricComparisonWidget` | `MetricWithComparison` + `ComparativeLineChart` | Generic metric with time series | +| `RevenueByCustomerTypeWidget` | `BarChart` | Revenue breakdown by customer type | +| `NewVsReturningCustomerWidget` | `DonutChart` | Customer counts by new vs returning | +| `OrderMetricWidget` | `ReportMetricWidget` | Order-based metrics (revenue, orders, AOV) | +| `SalesByCouponWidget` | `SemiCircleChart` | Coupon sales for all product types | +| `SalesByDeviceWidget` | `DonutChart` | Sales breakdown by device type | +| `SalesByUtmWidget` | `LeaderboardChart` | Sales by UTM parameters (source/channel/campaign) | +| `TotalReturnsWidget` | `DonutChart` | Returns/refunds for all product types | +| `VisitorMetricWidget` | `ReportMetricWidget` | Visitor-based metrics | +| `TopPerformingProductsWidget` | `LeaderboardChart` | Top products by revenue | +| `TopPerformingBookingsWidget` | `LeaderboardChart` | Top bookings by revenue | ## Chart Components @@ -36,8 +36,8 @@ Shared code is located in `common/`: ### Styles -- `donut-widget.module.scss` - Container styles for DonutChart widgets +- `donut-widget.module.scss` - Container styles for DonutChart widgets ### Hooks -- `useSegmentStyles( chartData )` - Builds segment colors from theme provider +- `useSegmentStyles( chartData )` - Builds segment colors from theme provider diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx index 9b6fa62723eb..8a514fa32d30 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx @@ -70,12 +70,7 @@ export function BookingsByAttendanceWidget() { return ( <> - + { - if ( - ! conversionData || - conversionData.summary.active_sessions === 0 - ) { + if ( ! conversionData || conversionData.summary.active_sessions === 0 ) { return { steps: [], overallRate: 0, @@ -77,9 +71,7 @@ export function ConversionRateWidget( { overallRate: conversionData.overallRate || 0, // Get comparison rate as decimal comparisonRate: - hasComparison && comparisonData?.summary - ? comparisonData.summary.conversion_rate - : null, + hasComparison && comparisonData?.summary ? comparisonData.summary.conversion_rate : null, }; }, [ conversionData, comparisonData, hasComparison ] ); @@ -96,10 +88,7 @@ export function ConversionRateWidget( { if ( steps.length === 0 ) { return ( <> - + { isRefetching && } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts index 47e5a57dd17c..ba8e8f3c81f6 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/index.ts @@ -1,4 +1 @@ -export { - ConversionRateWidget, - BookingConversionRateWidget, -} from './conversion-rate-widget'; +export { ConversionRateWidget, BookingConversionRateWidget } from './conversion-rate-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx index 1e9d00f942a9..0dcba5224c29 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx @@ -51,8 +51,7 @@ export function CouponUseWidget() { const isRefetching = isFetching && hasData; const { chartData, total, comparisonTotal, legendData } = useMemo( - () => - buildCouponUseData( primary.data, comparison.data, hasComparison ), + () => buildCouponUseData( primary.data, comparison.data, hasComparison ), [ primary.data, comparison.data, hasComparison ] ); @@ -69,12 +68,7 @@ export function CouponUseWidget() { return ( <> - + - buildNewVsReturningCustomerData( - primary.data, - comparison.data, - hasComparison - ), + () => buildNewVsReturningCustomerData( primary.data, comparison.data, hasComparison ), [ primary.data, comparison.data, hasComparison ] ); @@ -73,12 +68,7 @@ export function NewVsReturningCustomerWidget() { return ( <> - + * ``` */ -export function BookingOrderMetricWidget( { - metricKey, -}: BookingOrderMetricWidgetProps ) { +export function BookingOrderMetricWidget( { metricKey }: BookingOrderMetricWidgetProps ) { const { reportParams } = useWidgetRootContext(); return ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx index 1fcf945289d0..b233deb37177 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx @@ -104,12 +104,7 @@ export function OrdersFulfillmentWidget() { return ( <> - + - + * */ -export function TopPerformingBookingsWidget( { - limit = 5, -}: TopPerformingBookingsWidgetProps ) { +export function TopPerformingBookingsWidget( { limit = 5 }: TopPerformingBookingsWidgetProps ) { return ( data?.data?.map( ( item ) => item.product_id ) || [], + () => data?.data?.map( item => item.product_id ) || [], [ data?.data ] ); // Fetch product images - const { data: productImages, isLoading: imagesLoading } = useProductImages( - { - productIds, - } - ); + const { data: productImages, isLoading: imagesLoading } = useProductImages( { + productIds, + } ); const isInitialLoading = ( isLoading || imagesLoading ) && ! hasData; const isRefetching = ( isFetching || imagesLoading ) && hasData; @@ -131,19 +126,17 @@ export function TopPerformingProductLeaderboardWidget( { const comparisonItems = comparisonData?.data || []; // Create a map of product_id to comparison data for efficient lookup - const comparisonMap = new Map( - comparisonItems.map( ( item ) => [ item.product_id, item ] ) - ); + const comparisonMap = new Map( comparisonItems.map( item => [ item.product_id, item ] ) ); // Calculate maxValue once outside the map const maxCurrentValue = Math.max( - ...( data?.data?.map( ( p ) => p.product_net_revenue ?? 0 ) || [] ), + ...( data?.data?.map( p => p.product_net_revenue ?? 0 ) || [] ), 1 // Prevent division by zero ); // Calculate max previous value once outside the map const maxPreviousValue = Math.max( - ...comparisonItems.map( ( p ) => p.product_net_revenue ?? 0 ), + ...comparisonItems.map( p => p.product_net_revenue ?? 0 ), 1 // Prevent division by zero ); @@ -151,16 +144,11 @@ export function TopPerformingProductLeaderboardWidget( { data?.data?.map( ( product, index: number ) => { const currentValue = product.product_net_revenue ?? 0; - const productImage = productImages - ? productImages[ product.product_id ] - : undefined; + const productImage = productImages ? productImages[ product.product_id ] : undefined; // Match by product_id instead of index - const comparisonProduct = comparisonMap.get( - product.product_id - ); - const previousValue = - comparisonProduct?.product_net_revenue ?? 0; + const comparisonProduct = comparisonMap.get( product.product_id ); + const previousValue = comparisonProduct?.product_net_revenue ?? 0; const previousShare = comparisonItems.length > 0 && previousValue > 0 @@ -174,13 +162,7 @@ export function TopPerformingProductLeaderboardWidget( { return { id: String( product.product_id || index ), - label: ( - - ), + label: , currentValue, currentShare: ( currentValue / maxCurrentValue ) * 100, previousValue, @@ -191,10 +173,7 @@ export function TopPerformingProductLeaderboardWidget( { ); }, [ data?.data, comparisonData?.data, productImages ] ); - const legendLabels = useMemo( - () => formatLegendLabels( reportParams ), - [ reportParams ] - ); + const legendLabels = useMemo( () => formatLegendLabels( reportParams ), [ reportParams ] ); const hasError = useWidgetError( isError, error, refetch ); if ( hasError ) { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx index 2aeec116eaf0..d245dcdc2806 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx @@ -35,13 +35,8 @@ export type TopPerformingProductsWidgetProps = { * * */ -export function TopPerformingProductsWidget( { - limit = 5, -}: TopPerformingProductsWidgetProps ) { +export function TopPerformingProductsWidget( { limit = 5 }: TopPerformingProductsWidgetProps ) { return ( - + ); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx index d0288e2afde0..605ae530a257 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -3,10 +3,7 @@ */ import { useMemo } from 'react'; import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; -import { - useReportCustomers, - type FilterCondition, -} from '@jetpack-premium-analytics/data'; +import { useReportCustomers, type FilterCondition } from '@jetpack-premium-analytics/data'; import { customer } from '@jetpack-premium-analytics/icons'; /** @@ -44,35 +41,20 @@ type CustomerTypeRevenueWidgetProps = { * * ``` */ -function CustomerTypeRevenueWidget( { - filter, -}: CustomerTypeRevenueWidgetProps ) { +function CustomerTypeRevenueWidget( { filter }: CustomerTypeRevenueWidgetProps ) { const { reportParams } = useWidgetRootContext(); - const { - primary, - comparison, - isLoading, - isFetching, - hasData, - isError, - error, - refetch, - } = useReportCustomers( { - ...reportParams, - filters: filter ? [ filter ] : undefined, - } ); + const { primary, comparison, isLoading, isFetching, hasData, isError, error, refetch } = + useReportCustomers( { + ...reportParams, + filters: filter ? [ filter ] : undefined, + } ); const isInitialLoading = isLoading && ! hasData; const isRefetching = isFetching && hasData; const { chartData } = useMemo( - () => - buildRevenueByCustomerTypeData( - primary.data, - comparison.data, - reportParams - ), + () => buildRevenueByCustomerTypeData( primary.data, comparison.data, reportParams ), [ primary.data, comparison.data, reportParams ] ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx index 225ab34d0e7c..c02ddd00911e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -34,28 +34,14 @@ import { useBarStyles } from '../common'; export function SalesByCouponWidget() { const { reportParams } = useWidgetRootContext(); - const { - primary, - comparison, - isLoading, - isFetching, - hasData, - isError, - error, - refetch, - } = useReportCoupons( reportParams ); + const { primary, comparison, isLoading, isFetching, hasData, isError, error, refetch } = + useReportCoupons( reportParams ); const isInitialLoading = isLoading && ! hasData; const isRefetching = isFetching && hasData; const { chartData } = useMemo( - () => - buildSalesByCouponData( - primary.data, - comparison.data, - reportParams, - 3 - ), + () => buildSalesByCouponData( primary.data, comparison.data, reportParams, 3 ), [ primary.data, comparison.data, reportParams ] ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts index 1d452bd9937a..ab1e276c454b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/index.ts @@ -1,4 +1 @@ -export { - SalesByDeviceWidget, - BookingsByDeviceWidget, -} from './sales-by-device-widget'; +export { SalesByDeviceWidget, BookingsByDeviceWidget } from './sales-by-device-widget'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx index fed9f2fe599d..5a8321c571c1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -3,10 +3,7 @@ */ import { useMemo } from 'react'; import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; -import { - useReportOrderAttribution, - type FilterCondition, -} from '@jetpack-premium-analytics/data'; +import { useReportOrderAttribution, type FilterCondition } from '@jetpack-premium-analytics/data'; import { device } from '@jetpack-premium-analytics/icons'; /** @@ -67,23 +64,14 @@ export function SalesByDeviceWidget( { filter }: SalesByDeviceWidgetProps ) { [ reportParams, filter ] ); - const { - primary, - hasComparison, - isLoading, - isFetching, - hasData, - isError, - error, - refetch, - } = useReportOrderAttribution( paramsWithView ); + const { primary, hasComparison, isLoading, isFetching, hasData, isError, error, refetch } = + useReportOrderAttribution( paramsWithView ); const isInitialLoading = isLoading && ! hasData; const isRefetching = isFetching && hasData; const { chartData } = useMemo( - () => - buildSalesByDeviceData( primary.data, hasComparison, reportParams ), + () => buildSalesByDeviceData( primary.data, hasComparison, reportParams ), [ primary.data, hasComparison, reportParams ] ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx index 938d95e4d101..f29885af81c1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx @@ -59,29 +59,15 @@ export function SalesByUtmWidget( { view }: SalesByUtmWidgetProps ) { [ reportParams, view ] ); - const { - primary, - hasComparison, - isLoading, - isFetching, - hasData, - isError, - error, - refetch, - } = useReportOrderAttribution( params ); + const { primary, hasComparison, isLoading, isFetching, hasData, isError, error, refetch } = + useReportOrderAttribution( params ); const isInitialLoading = isLoading && ! hasData; const isRefetching = isFetching && hasData; - const chartData = useMemo( - () => buildSalesByUtmData( primary.data ), - [ primary.data ] - ); + const chartData = useMemo( () => buildSalesByUtmData( primary.data ), [ primary.data ] ); - const legendLabels = useMemo( - () => formatLegendLabels( reportParams ), - [ reportParams ] - ); + const legendLabels = useMemo( () => formatLegendLabels( reportParams ), [ reportParams ] ); const emptyStateIcon = useMemo( () => { switch ( view ) { @@ -114,8 +100,7 @@ export function SalesByUtmWidget( { view }: SalesByUtmWidgetProps ) { emptyStateIcon={ emptyStateIcon } style={ { - '--a8c--charts--leaderboard--bar--border-radius': - '0 1px 1px 0', + '--a8c--charts--leaderboard--bar--border-radius': '0 1px 1px 0', } as React.CSSProperties } /> diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx index 5896e3e2a085..26774bf98bfc 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx @@ -73,12 +73,7 @@ export function SessionsByDeviceWidget() { return ( <> - + - buildTotalReturnsData( - primary.data, - comparison.data, - reportParams - ), + () => buildTotalReturnsData( primary.data, comparison.data, reportParams ), [ primary.data, comparison.data, reportParams ] ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts index 3871d73b5c2d..27d70820abfe 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts @@ -2,10 +2,7 @@ * External dependencies */ import { useMemo } from 'react'; -import { - type ReportParams, - useReportVisitorsByLocation, -} from '@jetpack-premium-analytics/data'; +import { type ReportParams, useReportVisitorsByLocation } from '@jetpack-premium-analytics/data'; /** * Internal dependencies @@ -30,10 +27,7 @@ type LocationRawData = { * @param region - The region to get data for ('US' or 'world') * @return Geo chart data and leaderboard data for the selected region */ -export function useVisitorsByLocation( - reportParams: ReportParams, - region: Region -) { +export function useVisitorsByLocation( reportParams: ReportParams, region: Region ) { const usReport = useReportVisitorsByLocation( reportParams, { enabled: region === 'US', groupBy: 'region', @@ -57,8 +51,8 @@ export function useVisitorsByLocation( if ( region === 'US' ) { const mapUsRegions = ( items: typeof primaryItems ) => items - .filter( ( item ) => Boolean( item.region ) ) - .map( ( item ) => ( { + .filter( item => Boolean( item.region ) ) + .map( item => ( { id: item.region as string, label: item.region as string, value: item.visitors, @@ -71,12 +65,12 @@ export function useVisitorsByLocation( } return { - primary: primaryItems.map( ( item ) => ( { + primary: primaryItems.map( item => ( { id: item.country_code.toLowerCase(), label: item.label, value: item.visitors, } ) ), - comparison: comparisonItems.map( ( item ) => ( { + comparison: comparisonItems.map( item => ( { id: item.country_code.toLowerCase(), label: item.label, value: item.visitors, @@ -94,8 +88,7 @@ export function useVisitorsByLocation( [ rawData.primary, rawData.comparison, region, hasComparison ] ); - const { isLoading, isFetching, hasData, isError, error, refetch } = - activeReport; + const { isLoading, isFetching, hasData, isError, error, refetch } = activeReport; return { ...chartDataResult, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss index 892f02edfbf9..f70ff394883f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss @@ -18,14 +18,8 @@ // The upstream widget content container (.next-admin-dashboard-widget__content) // sets overflow: auto, which clips the ToggleGroupControl's outward focus ring. // Add padding to create space for the focus indicator. - padding-block-start: var( - --wpds-dimension-padding-xs, - 4px - ); - padding-inline-end: var( - --wpds-dimension-padding-xs, - 4px - ); + padding-block-start: var( --wpds-dimension-padding-xs, 4px ); + padding-inline-end: var( --wpds-dimension-padding-xs, 4px ); padding-block-end: 0; padding-inline-start: 0; } @@ -35,6 +29,6 @@ .leaderboardImage { height: 20px; - border-radius: var(--wpds-border-radius-sm, 2px); + border-radius: var( --wpds-border-radius-sm, 2px ); } } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx index 537b9812bf2d..fa6ee6f3b15b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx @@ -19,10 +19,7 @@ import { location } from '@jetpack-premium-analytics/icons'; import { useWidgetRootContext } from '../../components/widget-root'; import { useWidgetError } from '../../hooks'; import { useVisitorsByLocation, type Region } from './use-visitors-by-location'; -import { - LeaderboardChart, - LeaderboardLabel, -} from '../../components/chart-leaderboard'; +import { LeaderboardChart, LeaderboardLabel } from '../../components/chart-leaderboard'; import { flagUrl } from '../../helpers'; import styles from './visitors-by-location-widget.module.scss'; import { ChartEmptyState } from '../../components'; @@ -55,9 +52,7 @@ export function VisitorsByLocationWidget() { const [ isMinimized, setIsMinimized ] = useState( false ); const rootRef = useRef< HTMLDivElement | null >( null ); const tileButtonRef = useRef< HTMLElement | null >( null ); - const resizeDebounceTimeoutRef = useRef< ReturnType< - typeof setTimeout - > | null >( null ); + const resizeDebounceTimeoutRef = useRef< ReturnType< typeof setTimeout > | null >( null ); const { geoData, @@ -76,10 +71,9 @@ export function VisitorsByLocationWidget() { const leaderboardDataWithImages = useMemo( () => - leaderboardData.map( ( item ) => { + leaderboardData.map( item => { const imageUrl = flagUrl( region === 'US' ? 'us' : item.id ); - const labelText = - typeof item.label === 'string' ? item.label : ''; + const labelText = typeof item.label === 'string' ? item.label : ''; const imageAlt = region === 'US' ? __( 'United States flag', 'jetpack-premium-analytics' ) @@ -110,14 +104,10 @@ export function VisitorsByLocationWidget() { return; } - const nextIsMinimized = isSingleColumnTileFromGridColumnEnd( - tileButton.style.gridColumnEnd - ); + const nextIsMinimized = isSingleColumnTileFromGridColumnEnd( tileButton.style.gridColumnEnd ); // Avoid scheduling React state updates when nothing changes. - setIsMinimized( ( prev ) => - prev === nextIsMinimized ? prev : nextIsMinimized - ); + setIsMinimized( prev => ( prev === nextIsMinimized ? prev : nextIsMinimized ) ); }, [] ); const debouncedResizeUpdate = useCallback( () => { @@ -126,10 +116,7 @@ export function VisitorsByLocationWidget() { if ( resizeDebounceTimeoutRef.current ) { clearTimeout( resizeDebounceTimeoutRef.current ); } - resizeDebounceTimeoutRef.current = setTimeout( - updateIsMinimized, - RESIZE_DEBOUNCE_MS - ); + resizeDebounceTimeoutRef.current = setTimeout( updateIsMinimized, RESIZE_DEBOUNCE_MS ); }, [ updateIsMinimized ] ); const resizeObserverRef = useResizeObserver( () => { @@ -141,10 +128,7 @@ export function VisitorsByLocationWidget() { // DataViews picker grid: always render the simplified (map-only) tile // and avoid attaching any observers/listeners. - const dataViewsPickerGrid = closestHTMLElement( - root, - '.dataviews-view-picker-grid' - ); + const dataViewsPickerGrid = closestHTMLElement( root, '.dataviews-view-picker-grid' ); if ( dataViewsPickerGrid ) { tileButtonRef.current = null; @@ -199,11 +183,7 @@ export function VisitorsByLocationWidget() { : {}; const geoChart = ( - + ); const hasError = useWidgetError( isError, error, refetch ); @@ -243,11 +223,8 @@ export function VisitorsByLocationWidget() { __next40pxDefaultSize isBlock hideLabelFromVision - label={ __( - 'Location', - 'jetpack-premium-analytics' - ) } - onChange={ ( value ) => { + label={ __( 'Location', 'jetpack-premium-analytics' ) } + onChange={ value => { if ( isRegion( value ) ) { setRegion( value ); } @@ -256,17 +233,11 @@ export function VisitorsByLocationWidget() { >
From 284a65ccf3989d5fd594cbe924e921621000c68b Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:28:16 +0800 Subject: [PATCH 26/32] chore(premium-analytics): wire widgets-toolkit deps and relax lint for the port --- pnpm-lock.yaml | 582 +++++++++++++++++- .../premium-analytics/eslint.config.mjs | 16 + .../packages/premium-analytics/package.json | 3 + 3 files changed, 572 insertions(+), 29 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c7d0af6d59d..fdf7caf7545e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3866,6 +3866,9 @@ importers: projects/packages/premium-analytics: dependencies: + '@automattic/charts': + specifier: workspace:* + version: link:../../js-packages/charts '@automattic/number-formatters': specifier: workspace:* version: link:../../js-packages/number-formatters @@ -3896,6 +3899,9 @@ importers: '@wordpress/data': specifier: 10.46.0 version: 10.46.0(react@18.3.1) + '@wordpress/dataviews': + specifier: 16.0.0 + version: 16.0.0(react@18.3.1) '@wordpress/i18n': specifier: ^6.9.0 version: 6.19.0 @@ -3911,6 +3917,9 @@ importers: '@wordpress/route': specifier: 0.12.0 version: 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/theme': + specifier: 0.13.0 + version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/ui': specifier: 0.13.0 version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3956,7 +3965,7 @@ importers: version: 8.0.0 '@wordpress/build': specifier: 0.14.0 - version: 0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) + version: 0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2) browserslist: specifier: 4.28.2 version: 4.28.2 @@ -7882,6 +7891,17 @@ packages: '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + '@emotion/native@11.11.0': + resolution: {integrity: sha512-t1b5bLv+o5OUNLqXlnw+LJYU10OpmYkLC/1W873Y1ohG+vObx5TT3o3Eh1okXb2KCuZTTBPgsEnU/Sl7NNkJ9Q==} + peerDependencies: + react-native: '>=0.14.0 <1' + + '@emotion/primitives-core@11.13.2': + resolution: {integrity: sha512-+MX60ROt1fDi5EYafhE/zs78XD4OuFUn6j0Z274wo5wVMT8sSBRx2CKPMbOUnmCcT0K5GPog+41mtkcppzkMmg==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + react: '>=16.8.0' + '@emotion/react@11.14.0': resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} peerDependencies: @@ -10870,6 +10890,10 @@ packages: resolution: {integrity: sha512-9VKhQHB/TQHJciOtxbpJ5JPhxMHCOszcxs4eL27krFXMEp3fl4tzVy13r1LPuXg/yjZ9NpV3NY+Qwx4G0aW3Kw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/a11y@4.48.0': + resolution: {integrity: sha512-MXwBc2sYaemZCn1dqVutTbLdM6iy4bx/HS9hHR/+pRpaSVJUlguZ1aQ0BaoIbE4u0uOezGGc5d2bDfWCti3Dww==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/admin-ui@2.1.0': resolution: {integrity: sha512-wFZ6pOxex/3iz1NuV27/7uL/iJtbqrLPqWKuQlpaJiXL9ZLj2JvHWkQVpvzi10EzIDQLqX2WdSOzS0SyXIro2w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10908,6 +10932,10 @@ packages: resolution: {integrity: sha512-livtgwnvBm7xbpm/gaBxwtdZm3KCXq210UNsr48WA8TGfi/OfZ4oOzk4Mp4/ZHsq2baaXzhZ0iXjyR7oyaOTsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/base-styles@9.1.0': + resolution: {integrity: sha512-QONqtlA7IRYb6cbCjwTEiXJwfkWPpHl6PSS+F1TDeDP0L7m+hXfpRbH1qfKjSffWlyDaBqLFWwXZ3evpeFw5bg==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/blob@4.46.0': resolution: {integrity: sha512-TUu6k4SFPThT86ek/O87/aQfwKVYAGG9Gt14uvYPXPyLz90/KeFLr15v9waZV2luk2xCZACMIa4OdBHQlkL/aQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10980,12 +11008,25 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/components@35.0.0': + resolution: {integrity: sha512-zXhErp2/alcdvQST6pq/kkZGkiOvTGbYqRc3FuoQIDpCJJE70r243PRxokDvZ5ikHvBtg26kARN+JhXdzN4qjw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@wordpress/compose@7.46.0': resolution: {integrity: sha512-6Yv9Wb6tlA4JYU9bdWWuIWpTTzBAVA1zrYu1GY9x2/mCOckk9iLcEEfbKULxdjwwcMo3SKqvyby4f6kEUw/Wsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 + '@wordpress/compose@8.1.0': + resolution: {integrity: sha512-CAEQxrh3f19ku0SAnYAAiKcUe1zqaK9f0c8vJh+6qrpQnUjl7xLXj5TJOukXlzFH3Z9VZn6fJVfXqmDNAYIhQA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/core-data@7.46.0': resolution: {integrity: sha512-mfiqOrXcsv4rZJZFYjmUSc5goK1cKpuQ1lSoSBnuKMJNZAxTCVTwexIaj0XI5Qr/ngUjT5U1+w4I0Fzuv/qCMQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11005,16 +11046,32 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/data@10.48.0': + resolution: {integrity: sha512-6SjfTBlXu5fuJWmmlHlwV2wcrcsWL+M5O227AoEvrPSLo96UuMj2kAx3cKLtP3xyOMDyd38koQSf6+SS522bTA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/dataviews@14.3.0': resolution: {integrity: sha512-2fFSgyatDldjPb2gO+vLDwkbI2Jw+8zd/O0/BwLftQ5QhrrRtAqECFp+eYzcQ8Onh8OMhxq0n7tsaIHE/jWqJQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 + '@wordpress/dataviews@16.0.0': + resolution: {integrity: sha512-02rbslxalTNasLV8w/zAifCsUU5Pug8GiduWIEKRiNtazvJ8duz8fIcQ2Jgl31ruRItcu3fcG7XUk1OtwsdcZQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/date@5.46.0': resolution: {integrity: sha512-phbKy1siTFGwFet5hQzaSZJB1mMDIXflMLKj+oJ/mT/m9ughp3seFDPvKoL+UzukLxNJh3l5G5h1l9XQFfC2cA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/date@5.48.0': + resolution: {integrity: sha512-HgXtYAD2IOrPDY83xzkT/8abYj2nMlkbC+lfSpB4lExlSVrIbz5oYUtktH8k5EBZjVBMFsE7mdMQyQjUeCQbeQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dependency-extraction-webpack-plugin@6.46.0': resolution: {integrity: sha512-Lm2JFEI4NrcEQFdnIXK+CsUQGK/LTiRxrDY0ocpTLt5hhb3DJm3Ds2HFn8fa//H0U5B3FvO3XyGMHOUf9Q12Pg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11025,14 +11082,26 @@ packages: resolution: {integrity: sha512-d4Dy9GeJ/VIORTgYKYXT026/hhpV6VOf3VUDj10f+QFoIJ86VMBrzV6KQn8KUVH4T3oH1MSpo/A5t8ttYFemsg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/deprecated@4.48.0': + resolution: {integrity: sha512-aTa7oww6hvTjfIvxLsxlcwYj7skAGPnr1V2S0iBVQfiIn5wJPiGjM9hz4QEf6kyR44Vh0IYjW9wSxVuDMGZUdw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom-ready@4.46.0': resolution: {integrity: sha512-CQ6KPaCkMzAmbxmR4E4Fu99ngyPpkP9VGaIFu0xUgx0ubkYOzcvEfEEPuyEV3n7PY2Jg/XWzBilgWCa8PmaxWw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom-ready@4.48.0': + resolution: {integrity: sha512-jtH9/4FBTsfYLJDzgiXs41nceTrfvuLXqaWa5IN8drHvXZde6Dhz78m3KCZLrOB5DEE1tbyBNyZkcWM8HNVZ0Q==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom@4.46.0': resolution: {integrity: sha512-XngkvNJpf0JnpZuOcsbBl/cTprfYQTfSykttIL4laXcFXfZe8rU3bGgv8K7AEoYigDwxfw3g/yMPi4fn195Kpw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/dom@4.48.0': + resolution: {integrity: sha512-9UARZ0YQfmhx9VAi+QynSwu5fOJoG4mmPNTpYW8jDmtKh+9c2YIi1YSQFuOa1sipj78ZLPaBxaceZ7dbxKc3UA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/e2e-test-utils-playwright@1.46.0': resolution: {integrity: sha512-Bls5BGRNda0Oo4biTZ/KIwO8iHBeovvfWNfrPXReIsrW1td1UqXw2Z9l0/LaP3euJZFNom2QExHCOba+8eN5lQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11058,10 +11127,18 @@ packages: resolution: {integrity: sha512-hjnrqZi0cZVdkmN0xQavKfSQJYAkb9pVSnDPpuX65OLxeD9/EWkIXvFzBb+nH8c4NzKKSqQU96XCTQrH37OCIA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/element@8.0.0': + resolution: {integrity: sha512-lQ8TB2vBr6lzcwQ2zh0xy+FC77Demb3FqL81fzpSsLbGUF9hZiTyyUuwc6SG21gCkGGjVm2TnU9BuHax/8nDfQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/escape-html@3.46.0': resolution: {integrity: sha512-SzrVQwLQBZdaSStYVpTKeYqp97NABz1w551T8me3msDDsfhWWPhSZiZTNaGZ6iqUNfOX2uKyZsqXedvkqwLHqA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/escape-html@3.48.0': + resolution: {integrity: sha512-phw399RofSqTqIM4DikmkDfgJ7exDYgPfDuxjv3D2YnUTTUsR+U9fA+pA+/rNUiZD1YOmVILQmkJt6oLaVM+nQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/eslint-plugin@25.2.0': resolution: {integrity: sha512-h3Yz5Qzo1v53Rw9i8WBm68P6SFpVSeqWDohowpEeuIz2RC8Jg1CT5j49tVpSZXGNCQGSf3SaPLjXmiyxTZXkSw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11119,15 +11196,28 @@ packages: resolution: {integrity: sha512-fsKw4dmw4voIRoKc8t0XRREQlFvwj9XS/jTXvkh6mqRYCDpaEnrdB2Ji5jgbRXEMPU0GKVGMeAn5Wwi56gjBMg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/hooks@4.48.0': + resolution: {integrity: sha512-rU1yGEy0Mb+2oRG5QX/bKIIwKQmYAvATfUQeXIF20/mbR0qutYeVTCIvWEyb4pf71tvnQFiN18RWRXWsvKrDbQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/html-entities@4.46.0': resolution: {integrity: sha512-YJ/V9R2p4lwYkhc9/bQrXxoX0rNDtt1WQGInKAxRWqF1w1gYQk0iWiwGcNnahnFofwK2LJSVf4/jYFjJrS/sPw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/html-entities@4.48.0': + resolution: {integrity: sha512-KGxdaLC36wE10GybSfjYGcyWiy+KQCYheB6T8jhZhQ9mlf2Zwx6aJgfZm/L6BLwNN33Efx+sJY3nvMIxI5UwnA==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/i18n@6.19.0': resolution: {integrity: sha512-hRXd2E0SF9OQf22ZZWw7Ny/o+Q9u8jINiF1p0bF+rnSDKQUgoStihak6YiazWVRiIEYwctzotKXlt0HePJelXA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} hasBin: true + '@wordpress/i18n@6.21.0': + resolution: {integrity: sha512-IXGGUJqN6b7QddU0dZB3HLJKu6uDQuhLsrrzYpUYTjDhfa43XEaikA9xHNgZhqzRtOVYqsNHVliWcISvJ/xjZQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + hasBin: true + '@wordpress/icons@10.32.0': resolution: {integrity: sha512-1WvJdT361X1LnetYBpBWUjAVXZzl+pBdIwHbYRAp8ej47EI/igPmNxmq81nFd40s8fer/9qtipielcqSI6H2rA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11140,6 +11230,12 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/icons@13.3.0': + resolution: {integrity: sha512-Y/iE3aeHQ4XkX0fffiTPCUfjT8wNw1I7hDJkKqpaLmkD+C5NKWixRrDVfRnaJqU/MxY8RdyVC/nGng2MLPNH0A==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/image-cropper@1.10.0': resolution: {integrity: sha512-Aq4Wz2nGf+GYZPi+n+nWq7AtsGSjUbREzYKEUQIAnt2pPGn2ZkvooBUQE405WOqoJadE+tkLjKdyjY24iOtocA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11166,6 +11262,10 @@ packages: resolution: {integrity: sha512-46J36GNPw7q3c5HF0RurUx9yJHvBDYqOFVqbb8Td8bov9pVI6TGtcMKd+/O+Q89ZUVSTVx/NfxKjNwXpeQQCmg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/is-shallow-equal@5.48.0': + resolution: {integrity: sha512-7ipiZ1+m84RfuVhiMbtKm6RN571W3ERV/pTL+fSG2qOVhLqccFmliuFHTKQG+0KIhV8DegOlE6eoKOenf+I9ng==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/jest-console@8.46.0': resolution: {integrity: sha512-bD5FD/LDbDyfadZzxfUCOM6uBXlIfRFj+AAsgmCHuUBW3c7PrsZXDGh5KRaR8E0XLoeLpkxA78fpUaY8S1+XPw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11182,6 +11282,10 @@ packages: resolution: {integrity: sha512-+eW0b4bRrpmiOOfdmz1BtQsbTqWqCkgJyeiR5yMLJ+sGG2He9icVLjt/fSc4xCQ56MhT03Zypb33L6j+zJFEgA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/keycodes@4.48.0': + resolution: {integrity: sha512-u3Uxxe3rDAqEmerAiJ2X94s7iO3ZVgS+10MFyD4nWhfuB/C6m/M2TqHPgZiKvyDH04EIhe+pIF2KFO4pq7NWsw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/latex-to-mathml@1.14.0': resolution: {integrity: sha512-kLzovQBKlBSHsqXICIclUBOFCm9ROEmXU+xUaV6UpS2pb4BIcsBew/z8URKMHtYA/jRhDM9uejEy45YMI8swZw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11247,14 +11351,28 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/primitives@4.48.0': + resolution: {integrity: sha512-dfF7IZotIqb6LUiGs7oPwKbSF8RPoC0JDSIrtxvgwFA/yvbc/pDIp/Zs0O8GvxZNxu4JIVnKskOhoLq7lAeziQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/priority-queue@3.46.0': resolution: {integrity: sha512-rjwzO/I7Os16VMJFVdzIeXMmyvwe+DbODrXl3mgW5LZZeIYob94d++pjQxUdWN1/0APnXPQP6zk4yFfSLOVkYg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/priority-queue@3.48.0': + resolution: {integrity: sha512-NuGrfSSnBC794erb3xSEKrzWLGCNLa+ukob0pyVRtnebU7fPgrhx4NCBCXYK1vTcAta3NAkOVRfUZgcmLFYA6g==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/private-apis@1.46.0': resolution: {integrity: sha512-l8dsEuxq6CrtsI7Twfpn6CbPHmGBUQoGN4oLPJG1Bqsr1yXXLU/bEx9KAQN9emxRjXaELPsn7x7TVx0TUoKyJw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/private-apis@1.48.0': + resolution: {integrity: sha512-HHOSXLCAlBggfMozwWtX36wgsSt22g2tZwpka47Rjzr3hNY1BZ6SrrFJumiNxooy5PDKbRgcF092PAF82hdJXg==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/react-i18n@4.45.0': resolution: {integrity: sha512-9DchjKkdgw5r51br2fIqlMZ2zOd8Fk9xh3Kh8GRbTmeXfFKmgGz8okUrlbRgYPJKdh4b7slSkyQY26FrV7X/0w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11265,6 +11383,12 @@ packages: peerDependencies: redux: '>=4' + '@wordpress/redux-routine@5.48.0': + resolution: {integrity: sha512-MxRgJJyddivxvVhPrn8yEFXTH3WLtoRGNCMiBRJwoIr4GkY8iOFSfRaqOJEkE1zrP4JK6qGFmv1xMvWt78c7ow==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + redux: '>=4' + '@wordpress/reusable-blocks@5.46.0': resolution: {integrity: sha512-bAX9YGgb8OfTxNKimxzfuWj/39EP9jtJXKVYcChDgYPKH1gl02I8+OdyFx+RycWFxRjMTwsgL5WEjgkGC6nokA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11278,6 +11402,12 @@ packages: peerDependencies: react: ^18.0.0 + '@wordpress/rich-text@7.48.0': + resolution: {integrity: sha512-rMiTTpRnpdynL9BnuI2MkSXzd12Js8gYSnlbVwxNNKNeFEXT+3Ah2oNCGvSb82pD/73Bl5BIGC5395D5a3X9yw==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + '@wordpress/route@0.12.0': resolution: {integrity: sha512-JQ55LLMnzxjWWCd0UWnFrp1pTTC7u4KEA8w/IT01oV27Lyw4BQighl7KTE/mpN5CYupXUHUELCFceWyHiMDV4w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11309,6 +11439,10 @@ packages: resolution: {integrity: sha512-9pLmilkgWqTvIlrnnXbW7ECfEPvCSYOve7btXgYGgMOzrGs12ijnG+kSGGg0aJhEV8OCzQ/QdVBh4s1zQZ0bLQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} + '@wordpress/style-runtime@0.4.0': + resolution: {integrity: sha512-frzAg1rsn8X0KNgrxxLxszLvWCKY0Nk2e8j8Mjm2pI2URmS8Et7NefuXP3JnHBD4U1L1Ug9yKO/FA65ojQ7CEA==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + '@wordpress/stylelint-config@23.38.0': resolution: {integrity: sha512-F1Bo45fhWFrpEXlkkwVfopmmgM8PwbzplrlBwu1FGm+9ohF890IXKhjjQ/CDphE9pMBCQnAyofF6ESymhbEm5A==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11331,6 +11465,17 @@ packages: stylelint: optional: true + '@wordpress/theme@0.15.0': + resolution: {integrity: sha512-qoozJ4YEPb0LvTBnTMj8a7kPlQtT2LeGL7b/vKJkvnB9dIEUOED5c0rpeRZJoK9b77fpUH5GwYzPE3IWiQ6l2w==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + stylelint: '>=16.8.2' + peerDependenciesMeta: + stylelint: + optional: true + '@wordpress/token-list@3.46.0': resolution: {integrity: sha512-g9UytUCFcLnj8LWNHFUK0c53FeokTEXDlZ3C3VrpDnxq0jC0BnNj0uJCAmbzfehg23LWI2O5xnQzmpAJ9ldAKg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11342,10 +11487,21 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@wordpress/ui@0.15.0': + resolution: {integrity: sha512-7aAx1ovnC6JOb4Qfcnfk8ESfB0RTm6rqsdFrUn7TEY3LON/aEQisCb/bd7Yb8s9txb1GfaJYkgjiTvrr0M6EWA==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + '@wordpress/undo-manager@1.46.0': resolution: {integrity: sha512-vAchoUrF97IdjqVD30Iz7NI9YvDtgeMNPshgjsrM8MF9nOCMq2tBWb3HS+ue/kQknfAuU73FEnn/UNKt0JPH4Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/undo-manager@1.48.0': + resolution: {integrity: sha512-HqPGxMvZeWZJ6AVaCqZhfGpH6tqq5+hMlaqh4aCO0SvZ2Gvc6fbXEoVpqWfKozO1DyJW2GnRf8At8PpPt2IopQ==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/upload-media@0.31.0': resolution: {integrity: sha512-fpg1wx1p04AEyemQ7EGsR1c2oHcHCgsVdKbSl27L4Nvw2YlhiZY6yb/BOqUNyBqTJ6OBS85vKace6JzW6KR27w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11375,6 +11531,10 @@ packages: resolution: {integrity: sha512-Z1CE6x732iMD+NcWziitqWUyhxVy1JlioHDtQUU2oqhDcA0d/P2ifOc/af02dDYFIuLh7umurU19LqpBX6EoWw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/warning@3.48.0': + resolution: {integrity: sha512-En+A99j8aySNzUH0iXok0H2Xi+Uw2useKqYsvPm33VEMa0a0XIwa2I9srK5STp8RydCm1dK+/41K9e5xeFu23Q==} + engines: {node: '>=18.12.0', npm: '>=8.19.2'} + '@wordpress/widgets@4.46.0': resolution: {integrity: sha512-IwX6Vpfan3ShU/ezu9lzOcS4hGte2MyNFvc54TlXAb+nyQ9rYsvfNAmZAaVEXyJemBJJeH0tABVa8Ql3gtvUYA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -12341,6 +12501,10 @@ packages: csp_evaluator@1.1.5: resolution: {integrity: sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==} + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + css-declaration-sorter@7.4.0: resolution: {integrity: sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==} engines: {node: ^14 || ^16 || >=18} @@ -12394,6 +12558,9 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -18520,7 +18687,7 @@ snapshots: '@automattic/api-core@1.0.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@automattic/shopping-cart': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/dataviews': 14.3.0(@types/react@18.3.28)(react@18.3.1) + '@wordpress/dataviews': 16.0.0(@types/react@18.3.28)(react@18.3.1) '@wordpress/i18n': 6.19.0 he: 1.2.0 tslib: 2.8.1 @@ -18529,6 +18696,7 @@ snapshots: - '@types/react' - react - react-dom + - react-native - stylelint - supports-color @@ -18662,6 +18830,7 @@ snapshots: transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react' + - react-native - stylelint - supports-color @@ -18743,6 +18912,7 @@ snapshots: transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react' + - react-native - stylelint - supports-color @@ -20064,6 +20234,19 @@ snapshots: '@emotion/memoize@0.9.0': {} + '@emotion/native@11.11.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/primitives-core': 11.13.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@emotion/react' + - react + + '@emotion/primitives-core@11.13.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + css-to-react-native: 3.2.0 + react: 18.3.1 + '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 @@ -23564,6 +23747,11 @@ snapshots: '@wordpress/dom-ready': 4.46.0 '@wordpress/i18n': 6.19.0 + '@wordpress/a11y@4.48.0': + dependencies: + '@wordpress/dom-ready': 4.48.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/admin-ui@2.1.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/components': 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -23640,6 +23828,8 @@ snapshots: '@wordpress/base-styles@8.0.0': {} + '@wordpress/base-styles@9.1.0': {} + '@wordpress/blob@4.46.0': {} '@wordpress/block-editor@15.19.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -24165,33 +24355,6 @@ snapshots: - browserslist - supports-color - '@wordpress/build@0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': - dependencies: - '@emotion/babel-plugin': 11.13.5 - '@wordpress/style-runtime': 0.2.0 - autoprefixer: 10.5.0(postcss@8.5.14) - browserslist-to-esbuild: 2.1.1(browserslist@4.28.2) - change-case: 4.1.2 - chokidar: 4.0.3 - cssnano: 7.1.9(postcss@8.5.14) - esbuild: 0.27.4 - esbuild-plugin-babel: 0.2.3(@babel/core@7.29.0) - esbuild-sass-plugin: 3.3.1(esbuild@0.27.4)(sass-embedded@1.97.3) - fast-glob: 3.3.3 - moment-timezone: 0.5.48 - postcss: 8.5.14 - postcss-modules: 6.0.1(postcss@8.5.14) - rtlcss: 4.3.0 - sass-embedded: 1.97.3 - optionalDependencies: - '@wordpress/boot': 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/private-apis': 1.46.0 - '@wordpress/route': 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - transitivePeerDependencies: - - '@babel/core' - - browserslist - - supports-color - '@wordpress/build@0.14.0(@babel/core@7.29.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/theme@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)': dependencies: '@emotion/babel-plugin': 11.13.5 @@ -24342,6 +24505,68 @@ snapshots: - '@emotion/is-prop-valid' - supports-color + '@wordpress/components@35.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/utc': 2.1.1 + '@emotion/cache': 11.14.0 + '@emotion/css': 11.13.5 + '@emotion/native': 11.11.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@emotion/utils': 1.4.2 + '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/gradient-parser': 1.1.0 + '@types/highlight-words-core': 1.2.1 + '@types/react': 18.3.28 + '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/a11y': 4.48.0 + '@wordpress/base-styles': 9.1.0 + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/date': 5.48.0 + '@wordpress/deprecated': 4.48.0 + '@wordpress/dom': 4.48.0 + '@wordpress/element': 8.0.0 + '@wordpress/escape-html': 3.48.0 + '@wordpress/hooks': 4.48.0 + '@wordpress/html-entities': 4.48.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/icons': 13.3.0(react@18.3.1) + '@wordpress/is-shallow-equal': 5.48.0 + '@wordpress/keycodes': 4.48.0 + '@wordpress/primitives': 4.48.0(react@18.3.1) + '@wordpress/private-apis': 1.48.0 + '@wordpress/rich-text': 7.48.0(react@18.3.1) + '@wordpress/style-runtime': 0.4.0 + '@wordpress/ui': 0.15.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/warning': 3.48.0 + change-case: 4.1.2 + clsx: 2.1.1 + colord: 2.9.3 + csstype: 3.2.3 + date-fns: 4.1.0 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + gradient-parser: 1.1.1 + highlight-words-core: 1.2.3 + is-plain-object: 5.0.0 + memize: 2.1.1 + path-to-regexp: 6.3.0 + re-resizable: 6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: 9.14.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + remove-accents: 0.5.0 + uuid: 14.0.0 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - react-native + - stylelint + - supports-color + '@wordpress/compose@7.46.0(react@18.3.1)': dependencies: '@types/mousetrap': 1.6.15 @@ -24357,6 +24582,22 @@ snapshots: react: 18.3.1 use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/compose@8.1.0(react@18.3.1)': + dependencies: + '@types/mousetrap': 1.6.15 + '@wordpress/deprecated': 4.48.0 + '@wordpress/dom': 4.48.0 + '@wordpress/element': 8.0.0 + '@wordpress/is-shallow-equal': 5.48.0 + '@wordpress/keycodes': 4.48.0 + '@wordpress/priority-queue': 3.48.0 + '@wordpress/private-apis': 1.48.0 + '@wordpress/undo-manager': 1.48.0 + change-case: 4.1.2 + mousetrap: 1.6.5 + react: 18.3.1 + use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/core-data@7.46.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/api-fetch': 7.46.0 @@ -24478,6 +24719,24 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/data@10.48.0(react@18.3.1)': + dependencies: + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/deprecated': 4.48.0 + '@wordpress/element': 8.0.0 + '@wordpress/is-shallow-equal': 5.48.0 + '@wordpress/priority-queue': 3.48.0 + '@wordpress/private-apis': 1.48.0 + '@wordpress/redux-routine': 5.48.0(redux@5.0.1) + deepmerge: 4.3.1 + equivalent-key-map: 0.2.2 + is-plain-object: 5.0.0 + is-promise: 4.0.0 + react: 18.3.1 + redux: 5.0.1 + rememo: 4.0.2 + use-memo-one: 1.1.3(react@18.3.1) + '@wordpress/dataviews@14.3.0(@types/react@18.3.28)(react@18.3.1)': dependencies: '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -24580,12 +24839,122 @@ snapshots: - stylelint - supports-color + '@wordpress/dataviews@16.0.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/tz': 1.4.1 + '@wordpress/base-styles': 9.1.0 + '@wordpress/components': 35.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/data': 10.48.0(react@18.3.1) + '@wordpress/deprecated': 4.48.0 + '@wordpress/element': 8.0.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/icons': 13.3.0(react@18.3.1) + '@wordpress/keycodes': 4.48.0 + '@wordpress/primitives': 4.48.0(react@18.3.1) + '@wordpress/private-apis': 1.48.0 + '@wordpress/ui': 0.15.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/warning': 3.48.0 + clsx: 2.1.1 + react: 18.3.1 + remove-accents: 0.5.0 + optionalDependencies: + '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/cache': 11.14.0 + '@emotion/css': 11.13.5 + '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) + '@emotion/utils': 1.4.2 + '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/date': 5.48.0 + '@wordpress/hooks': 4.48.0 + change-case: 4.1.2 + colord: 2.9.3 + date-fns: 4.1.0 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + highlight-words-core: 1.2.3 + is-plain-object: 5.0.0 + memize: 2.1.1 + react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: 9.14.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + use-memo-one: 1.1.3(react@18.3.1) + uuid: 14.0.0 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - react-native + - stylelint + - supports-color + + '@wordpress/dataviews@16.0.0(react@18.3.1)': + dependencies: + '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/tz': 1.4.1 + '@wordpress/base-styles': 9.1.0 + '@wordpress/components': 35.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/data': 10.48.0(react@18.3.1) + '@wordpress/deprecated': 4.48.0 + '@wordpress/element': 8.0.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/icons': 13.3.0(react@18.3.1) + '@wordpress/keycodes': 4.48.0 + '@wordpress/primitives': 4.48.0(react@18.3.1) + '@wordpress/private-apis': 1.48.0 + '@wordpress/ui': 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@wordpress/warning': 3.48.0 + clsx: 2.1.1 + react: 18.3.1 + remove-accents: 0.5.0 + optionalDependencies: + '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@emotion/cache': 11.14.0 + '@emotion/css': 11.13.5 + '@emotion/react': 11.14.0(react@18.3.1) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(react@18.3.1))(react@18.3.1) + '@emotion/utils': 1.4.2 + '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@use-gesture/react': 10.3.1(react@18.3.1) + '@wordpress/date': 5.48.0 + '@wordpress/hooks': 4.48.0 + change-case: 4.1.2 + colord: 2.9.3 + date-fns: 4.1.0 + deepmerge: 4.3.1 + fast-deep-equal: 3.1.3 + framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + highlight-words-core: 1.2.3 + is-plain-object: 5.0.0 + memize: 2.1.1 + react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: 9.14.0(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + use-memo-one: 1.1.3(react@18.3.1) + uuid: 14.0.0 + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@types/react' + - react-native + - stylelint + - supports-color + '@wordpress/date@5.46.0': dependencies: '@wordpress/deprecated': 4.46.0 moment: 2.30.1 moment-timezone: 0.5.48 + '@wordpress/date@5.48.0': + dependencies: + '@wordpress/deprecated': 4.48.0 + moment: 2.30.1 + moment-timezone: 0.5.48 + '@wordpress/dependency-extraction-webpack-plugin@6.46.0(webpack@5.105.2)': dependencies: json2php: 0.0.7 @@ -24595,12 +24964,22 @@ snapshots: dependencies: '@wordpress/hooks': 4.46.0 + '@wordpress/deprecated@4.48.0': + dependencies: + '@wordpress/hooks': 4.48.0 + '@wordpress/dom-ready@4.46.0': {} + '@wordpress/dom-ready@4.48.0': {} + '@wordpress/dom@4.46.0': dependencies: '@wordpress/deprecated': 4.46.0 + '@wordpress/dom@4.48.0': + dependencies: + '@wordpress/deprecated': 4.48.0 + '@wordpress/e2e-test-utils-playwright@1.46.0(@playwright/test@1.60.0)(@types/node@24.12.3)': dependencies: '@playwright/test': 1.60.0 @@ -24961,8 +25340,21 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@wordpress/element@8.0.0': + dependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@wordpress/deprecated': 4.48.0 + '@wordpress/escape-html': 3.48.0 + change-case: 4.1.2 + is-plain-object: 5.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@wordpress/escape-html@3.46.0': {} + '@wordpress/escape-html@3.48.0': {} + '@wordpress/eslint-plugin@25.2.0(@babel/core@7.29.0)(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint-plugin-import@2.32.0)(eslint-plugin-jest@29.15.2(eslint@10.4.1)(jest@30.4.2)(typescript@5.9.3))(eslint-plugin-jsdoc@63.0.0(eslint@10.4.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1))(eslint-plugin-playwright@2.10.4(eslint@10.4.1))(eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint@10.4.1)(wp-prettier@3.0.3))(eslint-plugin-react-hooks@7.1.1(eslint@10.4.1))(eslint-plugin-react@7.37.5(eslint@10.4.1))(eslint@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0(typescript@5.9.3))(typescript@5.9.3)(wp-prettier@3.0.3)': dependencies: '@babel/core': 7.29.0 @@ -25252,8 +25644,12 @@ snapshots: '@wordpress/hooks@4.46.0': {} + '@wordpress/hooks@4.48.0': {} + '@wordpress/html-entities@4.46.0': {} + '@wordpress/html-entities@4.48.0': {} + '@wordpress/i18n@6.19.0': dependencies: '@tannin/sprintf': 1.3.3 @@ -25262,6 +25658,14 @@ snapshots: memize: 2.1.1 tannin: 1.2.0 + '@wordpress/i18n@6.21.0': + dependencies: + '@tannin/sprintf': 1.3.3 + '@wordpress/hooks': 4.48.0 + gettext-parser: 1.4.0 + memize: 2.1.1 + tannin: 1.2.0 + '@wordpress/icons@10.32.0(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 @@ -25276,6 +25680,13 @@ snapshots: change-case: 4.1.2 react: 18.3.1 + '@wordpress/icons@13.3.0(react@18.3.1)': + dependencies: + '@wordpress/element': 8.0.0 + '@wordpress/primitives': 4.48.0(react@18.3.1) + change-case: 4.1.2 + react: 18.3.1 + '@wordpress/image-cropper@1.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/components': 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -25351,6 +25762,8 @@ snapshots: '@wordpress/is-shallow-equal@5.46.0': {} + '@wordpress/is-shallow-equal@5.48.0': {} + '@wordpress/jest-console@8.46.0(jest@30.4.2)': dependencies: jest: 30.4.2 @@ -25368,6 +25781,10 @@ snapshots: dependencies: '@wordpress/i18n': 6.19.0 + '@wordpress/keycodes@4.48.0': + dependencies: + '@wordpress/i18n': 6.21.0 + '@wordpress/latex-to-mathml@1.14.0': dependencies: temml: 0.10.34 @@ -25787,12 +26204,24 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@wordpress/primitives@4.48.0(react@18.3.1)': + dependencies: + '@wordpress/element': 8.0.0 + clsx: 2.1.1 + react: 18.3.1 + '@wordpress/priority-queue@3.46.0': dependencies: requestidlecallback: 0.3.0 + '@wordpress/priority-queue@3.48.0': + dependencies: + requestidlecallback: 0.3.0 + '@wordpress/private-apis@1.46.0': {} + '@wordpress/private-apis@1.48.0': {} + '@wordpress/react-i18n@4.45.0': dependencies: '@wordpress/element': 6.46.0 @@ -25806,6 +26235,13 @@ snapshots: redux: 5.0.1 rungen: 0.3.2 + '@wordpress/redux-routine@5.48.0(redux@5.0.1)': + dependencies: + is-plain-object: 5.0.0 + is-promise: 4.0.0 + redux: 5.0.1 + rungen: 0.3.2 + '@wordpress/reusable-blocks@5.46.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/base-styles': 8.0.0 @@ -25891,6 +26327,22 @@ snapshots: memize: 2.1.1 react: 18.3.1 + '@wordpress/rich-text@7.48.0(react@18.3.1)': + dependencies: + '@wordpress/a11y': 4.48.0 + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/data': 10.48.0(react@18.3.1) + '@wordpress/deprecated': 4.48.0 + '@wordpress/dom': 4.48.0 + '@wordpress/element': 8.0.0 + '@wordpress/escape-html': 3.48.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/keycodes': 4.48.0 + '@wordpress/private-apis': 1.48.0 + colord: 2.9.3 + memize: 2.1.1 + react: 18.3.1 + '@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.161.6 @@ -25937,6 +26389,8 @@ snapshots: '@wordpress/style-runtime@0.2.0': {} + '@wordpress/style-runtime@0.4.0': {} + '@wordpress/stylelint-config@23.38.0(postcss@8.5.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint-scss@7.0.0(stylelint@17.7.0(typescript@5.9.3)))(stylelint@17.7.0(typescript@5.9.3))': dependencies: '@stylistic/stylelint-plugin': 5.1.0(stylelint@17.7.0(typescript@5.9.3)) @@ -25984,6 +26438,16 @@ snapshots: optionalDependencies: stylelint: 17.7.0(typescript@5.9.3) + '@wordpress/theme@0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@wordpress/element': 8.0.0 + '@wordpress/private-apis': 1.48.0 + '@wordpress/style-runtime': 0.4.0 + colorjs.io: 0.6.1 + memize: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@wordpress/token-list@3.46.0': {} '@wordpress/ui@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -26032,10 +26496,60 @@ snapshots: - '@types/react' - stylelint + '@wordpress/ui@0.15.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/tz': 1.4.1 + '@wordpress/a11y': 4.48.0 + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/element': 8.0.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/icons': 13.3.0(react@18.3.1) + '@wordpress/keycodes': 4.48.0 + '@wordpress/primitives': 4.48.0(react@18.3.1) + '@wordpress/private-apis': 1.48.0 + '@wordpress/style-runtime': 0.4.0 + '@wordpress/theme': 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + date-fns: 4.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@types/react' + - stylelint + + '@wordpress/ui@0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@date-fns/tz': 1.4.1 + '@wordpress/a11y': 4.48.0 + '@wordpress/compose': 8.1.0(react@18.3.1) + '@wordpress/element': 8.0.0 + '@wordpress/i18n': 6.21.0 + '@wordpress/icons': 13.3.0(react@18.3.1) + '@wordpress/keycodes': 4.48.0 + '@wordpress/primitives': 4.48.0(react@18.3.1) + '@wordpress/private-apis': 1.48.0 + '@wordpress/style-runtime': 0.4.0 + '@wordpress/theme': 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + clsx: 2.1.1 + date-fns: 4.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.4.0 + transitivePeerDependencies: + - '@types/react' + - stylelint + '@wordpress/undo-manager@1.46.0': dependencies: '@wordpress/is-shallow-equal': 5.46.0 + '@wordpress/undo-manager@1.48.0': + dependencies: + '@wordpress/is-shallow-equal': 5.48.0 + '@wordpress/upload-media@0.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/blob': 4.46.0 @@ -26126,6 +26640,8 @@ snapshots: '@wordpress/warning@3.46.0': {} + '@wordpress/warning@3.48.0': {} + '@wordpress/widgets@4.46.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/api-fetch': 7.46.0 @@ -27256,6 +27772,8 @@ snapshots: csp_evaluator@1.1.5: {} + css-color-keywords@1.0.0: {} + css-declaration-sorter@7.4.0(postcss@8.5.14): dependencies: postcss: 8.5.14 @@ -27301,6 +27819,12 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + css-tree@2.2.1: dependencies: mdn-data: 2.0.28 diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index dda5855eea18..d735b4e9dcd2 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -63,5 +63,21 @@ export default defineConfig( 'jsdoc/escape-inline-tags': 'off', 'react/jsx-no-bind': 'off', }, + }, + { + // Same as the ui package: soften JSDoc rules for the widgets-toolkit + // port and allow the upstream inline-handler JSX style. Temporary — + // tighten these up in a follow-up alongside the other ports. + files: [ 'packages/widgets-toolkit/**' ], + rules: { + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-description': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/check-indentation': 'off', + 'jsdoc/escape-inline-tags': 'off', + 'react/jsx-no-bind': 'off', + }, } ); diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index e9c1a8260cf5..1d2dd43892d8 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -31,6 +31,7 @@ } }, "dependencies": { + "@automattic/charts": "workspace:*", "@automattic/number-formatters": "workspace:*", "@automattic/ui": "1.0.2", "@date-fns/tz": "1.4.1", @@ -41,11 +42,13 @@ "@wordpress/compose": "7.46.0", "@wordpress/core-data": "7.46.0", "@wordpress/data": "10.46.0", + "@wordpress/dataviews": "16.0.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^13.0.0", "@wordpress/primitives": "4.46.0", "@wordpress/private-apis": "1.46.0", "@wordpress/route": "0.12.0", + "@wordpress/theme": "0.13.0", "@wordpress/ui": "0.13.0", "@wordpress/url": "4.46.0", "clsx": "2.1.1", From b42183f6c980e6709b88e9eb38436bbd01656818 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:32:46 +0800 Subject: [PATCH 27/32] fix(premium-analytics): satisfy widgets-toolkit typecheck and import lint --- .../premium-analytics/eslint.config.mjs | 11 ++++++++++ .../src/components/chart-bar/bar-chart.tsx | 5 ++--- .../comparative-line-chart.tsx | 11 +++++----- .../components/chart-donut/donut-chart.tsx | 15 +++++++------ .../chart-empty-state/chart-empty-state.tsx | 3 +-- .../chart-leaderboard/leaderboard-chart.tsx | 11 +++++----- .../chart-leaderboard/leaderboard-label.tsx | 1 - .../chart-semi-circle/semi-circle-chart.tsx | 21 +++++++++---------- .../chart-tooltip/chart-tooltip.tsx | 11 ++++------ .../chart-tooltip/pie-chart-tooltip.tsx | 8 +++---- .../components/chart-tooltip/tooltip-row.tsx | 3 +-- .../src/components/chart-tooltip/utils.ts | 2 +- .../components/legend/legend-with-theme.tsx | 1 - .../src/components/legend/legend.tsx | 3 +-- .../src/components/legend/row/legend-row.tsx | 2 +- .../components/metric-delta/metric-delta.tsx | 5 ++--- .../components/metric-value/metric-value.tsx | 5 ++--- .../metric-with-comparison.tsx | 5 ++--- .../report-metric/report-metric.tsx | 5 ++--- .../widget-loading-overlay.tsx | 1 - .../src/components/widget-root/context.tsx | 2 +- .../components/widget-root/widget-root.tsx | 9 ++++---- .../date-report-params-field.tsx | 14 ++++++------- .../fields/metrics-field/metrics-field.tsx | 5 ++--- .../src/fields/metrics-field/metrics.ts | 3 +-- .../__tests__/build-coupon-use-data.test.ts | 1 - .../build-sales-by-coupon-data.test.ts | 1 - .../build-bookings-by-attendance-data.ts | 5 ++--- .../src/helpers/build-coupon-use-data.ts | 5 ++--- .../build-new-vs-returning-customer-data.ts | 5 ++--- .../helpers/build-orders-fulfillment-data.ts | 5 ++--- .../src/helpers/build-payment-status-data.ts | 5 ++--- .../build-revenue-by-customer-type-data.ts | 4 ++-- .../src/helpers/build-sales-by-coupon-data.ts | 4 ++-- .../src/helpers/build-sales-by-device-data.ts | 4 ++-- .../src/helpers/build-sales-by-utm-data.ts | 4 ++-- .../helpers/build-sessions-by-device-data.ts | 5 ++--- .../helpers/build-time-series-chart-data.ts | 3 +-- .../src/helpers/build-total-returns-data.ts | 4 ++-- .../build-visitors-by-location-data.ts | 3 +-- .../widgets-toolkit/src/helpers/flag-url.ts | 2 +- .../src/helpers/format-legend-labels.ts | 4 ++-- .../src/helpers/format-orders-metrics.ts | 1 - .../use-attributes-with-search-fallback.ts | 1 - .../src/hooks/use-chart-theme.ts | 2 +- .../src/hooks/use-series-styles.ts | 3 +-- .../src/hooks/use-widget-error.ts | 5 ++--- .../bookings-by-attendance-widget.tsx | 9 ++++---- .../src/widgets/common/use-bar-styles.ts | 4 ++-- .../src/widgets/common/use-segment-styles.ts | 3 +-- .../conversion-rate-widget.tsx | 9 ++++---- .../widgets/coupon-use/coupon-use-widget.tsx | 9 ++++---- .../metric-comparison-widget.tsx | 5 ++--- .../new-vs-returning-customer-widget.tsx | 9 ++++---- .../booking-order-metric-widget.tsx | 3 +-- .../order-metric/widget-order-metric.tsx | 3 +-- .../orders-fulfillment-widget.tsx | 9 ++++---- .../payment-status/payment-status-widget.tsx | 9 ++++---- .../top-performing-bookings-widget.tsx | 3 +-- ...-performing-product-leaderboard-widget.tsx | 9 ++++---- .../top-performing-products-widget.tsx | 2 +- .../revenue-by-customer-type-widget.tsx | 7 +++---- .../sales-by-coupon-widget.tsx | 7 +++---- .../sales-by-device-widget.tsx | 7 +++---- .../sales-by-utm/sales-by-utm-widget.tsx | 7 +++---- .../sessions-by-device-widget.tsx | 11 +++++----- .../total-returns/total-returns-widget.tsx | 7 +++---- .../visitor-metric/widget-visitor-metric.tsx | 1 - .../use-visitors-by-location.ts | 3 +-- .../visitors-by-location-widget.tsx | 13 ++++++------ 70 files changed, 171 insertions(+), 216 deletions(-) diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index d735b4e9dcd2..dc7422c58d66 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -68,6 +68,14 @@ export default defineConfig( // Same as the ui package: soften JSDoc rules for the widgets-toolkit // port and allow the upstream inline-handler JSX style. Temporary — // tighten these up in a follow-up alongside the other ports. + // The port also keeps a few upstream patterns as-is: + // - intentional `any` escapes in test fixtures and the router search + // record (see use-attributes-with-search-fallback.ts) + // - `__experimental*` imports from `@wordpress/components` + // (ToggleGroupControl, Grid) that have no stable equivalents yet + // - CIAB design-system tokens not yet in the local token inventory, + // plus raw/dynamic token names required by the `@automattic/charts` + // theme contract (see use-chart-theme.ts, metric-value.tsx) files: [ 'packages/widgets-toolkit/**' ], rules: { 'jsdoc/require-jsdoc': 'off', @@ -78,6 +86,9 @@ export default defineConfig( 'jsdoc/check-indentation': 'off', 'jsdoc/escape-inline-tags': 'off', 'react/jsx-no-bind': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@wordpress/no-unsafe-wp-apis': 'off', + '@wordpress/no-unknown-ds-tokens': 'off', }, } ); diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx index 4c081526c228..345f84a11a76 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.tsx @@ -5,17 +5,16 @@ import { BarChart as BarChartBase } from '@automattic/charts'; import { Icon } from '@wordpress/ui'; import clsx from 'clsx'; import { useCallback, useMemo, useId } from 'react'; - -import type { ComponentProps } from 'react'; /** * Internal dependencies */ -import type { DataFormat } from '../../types'; import { RESIZE_DEBOUNCE_MS } from '../../constants'; import { isEmptyChartData, getEmptyChartDomain } from '../../helpers'; import { ChartEmptyState } from '../chart-empty-state'; import { ChartTooltip } from '../chart-tooltip'; import styles from './bar-chart.module.scss'; +import type { DataFormat } from '../../types'; +import type { ComponentProps } from 'react'; export type BarChartData = ComponentProps< typeof BarChartBase >[ 'data' ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx index 56044df77c2e..b65f1f95780c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx @@ -1,22 +1,21 @@ /** * External dependencies */ -import { useCallback, useMemo } from 'react'; import { LineChart } from '@automattic/charts'; import { formatDate, formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import { type ComponentProps } from 'react'; import clsx from 'clsx'; - +import { useCallback, useMemo } from 'react'; +import { type ComponentProps } from 'react'; /** * Internal dependencies */ -import { ChartTooltip } from '../chart-tooltip'; -import { alignSeriesDates } from './utils'; import { RESIZE_DEBOUNCE_MS } from '../../constants'; import { isEmptyChartData, getEmptyChartDomain } from '../../helpers'; +import { ChartTooltip } from '../chart-tooltip'; +import styles from './comparative-line-chart.module.scss'; +import { alignSeriesDates } from './utils'; import type { ComparativeLineChartSeries, SeriesStyle } from './types'; import type { DataFormat } from '../../types'; -import styles from './comparative-line-chart.module.scss'; /** * Resolves series styles from either the explicit styles prop or series options. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx index e12434cafdc4..be0a260ef93c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-donut/donut-chart.tsx @@ -2,19 +2,12 @@ * External dependencies */ import { PieChartUnresponsive as PieChart } from '@automattic/charts'; -import { Icon, Stack } from '@wordpress/ui'; import { useResizeObserver } from '@wordpress/compose'; +import { Icon, Stack } from '@wordpress/ui'; import { useMemo, useState } from 'react'; - -import type { ComponentProps } from 'react'; /** * Internal dependencies */ -import { PieChartTooltip } from '../chart-tooltip'; -import { MetricWithComparison } from '../metric-with-comparison'; -import { Legend as LegendPure } from '../legend/legend'; -import type { LegendItem } from '../legend/legend'; -import type { DataFormat } from '../../types'; import { resolveSegmentStyles, applyStylesToItems, @@ -22,7 +15,13 @@ import { type SegmentStyle, } from '../../helpers'; import { ChartEmptyState } from '../chart-empty-state'; +import { PieChartTooltip } from '../chart-tooltip'; +import { Legend as LegendPure } from '../legend/legend'; +import { MetricWithComparison } from '../metric-with-comparison'; import styles from './donut-chart.module.scss'; +import type { DataFormat } from '../../types'; +import type { LegendItem } from '../legend/legend'; +import type { ComponentProps } from 'react'; // Default chart configuration const DEFAULT_THICKNESS = 0.3; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx index 9fe852dc7f5e..3abe51d04d43 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.tsx @@ -1,10 +1,9 @@ /** * External dependencies */ -import { EmptyState, Icon } from '@wordpress/ui'; import { __ } from '@wordpress/i18n'; import { cautionFilled } from '@wordpress/icons'; - +import { EmptyState, Icon } from '@wordpress/ui'; /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx index 6edecdcc802e..099a44db2d0d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx @@ -1,25 +1,24 @@ /** * External dependencies */ -import { Icon, Stack } from '@wordpress/ui'; import { LeaderboardChartUnresponsive as BaseLeaderboardChart, useGlobalChartsContext, Legend, hexToRgba, } from '@automattic/charts'; +import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; +import { Icon, Stack } from '@wordpress/ui'; import clsx from 'clsx'; -import type { ComponentProps, ReactNode } from 'react'; import { useMemo } from 'react'; -import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; - /** * Internal dependencies */ -import type { DataFormat } from '../../types'; -import type { WooChartTheme } from '../../hooks/use-chart-theme'; import { ChartEmptyState } from '../chart-empty-state'; import styles from './leaderboard-chart.module.scss'; +import type { WooChartTheme } from '../../hooks/use-chart-theme'; +import type { DataFormat } from '../../types'; +import type { ComponentProps, ReactNode } from 'react'; type LeaderboardChartData = ComponentProps< typeof BaseLeaderboardChart >[ 'data' ]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx index 9d70da3192f5..e8bdad3d41cb 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.tsx @@ -3,7 +3,6 @@ */ import { Stack } from '@wordpress/ui'; import clsx from 'clsx'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx index 6f639b6e8110..c27a2fd23f4d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-semi-circle/semi-circle-chart.tsx @@ -4,16 +4,7 @@ import { PieSemiCircleChart } from '@automattic/charts'; import { Icon, Stack } from '@wordpress/ui'; import { useMemo } from 'react'; - -import type { ComponentProps } from 'react'; -/** - * Internal dependencies - */ -import { PieChartTooltip } from '../chart-tooltip'; -import { MetricWithComparison } from '../metric-with-comparison'; -import { Legend as LegendPure } from '../legend/legend'; -import type { LegendItem } from '../legend/legend'; -import type { DataFormat } from '../../types'; +import { RESIZE_DEBOUNCE_MS } from '../../constants'; import { resolveSegmentStyles, applyStylesToItems, @@ -21,8 +12,16 @@ import { type SegmentStyle, } from '../../helpers'; import { ChartEmptyState } from '../chart-empty-state'; +import { PieChartTooltip } from '../chart-tooltip'; +/** + * Internal dependencies + */ +import { Legend as LegendPure } from '../legend/legend'; +import { MetricWithComparison } from '../metric-with-comparison'; import styles from './semi-circle-chart.module.scss'; -import { RESIZE_DEBOUNCE_MS } from '../../constants'; +import type { DataFormat } from '../../types'; +import type { LegendItem } from '../legend/legend'; +import type { ComponentProps } from 'react'; // Default chart configuration const DEFAULT_THICKNESS = 0.3; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx index 2bc3566f432f..27b973b85191 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { Stack } from '@wordpress/ui'; import { LineShape, RectShape } from '@automattic/charts/visx/legend'; - +import { Stack } from '@wordpress/ui'; /** * Internal dependencies */ -import type { DataFormat } from '../../types'; -import { isChartDatumEntry } from './utils'; -import { TooltipRow } from './tooltip-row'; import styles from './chart-tooltip.module.scss'; +import { TooltipRow } from './tooltip-row'; +import { isChartDatumEntry } from './utils'; +import type { DataFormat } from '../../types'; /** * Style configuration for tooltip indicators. @@ -44,7 +43,6 @@ type DatumWithValue = { value: number }; * @param datum - The data point */ function defaultGetLabel( datum: unknown ): string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic datum narrowed by convention return ( datum as DatumWithLabel ).label ?? ''; } @@ -52,7 +50,6 @@ function defaultGetLabel( datum: unknown ): string { * Default value extractor - assumes datum has a 'value' property. */ function defaultGetValue( datum: unknown ): number { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic datum narrowed by convention return ( datum as DatumWithValue ).value; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx index 31a194156181..c8a57451139c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/pie-chart-tooltip.tsx @@ -1,16 +1,16 @@ /** * External dependencies */ +import { RectShape } from '@automattic/charts/visx/legend'; import { Stack } from '@wordpress/ui'; +import styles from './chart-tooltip.module.scss'; +import { TooltipRow } from './tooltip-row'; +import type { DataFormat } from '../../types'; import type { DataPointPercentage } from '@automattic/charts'; -import { RectShape } from '@automattic/charts/visx/legend'; /** * Internal dependencies */ -import type { DataFormat } from '../../types'; -import { TooltipRow } from './tooltip-row'; -import styles from './chart-tooltip.module.scss'; export type PieChartTooltipProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx index 6c134546c375..624387ee46c3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/tooltip-row.tsx @@ -2,13 +2,12 @@ * External dependencies */ import { Stack } from '@wordpress/ui'; - /** * Internal dependencies */ -import type { DataFormat } from '../../types'; import { MetricValue } from '../metric-value'; import styles from './chart-tooltip.module.scss'; +import type { DataFormat } from '../../types'; export type TooltipRowProps = { /** Pre-rendered indicator element (LineShape, RectShape, etc.) */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts index f4b59cae6338..6f55b73c2f39 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/utils.ts @@ -11,7 +11,7 @@ export type ChartDatumEntry< T = unknown > = { /** * Type guard to check if an entry is a valid chart datum entry. * - * @param entry The entry to check. + * @param entry - The entry to check. * @return True if the entry has the expected structure. */ export const isChartDatumEntry = < T >( entry: unknown ): entry is ChartDatumEntry< T > => { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx index df56c2229486..92d58b474e33 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend-with-theme.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { type BaseLegendItem, useGlobalChartsContext } from '@automattic/charts'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx index 79fe63740322..3121846c256c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/legend.tsx @@ -2,13 +2,12 @@ * External dependencies */ import { __experimentalGrid as Grid } from '@wordpress/components'; - /** * Internal dependencies */ -import { LegendRow } from './row'; import { MetricDelta } from '../metric-delta'; import styles from './legend.module.scss'; +import { LegendRow } from './row'; export type LegendItem = { label: string; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx index a7f8d7e65bb2..1f8de76e005a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/legend/row/legend-row.tsx @@ -2,12 +2,12 @@ * External dependencies */ import { Stack } from '@wordpress/ui'; +import styles from '../legend.module.scss'; import type { ReactNode } from 'react'; /** * Internal dependencies */ -import styles from '../legend.module.scss'; export type LegendRowProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx index 6fa96f4a9403..cf3974e7b52c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.tsx @@ -1,15 +1,14 @@ /** * External dependencies */ -import { Stack } from '@wordpress/ui'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ComponentProps } from 'react'; +import { Stack } from '@wordpress/ui'; import clsx from 'clsx'; - /** * Internal dependencies */ import styles from './metric-delta.module.scss'; +import type { ComponentProps } from 'react'; export type MetricDeltaProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx index cfb58608e10b..985f26ff6032 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { type CSSProperties, useMemo } from 'react'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { FontSize } from '@wordpress/theme'; import clsx from 'clsx'; - +import { type CSSProperties, useMemo } from 'react'; /** * Internal dependencies */ import styles from './metric-value.module.scss'; import type { DataFormat } from '../../types'; +import type { FontSize } from '@wordpress/theme'; export type MetricValueProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx index c233b0424efa..648690364706 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-with-comparison/metric-with-comparison.tsx @@ -3,14 +3,13 @@ */ import { Stack } from '@wordpress/ui'; import { ComponentProps } from 'react'; - /** * Internal dependencies */ -import { MetricValue } from '../metric-value'; import { MetricDelta } from '../metric-delta'; -import type { MetricValueProps } from '../metric-value'; +import { MetricValue } from '../metric-value'; import type { DataFormat } from '../../types'; +import type { MetricValueProps } from '../metric-value'; export type MetricWithComparisonProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx index f49ba65c1c7e..82ab16e08b09 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-metric/report-metric.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { WidgetLoadingOverlay } from '../widget-loading-overlay'; import { useGlobalChartsContext } from '@automattic/charts'; import { useMemo } from 'react'; - /** * Internal dependencies */ -import { MetricComparisonWidget } from '../../widgets/metric-comparison'; import { buildTimeSeriesChartData } from '../../helpers'; import { useWidgetError } from '../../hooks'; +import { MetricComparisonWidget } from '../../widgets/metric-comparison'; +import { WidgetLoadingOverlay } from '../widget-loading-overlay'; import type { DataFormat } from '../../types'; /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx index 0a3d6d9545d5..914183478196 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.tsx @@ -3,7 +3,6 @@ */ import { Spinner } from '@wordpress/components'; import { Stack } from '@wordpress/ui'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx index ebe151f8d80c..a27238ad1a72 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/context.tsx @@ -2,8 +2,8 @@ * External dependencies */ import { createContext, useContext } from 'react'; -import type { ReportParams } from '@jetpack-premium-analytics/data'; import type { WidgetErrorConfig } from '../../types'; +import type { ReportParams } from '@jetpack-premium-analytics/data'; export type WidgetRootContextValue = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx index b51eaf7902e5..a3cc938644ad 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.tsx @@ -1,25 +1,24 @@ /** * External dependencies */ +import { GlobalChartsProvider } from '@automattic/charts'; import { AnalyticsQueryClientProvider, getDefaultPreset, normalizeReportParams, } from '@jetpack-premium-analytics/data'; -import { getStoreInfo } from '../../helpers/store-info'; -import { GlobalChartsProvider } from '@automattic/charts'; import { useSearch } from '@wordpress/route'; import { useMemo, type ReactNode } from 'react'; -import type { WidgetErrorConfig } from '../../types'; +import { getStoreInfo } from '../../helpers/store-info'; import '@automattic/charts/style.css'; - /** * Internal dependencies */ import { useChartTheme } from '../../hooks'; import { WidgetRootContext } from './context'; -import type { ReportParamsFieldAttributes } from '../../fields'; import styles from './widget-root.module.scss'; +import type { ReportParamsFieldAttributes } from '../../fields'; +import type { WidgetErrorConfig } from '../../types'; type WidgetRootProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx index adb3cbb860cf..2cc63fad2139 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/date-report-params-field/date-report-params-field.tsx @@ -1,24 +1,24 @@ /** * External dependencies */ -import { Stack } from '@wordpress/ui'; -import { DateFiltersPanel } from '@jetpack-premium-analytics/ui'; import { getDefaultPreset, normalizeReportParams, localTZDate, getSiteTimezone, } from '@jetpack-premium-analytics/data'; -import { getStoreInfo } from '../../helpers/store-info'; -import { endOfDay } from 'date-fns'; -import { deriveComparisonRange, encodeDateToSearchParam } from '@jetpack-premium-analytics/routing'; -import { useCallback, useMemo, useState, useEffect } from 'react'; -import type { DataFormControlProps } from '@wordpress/dataviews'; import { type ComparisonPresetId, isPrimaryPreset, type DateRange, } from '@jetpack-premium-analytics/datetime'; +import { deriveComparisonRange, encodeDateToSearchParam } from '@jetpack-premium-analytics/routing'; +import { DateFiltersPanel } from '@jetpack-premium-analytics/ui'; +import { Stack } from '@wordpress/ui'; +import { endOfDay } from 'date-fns'; +import { useCallback, useMemo, useState, useEffect } from 'react'; +import { getStoreInfo } from '../../helpers/store-info'; +import type { DataFormControlProps } from '@wordpress/dataviews'; /** * Inferred types diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx index 52c683e111b5..97dd1126a26b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics-field.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { Fieldset, Stack } from '@wordpress/ui'; import { CheckboxControl } from '@wordpress/components'; import { __, _n, sprintf } from '@wordpress/i18n'; +import { Fieldset, Stack } from '@wordpress/ui'; import { useCallback, useEffect } from 'react'; -import type { DataFormControlProps } from '@wordpress/dataviews'; - /** * Internal dependencies */ import { DEFAULT_METRICS, type Metric } from './metrics'; +import type { DataFormControlProps } from '@wordpress/dataviews'; type MetricsAttributes = { metrics: Metric[]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts index c9346317b76c..0988d96b7e0f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/fields/metrics-field/metrics.ts @@ -1,9 +1,8 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { FilterCondition } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts index bd1242234398..075a2a317dde 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-coupon-use-data.test.ts @@ -4,7 +4,6 @@ jest.mock( '@jetpack-premium-analytics/formatters', () => ( { formatMetricValue: ( value: number ) => `$${ value }`, } ) ); - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts index 82171b16660f..1b1a595bd1db 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/__tests__/build-sales-by-coupon-data.test.ts @@ -4,7 +4,6 @@ jest.mock( '@jetpack-premium-analytics/formatters', () => ( { formatDateRange: () => 'Jan 1 – 31, 2024', } ) ); - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts index 0a0e31d15f04..0579a7e8bbad 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-bookings-by-attendance-data.ts @@ -1,15 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ReportDataMap } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { LegendItem } from '../components'; import type { DonutChartData } from '../components/chart-donut/donut-chart'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; // Color for cancelled status const CANCELLED_COLOR = 'rgb(240, 240, 240)'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts index d34d8865a14d..eb1ecdc42d55 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-coupon-use-data.ts @@ -1,15 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ReportDataMap } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { LegendItem } from '../components'; import type { DonutChartData } from '../components/chart-donut/donut-chart'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; export interface CouponUseData { chartData: DonutChartData; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts index c14a9b19e5e1..3647c61300b2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-new-vs-returning-customer-data.ts @@ -1,15 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ReportDataMap } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { LegendItem } from '../components'; import type { DonutChartData } from '../components/chart-donut/donut-chart'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; export interface NewVsReturningCustomerData { chartData: DonutChartData; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts index 954a0900233d..87e38ca5908f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-orders-fulfillment-data.ts @@ -1,15 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ReportDataMap } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { LegendItem } from '../components'; import type { DonutChartData } from '../components/chart-donut/donut-chart'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; export interface OrdersFulfillmentData { chartData: DonutChartData; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts index d0d9feac37f3..9b4e1878a005 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-payment-status-data.ts @@ -1,15 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ReportDataMap } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { LegendItem } from '../components'; import type { DonutChartData } from '../components/chart-donut/donut-chart'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; export interface PaymentStatusData { chartData: DonutChartData; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts index b37202af69ee..3de41d93e279 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-revenue-by-customer-type-data.ts @@ -2,13 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; +import { formatLegendLabels } from './format-legend-labels'; import type { SeriesData } from '@automattic/charts'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; /** * Internal dependencies */ -import { formatLegendLabels } from './format-legend-labels'; export interface RevenueByCustomerTypeData { chartData: SeriesData[]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts index 169ca630f6f1..eb595ca2f305 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-coupon-data.ts @@ -2,13 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; +import { formatLegendLabels } from './format-legend-labels'; import type { SeriesData } from '@automattic/charts'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; /** * Internal dependencies */ -import { formatLegendLabels } from './format-legend-labels'; export interface SalesByCouponData { chartData: SeriesData[]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts index 551b3609d89a..b37056adc5bf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-device-data.ts @@ -2,13 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; +import { formatLegendLabels } from './format-legend-labels'; import type { SeriesData } from '@automattic/charts'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; /** * Internal dependencies */ -import { formatLegendLabels } from './format-legend-labels'; export interface SalesByDeviceData { chartData: SeriesData[]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts index 5aa5a44247c2..b55b32de46d5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sales-by-utm-data.ts @@ -2,13 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { calculateDelta } from './calculate-delta'; +import type { LeaderboardChartData } from '../components/chart-leaderboard'; import type { ReportDataMap } from '@jetpack-premium-analytics/data'; /** * Internal dependencies */ -import type { LeaderboardChartData } from '../components/chart-leaderboard'; -import { calculateDelta } from './calculate-delta'; /** * Builds leaderboard chart data for the Sales by UTM widget. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts index 2f579588c5e1..4973191065a9 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-sessions-by-device-data.ts @@ -1,15 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; -import type { ReportDataMap } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { LegendItem } from '../components'; import type { SemiCircleChartData } from '../components/chart-semi-circle/semi-circle-chart'; +import type { ReportDataMap } from '@jetpack-premium-analytics/data'; export interface SessionsByDeviceData { chartData: SemiCircleChartData; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts index 57dfa4edf666..b18c4c04eb55 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-time-series-chart-data.ts @@ -1,10 +1,9 @@ /** * External dependencies */ -import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { localTZDate } from '@jetpack-premium-analytics/data'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts index cc02fd4fb339..e4937bb25b4c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-total-returns-data.ts @@ -2,13 +2,13 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; +import { formatLegendLabels } from './format-legend-labels'; import type { SeriesData } from '@automattic/charts'; +import type { ReportDataMap, ReportParams } from '@jetpack-premium-analytics/data'; /** * Internal dependencies */ -import { formatLegendLabels } from './format-legend-labels'; export interface TotalReturnsData { chartData: SeriesData[]; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts index 8ac0ce709502..068271bdc6ec 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/build-visitors-by-location-data.ts @@ -1,13 +1,12 @@ /** * External dependencies */ -import type { GeoData } from '@automattic/charts'; import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ import type { LeaderboardChartData } from '../components/chart-leaderboard/leaderboard-chart'; +import type { GeoData } from '@automattic/charts'; export type Region = 'US' | 'world'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts index 3d755706c555..4c6744d7cf59 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/flag-url.ts @@ -1,6 +1,6 @@ /** * Given a country code, return a flag SVG URL from CDN. - * @param countryCode A two-letter ISO 3166-1 country code (lowercase) + * @param countryCode - A two-letter ISO 3166-1 country code (lowercase) * @return Flag SVG URL */ export function flagUrl( countryCode: string ): string | null { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts index 77552a7defd3..cdd97c486a8f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-legend-labels.ts @@ -1,14 +1,14 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; import { formatDateRange } from '@jetpack-premium-analytics/formatters'; +import { __ } from '@wordpress/i18n'; +import type { LegendLabels } from '../components/chart-leaderboard'; import type { ReportParams } from '@jetpack-premium-analytics/data'; /** * Internal dependencies */ -import type { LegendLabels } from '../components/chart-leaderboard'; /** * Formats legend labels from report parameters. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts index 2d4685e5a00f..ac6be5ed936d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/helpers/format-orders-metrics.ts @@ -2,7 +2,6 @@ * External dependencies */ import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts index 6152bcfd39bf..6ce75cf2295f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-attributes-with-search-fallback.ts @@ -2,7 +2,6 @@ * External dependencies */ import { useSearch } from '@wordpress/route'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts index e01f342c65a3..c0606621c774 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts @@ -2,13 +2,13 @@ * External dependencies */ import { useMemo } from 'react'; +import { WOO_COLORS } from '../constants'; import { useColorPreference } from './use-color-preference'; import type { ChartTheme } from '@automattic/charts'; /** * Internal dependencies */ -import { WOO_COLORS } from '../constants'; /** * Extended chart theme with WooCommerce-specific properties. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts index 427efb2b2b7b..6a024c2cae09 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-series-styles.ts @@ -1,9 +1,8 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { useGlobalChartsContext } from '@automattic/charts'; - +import { useMemo } from 'react'; /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts index 8819f19f605b..66d82ede71ba 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-widget-error.ts @@ -1,10 +1,9 @@ /** * External dependencies */ -import { useEffect } from 'react'; -import { __ } from '@wordpress/i18n'; import { useGlobalError } from '@jetpack-premium-analytics/data'; - +import { __ } from '@wordpress/i18n'; +import { useEffect } from 'react'; /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx index 8a514fa32d30..809bcfbd0237 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/bookings-by-attendance/bookings-by-attendance-widget.tsx @@ -1,17 +1,16 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportBookings } from '@jetpack-premium-analytics/data'; import { calendar } from '@jetpack-premium-analytics/icons'; - +import { Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; +import { DonutChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { DonutChart } from '../../components'; import { buildBookingsByAttendanceData } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useSegmentStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts index b5be75706600..cdf0fa4eda9b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-bar-styles.ts @@ -1,14 +1,14 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { useGlobalChartsContext } from '@automattic/charts'; +import { useMemo } from 'react'; +import type { BarChartStyle } from '../../components'; import type { SeriesData } from '@automattic/charts'; /** * Internal dependencies */ -import type { BarChartStyle } from '../../components'; /** * Hook to build bar chart styles from theme. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts index f876a5207a71..159b65a6603f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/common/use-segment-styles.ts @@ -1,9 +1,8 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { useGlobalChartsContext } from '@automattic/charts'; - +import { useMemo } from 'react'; /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx index 74e1bec78993..41bcdbfaa786 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.tsx @@ -1,18 +1,17 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { ConversionFunnelChart } from '@automattic/charts'; -import { Icon, Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { FilterCondition, useReportConversionRate } from '@jetpack-premium-analytics/data'; import { goal } from '@jetpack-premium-analytics/icons'; - +import { Icon, Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; /** * Internal dependencies */ -import { useWidgetRootContext } from '../../components/widget-root'; import { MetricWithComparison, ChartEmptyState } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; +import { useWidgetRootContext } from '../../components/widget-root'; import { BOOKINGS_FILTER } from '../../helpers'; import { useWidgetError } from '../../hooks'; import styles from './conversion-rate-widget.module.scss'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx index 0dcba5224c29..d2d333fc9e7f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/coupon-use/coupon-use-widget.tsx @@ -1,17 +1,16 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCouponsByDate } from '@jetpack-premium-analytics/data'; import { coupon } from '@jetpack-premium-analytics/icons'; - +import { Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; +import { DonutChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { DonutChart } from '../../components'; import { buildCouponUseData } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useSegmentStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx index 042cebaf6741..c48fdc2158bf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/metric-comparison/metric-comparison-widget.tsx @@ -2,17 +2,16 @@ * External dependencies */ import { Stack } from '@wordpress/ui'; - /** * Internal dependencies */ import { MetricWithComparison, ComparativeLineChart } from '../../components'; -import type { DataFormat } from '../../types'; +import styles from './metric-comparison-widget.module.scss'; import type { ComparativeLineChartSeries, SeriesStyle, } from '../../components/chart-comparative-line/types'; -import styles from './metric-comparison-widget.module.scss'; +import type { DataFormat } from '../../types'; export type MetricComparisonWidgetProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx index b68e860277eb..83ede41bdf82 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/new-vs-returning-customer/new-vs-returning-customer-widget.tsx @@ -1,17 +1,16 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCustomersByDate } from '@jetpack-premium-analytics/data'; import { customer } from '@jetpack-premium-analytics/icons'; - +import { Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; +import { DonutChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { DonutChart } from '../../components'; import { buildNewVsReturningCustomerData } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useSegmentStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx index 7238ee979cd1..f0049a46254a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/booking-order-metric-widget.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { useReportOrders } from '@jetpack-premium-analytics/data'; - /** * Internal dependencies */ @@ -28,7 +27,7 @@ export type BookingOrderMetricWidgetProps = { * This component must be used within a WidgetRoot which provides reportParams * via context. * - * @param {Object} props - Component props + * @param {object} props - Component props * @param {OrderMetricKey} props.metricKey - The metric key to display * * @example diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx index 9e5373783d08..ed67900c19bc 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/order-metric/widget-order-metric.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { useReportOrders } from '@jetpack-premium-analytics/data'; - /** * Internal dependencies */ @@ -25,7 +24,7 @@ export type OrderMetricWidgetProps = { * This component must be used within a WidgetRoot which provides reportParams * via context. * - * @param {Object} props - Component props + * @param {object} props - Component props * @param {OrderMetricKey} props.metricKey - The metric key to display * * @example diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx index b233deb37177..d08eb01a72df 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/orders-fulfillment/orders-fulfillment-widget.tsx @@ -1,17 +1,16 @@ /** * External dependencies */ -import { useMemo, useCallback } from 'react'; -import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrders } from '@jetpack-premium-analytics/data'; import { reports } from '@jetpack-premium-analytics/icons'; - +import { Stack } from '@wordpress/ui'; +import { useMemo, useCallback } from 'react'; +import { DonutChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { DonutChart } from '../../components'; import { buildOrdersFulfillmentData, FULFILLED_ORDERS_FILTER, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx index f76cdb42d095..c0ac049cc06a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/payment-status/payment-status-widget.tsx @@ -1,17 +1,16 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrders } from '@jetpack-premium-analytics/data'; import { payment } from '@jetpack-premium-analytics/icons'; - +import { Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; +import { DonutChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { DonutChart } from '../../components'; import { buildPaymentStatusData, PAYMENT_STATUS_FILTERS } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useSegmentStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx index 832f00237d9d..086568a222ed 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-bookings-widget.tsx @@ -2,12 +2,11 @@ * External dependencies */ import { calendar } from '@jetpack-premium-analytics/icons'; - /** * Internal dependencies */ -import { TopPerformingProductLeaderboardWidget } from './top-performing-product-leaderboard-widget'; import { BOOKINGS_FILTER } from '../../helpers'; +import { TopPerformingProductLeaderboardWidget } from './top-performing-product-leaderboard-widget'; export type TopPerformingBookingsWidgetProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx index 546c8d781d91..622b201e0080 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-product-leaderboard-widget.tsx @@ -1,21 +1,20 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportProducts, useProductImages, type FilterCondition, } from '@jetpack-premium-analytics/data'; -import { Icon } from '@wordpress/ui'; import { productBlouse } from '@jetpack-premium-analytics/icons'; - +import { Icon } from '@wordpress/ui'; +import { useMemo } from 'react'; +import { LeaderboardChart, LeaderboardLabel } from '../../components/chart-leaderboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { LeaderboardChart, LeaderboardLabel } from '../../components/chart-leaderboard'; import { formatLegendLabels, calculateDelta } from '../../helpers'; import { useWidgetError } from '../../hooks'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx index d245dcdc2806..1ccaa05741e3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/product-leaderboard/top-performing-products-widget.tsx @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import { TopPerformingProductLeaderboardWidget } from './top-performing-product-leaderboard-widget'; import { PHYSICAL_PRODUCTS_FILTER } from '../../helpers'; +import { TopPerformingProductLeaderboardWidget } from './top-performing-product-leaderboard-widget'; export type TopPerformingProductsWidgetProps = { /** diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx index 605ae530a257..ece1fe954957 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/revenue-by-customer-type/revenue-by-customer-type-widget.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCustomers, type FilterCondition } from '@jetpack-premium-analytics/data'; import { customer } from '@jetpack-premium-analytics/icons'; - +import { useMemo } from 'react'; +import { BarChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { BarChart } from '../../components'; import { buildRevenueByCustomerTypeData, BOOKINGS_FILTER } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useBarStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx index c02ddd00911e..66f7572af684 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-coupon/sales-by-coupon-widget.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportCoupons } from '@jetpack-premium-analytics/data'; import { coupon } from '@jetpack-premium-analytics/icons'; - +import { useMemo } from 'react'; +import { BarChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { BarChart } from '../../components'; import { buildSalesByCouponData } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useBarStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx index 5a8321c571c1..6bffa2e8cfa2 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-device/sales-by-device-widget.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrderAttribution, type FilterCondition } from '@jetpack-premium-analytics/data'; import { device } from '@jetpack-premium-analytics/icons'; - +import { useMemo } from 'react'; +import { BarChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { BarChart } from '../../components'; import { buildSalesByDeviceData, BOOKINGS_FILTER } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useBarStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx index f29885af81c1..eecabb0cad4b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sales-by-utm/sales-by-utm-widget.tsx @@ -1,19 +1,18 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrderAttribution, ORDER_ATTRIBUTION_VIEWS, } from '@jetpack-premium-analytics/data'; import { megaphone, search, channel } from '@jetpack-premium-analytics/icons'; - +import { useMemo } from 'react'; +import { LeaderboardChart } from '../../components/chart-leaderboard'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { LeaderboardChart } from '../../components/chart-leaderboard'; import { buildSalesByUtmData, formatLegendLabels } from '../../helpers'; import { useWidgetError } from '../../hooks'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx index 26774bf98bfc..90ac045400c3 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/sessions-by-device/sessions-by-device-widget.tsx @@ -1,19 +1,18 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { Stack } from '@wordpress/ui'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportSessionsByDevice } from '@jetpack-premium-analytics/data'; import { device } from '@jetpack-premium-analytics/icons'; - +import { Stack } from '@wordpress/ui'; +import { useMemo } from 'react'; +import { SemiCircleChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { useWidgetError } from '../../hooks'; -import { SemiCircleChart } from '../../components'; import { buildSessionsByDeviceData } from '../../helpers'; +import { useWidgetError } from '../../hooks'; import { useSegmentStyles } from '../common'; import styles from './sessions-by-device-widget.module.scss'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx index 98da26cb9f48..61b3f5fcb3df 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/total-returns/total-returns-widget.tsx @@ -1,16 +1,15 @@ /** * External dependencies */ -import { useMemo } from 'react'; -import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; import { useReportOrders } from '@jetpack-premium-analytics/data'; import { paymentReturn } from '@jetpack-premium-analytics/icons'; - +import { useMemo } from 'react'; +import { BarChart } from '../../components'; +import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; -import { BarChart } from '../../components'; import { buildTotalReturnsData } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useBarStyles } from '../common'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx index 64f52c36e1e2..84fbcee4627d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitor-metric/widget-visitor-metric.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { useReportVisitors } from '@jetpack-premium-analytics/data'; - /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts index 27d70820abfe..75536863c30a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/use-visitors-by-location.ts @@ -1,9 +1,8 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { type ReportParams, useReportVisitorsByLocation } from '@jetpack-premium-analytics/data'; - +import { useMemo } from 'react'; /** * Internal dependencies */ diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx index fa6ee6f3b15b..b8cee6439f50 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.tsx @@ -1,6 +1,8 @@ /** * External dependencies */ +import { GeoChart } from '@automattic/charts'; +import { location } from '@jetpack-premium-analytics/icons'; import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, @@ -9,21 +11,18 @@ import { import { useResizeObserver } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { GeoChart } from '@automattic/charts'; +import { ChartEmptyState } from '../../components'; +import { LeaderboardChart, LeaderboardLabel } from '../../components/chart-leaderboard'; import { WidgetLoadingOverlay } from '../../components/widget-loading-overlay'; -import { location } from '@jetpack-premium-analytics/icons'; - /** * Internal dependencies */ import { useWidgetRootContext } from '../../components/widget-root'; +import { RESIZE_DEBOUNCE_MS } from '../../constants'; +import { flagUrl } from '../../helpers'; import { useWidgetError } from '../../hooks'; import { useVisitorsByLocation, type Region } from './use-visitors-by-location'; -import { LeaderboardChart, LeaderboardLabel } from '../../components/chart-leaderboard'; -import { flagUrl } from '../../helpers'; import styles from './visitors-by-location-widget.module.scss'; -import { ChartEmptyState } from '../../components'; -import { RESIZE_DEBOUNCE_MS } from '../../constants'; function isRegion( value: unknown ): value is Region { return value === 'US' || value === 'world'; From c19f2824867660ead4aaa42a5bacfd70dc655a42 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:37:30 +0800 Subject: [PATCH 28/32] fix(premium-analytics): rename CIAB design tokens to canonical wpds typography tokens --- .../src/components/chart-bar/bar-chart.module.scss | 4 ++-- .../chart-comparative-line/comparative-line-chart.module.scss | 4 ++-- .../chart-leaderboard/leaderboard-label.module.scss | 2 +- .../src/components/chart-tooltip/chart-tooltip.module.scss | 2 +- .../src/components/metric-delta/metric-delta.module.scss | 4 ++-- .../src/components/metric-value/metric-value.module.scss | 2 +- .../src/components/metric-value/metric-value.tsx | 4 ++-- .../packages/widgets-toolkit/src/hooks/use-chart-theme.ts | 4 ++-- .../conversion-rate/conversion-rate-widget.module.scss | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss index 6202e0fad9b3..9a9f6722755f 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss @@ -8,8 +8,8 @@ } .legend { - height: var( --wpds-font-line-height-lg ); - min-height: var( --wpds-font-line-height-lg ); + height: var( --wpds-typography-line-height-lg ); + min-height: var( --wpds-typography-line-height-lg ); flex-wrap: nowrap; // Vertically center the legend shape. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss index 2dfe1093d311..5c7b043d9d92 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss @@ -3,8 +3,8 @@ .legend { flex: 0 0 auto; - height: var( --wpds-font-line-height-lg ); - min-height: var( --wpds-font-line-height-lg ); + height: var( --wpds-typography-line-height-lg ); + min-height: var( --wpds-typography-line-height-lg ); flex-wrap: nowrap; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss index 61b2c1c5a20e..35b64c85c83e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss @@ -3,7 +3,7 @@ } .label { - font-size: var( --wpds-font-size-sm ); + font-size: var( --wpds-typography-font-size-sm ); } .labelImage { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss index 133e474b7e81..eeecdcf4cde5 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss @@ -8,7 +8,7 @@ .item { font-weight: 400; - line-height: var( --wpds-font-line-height-xs ); + line-height: var( --wpds-typography-line-height-xs ); } .label { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss index d31aff2462ef..9d41417fe3ab 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss @@ -1,7 +1,7 @@ .delta { - font-size: var( --wpds-font-size-md ); + font-size: var( --wpds-typography-font-size-md ); font-weight: 400; - line-height: var( --wpds-font-size-lg ); + line-height: var( --wpds-typography-font-size-lg ); &.invalid, &.neutral { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss index 890273810e87..a23979f919ce 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss @@ -1,7 +1,7 @@ .metricValue { font-weight: 500; font-size: var( --wp-ui-metric-font-size ); - line-height: var( --wpds-font-line-height-sm ); + line-height: var( --wpds-typography-line-height-sm ); // Color variants &.color--neutral { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx index 985f26ff6032..bc79f5685077 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.tsx @@ -35,7 +35,7 @@ export type MetricValueProps = { /** * Font size token from the WordPress Design System. - * Maps directly to `--wpds-font-size-{value}`. + * Maps directly to `--wpds-typography-font-size-{value}`. * @default 'lg' */ fontSize?: FontSize; @@ -68,7 +68,7 @@ export function MetricValue( { ); const style = { - '--wp-ui-metric-font-size': `var( --wpds-font-size-${ fontSize } )`, + '--wp-ui-metric-font-size': `var( --wpds-typography-font-size-${ fontSize } )`, } as CSSProperties; return ( diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts index c0606621c774..b0e60a135051 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-chart-theme.ts @@ -33,7 +33,7 @@ export function useChartTheme(): WooChartTheme { return { backgroundColor: 'var(--wpds-color-bg-surface-neutral-strong)', - labelBackgroundColor: 'var(--wpds-color-bg-interactive-neutral)', + labelBackgroundColor: 'var(--wpds-color-bg-interactive-neutral-weak)', labelTextColor: 'var(--wpds-color-fg-interactive-neutral-strong)', colors, gridStyles: { @@ -53,7 +53,7 @@ export function useChartTheme(): WooChartTheme { }, legend: { labelStyles: { - fontSize: 'var(--wpds-font-size-sm)', + fontSize: 'var(--wpds-typography-font-size-sm)', fontWeight: 400, color: 'var(--wpds-color-fg-content-neutral)', }, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss index 2aa79ef412ea..e3a59abfe097 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss @@ -4,8 +4,8 @@ } .conversionFunnelChart { - --funnel-font-family: var( --wpds-font-family-body ); - --step-font-family: var( --wpds-font-family-body ); + --funnel-font-family: var( --wpds-typography-font-family-body ); + --step-font-family: var( --wpds-typography-font-family-body ); flex: 1; min-height: 0; } From d874acb8116a9f08ef4372f51b86b8bb68776ad7 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:39:02 +0800 Subject: [PATCH 29/32] changelog: add entry for premium-analytics widgets-toolkit package port --- ...oa7s-1319-integrate-widgets-toolkit-package-into-analytics | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1319-integrate-widgets-toolkit-package-into-analytics diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1319-integrate-widgets-toolkit-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1319-integrate-widgets-toolkit-package-into-analytics new file mode 100644 index 000000000000..1cd2f13c7091 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1319-integrate-widgets-toolkit-package-into-analytics @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Port the widgets-toolkit package (dashboard widgets, chart components, fields, and helpers) from next-woocommerce-analytics as an internal package. From 09e6f079af914e793bc338d585519f11af6eb989 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:52:49 +0800 Subject: [PATCH 30/32] fix(premium-analytics): resolve stylelint errors in widgets-toolkit scss --- .../chart-bar/bar-chart.module.scss | 24 ++++++++++--------- .../comparative-line-chart.module.scss | 8 +++---- .../chart-empty-state.module.scss | 4 ++-- .../leaderboard-chart.module.scss | 4 ++-- .../leaderboard-label.module.scss | 6 ++--- .../chart-tooltip/chart-tooltip.module.scss | 13 +++++----- .../metric-delta/metric-delta.module.scss | 10 ++++---- .../metric-value/metric-value.module.scss | 10 ++++---- .../widget-loading-overlay.module.scss | 2 +- .../widget-root/widget-root.module.scss | 5 ++-- .../src/styles/_widget-container.scss | 17 ++++++++----- .../conversion-rate-widget.module.scss | 4 ++-- .../visitors-by-location-widget.module.scss | 11 +++++---- 13 files changed, 64 insertions(+), 54 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss index 9a9f6722755f..8dfc1a5d1860 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-bar/bar-chart.module.scss @@ -2,29 +2,31 @@ // Override visx-bar default styles that break the layout // Todo: address upstream in Charts package. /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ - :global( .visx-bar ) { - // All corners rounded to handle both positive and negative bar values consistently. - clip-path: inset( 0 round 4px ); + :global(.visx-bar) { + // All corners rounded to handle both positive and negative bar + // values consistently. + clip-path: inset(0 round 4px); } .legend { - height: var( --wpds-typography-line-height-lg ); - min-height: var( --wpds-typography-line-height-lg ); + height: var(--wpds-typography-line-height-lg); + min-height: var(--wpds-typography-line-height-lg); flex-wrap: nowrap; // Vertically center the legend shape. - // The Charts package applies a non-zero transform to legend circles, which offsets them - // from the label baseline in this layout. Reset the transform here so the circle is - // aligned with the accompanying text. TODO: address this default upstream in the Charts package. + // The Charts package applies a non-zero transform to legend circles, + // which offsets them from the label baseline in this layout. Reset + // the transform here so the circle is aligned with the accompanying + // text. TODO: address this default upstream in the Charts package. circle { - transform: translate( 0, 0 ) !important; + transform: translate(0, 0) !important; } } .legendItem { min-width: 0; - gap: var( --wpds-dimension-gap-sm ); - padding: var( --wpds-dimension-padding-xs ) var( --wpds-dimension-padding-sm ); // 4px 6px->8px + gap: var(--wpds-dimension-gap-sm); + padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px } .legendLabel { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss index 5c7b043d9d92..2f4c511d9c7b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss @@ -3,15 +3,15 @@ .legend { flex: 0 0 auto; - height: var( --wpds-typography-line-height-lg ); - min-height: var( --wpds-typography-line-height-lg ); + height: var(--wpds-typography-line-height-lg); + min-height: var(--wpds-typography-line-height-lg); flex-wrap: nowrap; } .legendItem { min-width: 0; - gap: var( --wpds-dimension-gap-sm ); - padding: var( --wpds-dimension-padding-xs ) var( --wpds-dimension-padding-sm ); // 4px 6px->8px + gap: var(--wpds-dimension-gap-sm); + padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px } .legendLabel { diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss index 9f0076b968a1..60786aa66322 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-empty-state/chart-empty-state.module.scss @@ -4,9 +4,9 @@ justify-content: center; height: 100%; width: 100%; - gap: var( --wpds-dimension-gap-lg ); + gap: var(--wpds-dimension-gap-lg); } .icon { - color: var( --wpds-color-stroke-surface-neutral-weak, #e0e0e0 ); + color: var(--wpds-color-stroke-surface-neutral-weak, #e0e0e0); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss index c306a3f338a2..31919b013c4b 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.module.scss @@ -30,13 +30,13 @@ } .emptyStateIcon { - color: var( --wpds-color-fg-content-neutral-weak ); + color: var(--wpds-color-fg-content-neutral-weak); opacity: 0.5; } .emptyStateText { margin: 0; - color: var( --wpds-color-fg-content-neutral ); + color: var(--wpds-color-fg-content-neutral); font-size: 14px; text-align: center; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss index 35b64c85c83e..b13be4a837cf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-label.module.scss @@ -1,15 +1,15 @@ .container { - padding: var( --wpds-dimension-padding-sm ); + padding: var(--wpds-dimension-padding-sm); } .label { - font-size: var( --wpds-typography-font-size-sm ); + font-size: var(--wpds-typography-font-size-sm); } .labelImage { width: 28px; height: 28px; vertical-align: middle; - border-radius: var( --wpds-border-radius-md ); + border-radius: var(--wpds-border-radius-md); object-fit: cover; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss index eeecdcf4cde5..10fd837b1b6a 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-tooltip/chart-tooltip.module.scss @@ -1,14 +1,14 @@ .tooltip { - color: var( --wpds-color-fg-content-neutral ); - padding: var( --wpds-dimension-padding-sm ); + color: var(--wpds-color-fg-content-neutral); + padding: var(--wpds-dimension-padding-sm); margin: 0; - box-shadow: var( --wpds-elevation-sm ); + box-shadow: var(--wpds-elevation-sm); min-width: 200px; } .item { font-weight: 400; - line-height: var( --wpds-typography-line-height-xs ); + line-height: var(--wpds-typography-line-height-xs); } .label { @@ -16,9 +16,10 @@ } // Override visx-tooltip ONLY when our custom tooltip components are used. -// This applies to ChartTooltip (line/bar) and PieChartTooltip (pie/semi-circle). +// This applies to ChartTooltip (line/bar) and PieChartTooltip +// (pie/semi-circle). /* stylelint-disable-next-line selector-pseudo-class-no-unknown */ -:global( .visx-tooltip ):has( .tooltip ) { +:global(.visx-tooltip):has(.tooltip) { max-width: none !important; box-shadow: none !important; margin: 0 !important; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss index 9d41417fe3ab..0c3bc7d9e000 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-delta/metric-delta.module.scss @@ -1,18 +1,18 @@ .delta { - font-size: var( --wpds-typography-font-size-md ); + font-size: var(--wpds-typography-font-size-md); font-weight: 400; - line-height: var( --wpds-typography-font-size-lg ); + line-height: var(--wpds-typography-font-size-lg); &.invalid, &.neutral { - color: var( --wpds-color-fg-content-neutral-weak ); + color: var(--wpds-color-fg-content-neutral-weak); } &.positive { - color: var( --wpds-color-stroke-surface-success-strong ); + color: var(--wpds-color-stroke-surface-success-strong); } &.negative { - color: var( --wpds-color-stroke-surface-error-strong ); + color: var(--wpds-color-stroke-surface-error-strong); } } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss index a23979f919ce..b312580c1e0e 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/metric-value/metric-value.module.scss @@ -1,18 +1,18 @@ .metricValue { font-weight: 500; - font-size: var( --wp-ui-metric-font-size ); - line-height: var( --wpds-typography-line-height-sm ); + font-size: var(--wp-ui-metric-font-size); + line-height: var(--wpds-typography-line-height-sm); // Color variants &.color--neutral { - color: var( --wpds-color-fg-content-neutral ); + color: var(--wpds-color-fg-content-neutral); } &.color--positive { - color: var( --wpds-color-stroke-surface-success-strong ); + color: var(--wpds-color-stroke-surface-success-strong); } &.color--negative { - color: var( --wpds-color-stroke-surface-error-strong ); + color: var(--wpds-color-stroke-surface-error-strong); } } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss index c80a0707f40d..ff81e242d1ab 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-loading-overlay/widget-loading-overlay.module.scss @@ -3,5 +3,5 @@ inset: 0; z-index: 1; height: 100%; - background: color-mix( in srgb, var( --wpds-color-bg-surface-neutral-strong ) 60%, transparent ); + background: color-mix(in sRGB, var(--wpds-color-bg-surface-neutral-strong) 60%, transparent); } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss index e241cb72599e..465c6c663baf 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/widget-root/widget-root.module.scss @@ -1,5 +1,6 @@ -@use '../../styles/widget-container' as *; +@use "../../styles/widget-container" as *; .root { - @extend .widgetContainer; + + @extend %widget-container; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss index ed57843940c7..7172a7f25be7 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/styles/_widget-container.scss @@ -1,8 +1,11 @@ +@use "sass:map"; + /** * Widget Container Queries * * Provides CSS Container Query support for responsive widgets. - * Breakpoints aligned with Tailwind defaults for consistency with Design System. + * Breakpoints aligned with Tailwind defaults for consistency with the + * Design System. * * @see https://linear.app/a8c/issue/ARC-464/design-system-wide-responsiveness * @see https://tailwindcss.com/docs/responsive-design#container-size-reference @@ -44,7 +47,8 @@ $widget-breakpoints: ( * Container query mixin for widgets. * * @param {string} $breakpoint - Breakpoint name (xs, sm, md, lg, xl, 2xl) - * @param {string} $type - Query type (min-width or max-width), default: min-width + * @param {string} $type - Query type (min-width or max-width), + * default: min-width * * @example * // Min-width query (mobile-first) @@ -54,9 +58,10 @@ $widget-breakpoints: ( * @include widget-query( sm, max-width ) { ... } */ @mixin widget-query( $breakpoint, $type: min-width ) { - $size: map-get( $widget-breakpoints, $breakpoint ); + $size: map.get($widget-breakpoints, $breakpoint); @if not $size { + @error "Unknown breakpoint: #{$breakpoint}. Valid: xs, sm, md, lg, xl, 2xl"; } @@ -66,10 +71,10 @@ $widget-breakpoints: ( } /** - * Widget container base class. - * Apply to the widget wrapper element to enable container queries. + * Widget container base placeholder. + * Extend from the widget wrapper element to enable container queries. */ -.widgetContainer { +%widget-container { container-type: inline-size; container-name: widget; width: 100%; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss index e3a59abfe097..47a7ea065ed1 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/conversion-rate/conversion-rate-widget.module.scss @@ -4,8 +4,8 @@ } .conversionFunnelChart { - --funnel-font-family: var( --wpds-typography-font-family-body ); - --step-font-family: var( --wpds-typography-font-family-body ); + --funnel-font-family: var(--wpds-typography-font-family-body); + --step-font-family: var(--wpds-typography-font-family-body); flex: 1; min-height: 0; } diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss index f70ff394883f..40b03dafe03d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/widgets/visitors-by-location/visitors-by-location-widget.module.scss @@ -15,11 +15,12 @@ .toggleControl { grid-column: 2; - // The upstream widget content container (.next-admin-dashboard-widget__content) - // sets overflow: auto, which clips the ToggleGroupControl's outward focus ring. + // The upstream widget content container + // (.next-admin-dashboard-widget__content) sets overflow: auto, which + // clips the ToggleGroupControl's outward focus ring. // Add padding to create space for the focus indicator. - padding-block-start: var( --wpds-dimension-padding-xs, 4px ); - padding-inline-end: var( --wpds-dimension-padding-xs, 4px ); + padding-block-start: var(--wpds-dimension-padding-xs, 4px); + padding-inline-end: var(--wpds-dimension-padding-xs, 4px); padding-block-end: 0; padding-inline-start: 0; } @@ -29,6 +30,6 @@ .leaderboardImage { height: 20px; - border-radius: var( --wpds-border-radius-sm, 2px ); + border-radius: var(--wpds-border-radius-sm, 2px); } } From 01f7aa92e3df366f1ee0854ed79ced9e0741a1d9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 12:52:49 +0800 Subject: [PATCH 31/32] fix(premium-analytics): pin @wordpress/dataviews to 14.3.0 to avoid react-native peer issue --- pnpm-lock.yaml | 548 +----------------- .../packages/premium-analytics/package.json | 2 +- .../packages/widgets-toolkit/package.json | 2 +- 3 files changed, 15 insertions(+), 537 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdf7caf7545e..7d4c70511a3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3900,8 +3900,8 @@ importers: specifier: 10.46.0 version: 10.46.0(react@18.3.1) '@wordpress/dataviews': - specifier: 16.0.0 - version: 16.0.0(react@18.3.1) + specifier: 14.3.0 + version: 14.3.0(react@18.3.1) '@wordpress/i18n': specifier: ^6.9.0 version: 6.19.0 @@ -7891,17 +7891,6 @@ packages: '@emotion/memoize@0.9.0': resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} - '@emotion/native@11.11.0': - resolution: {integrity: sha512-t1b5bLv+o5OUNLqXlnw+LJYU10OpmYkLC/1W873Y1ohG+vObx5TT3o3Eh1okXb2KCuZTTBPgsEnU/Sl7NNkJ9Q==} - peerDependencies: - react-native: '>=0.14.0 <1' - - '@emotion/primitives-core@11.13.2': - resolution: {integrity: sha512-+MX60ROt1fDi5EYafhE/zs78XD4OuFUn6j0Z274wo5wVMT8sSBRx2CKPMbOUnmCcT0K5GPog+41mtkcppzkMmg==} - peerDependencies: - '@emotion/react': ^11.0.0-rc.0 - react: '>=16.8.0' - '@emotion/react@11.14.0': resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} peerDependencies: @@ -10890,10 +10879,6 @@ packages: resolution: {integrity: sha512-9VKhQHB/TQHJciOtxbpJ5JPhxMHCOszcxs4eL27krFXMEp3fl4tzVy13r1LPuXg/yjZ9NpV3NY+Qwx4G0aW3Kw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/a11y@4.48.0': - resolution: {integrity: sha512-MXwBc2sYaemZCn1dqVutTbLdM6iy4bx/HS9hHR/+pRpaSVJUlguZ1aQ0BaoIbE4u0uOezGGc5d2bDfWCti3Dww==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/admin-ui@2.1.0': resolution: {integrity: sha512-wFZ6pOxex/3iz1NuV27/7uL/iJtbqrLPqWKuQlpaJiXL9ZLj2JvHWkQVpvzi10EzIDQLqX2WdSOzS0SyXIro2w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -10932,10 +10917,6 @@ packages: resolution: {integrity: sha512-livtgwnvBm7xbpm/gaBxwtdZm3KCXq210UNsr48WA8TGfi/OfZ4oOzk4Mp4/ZHsq2baaXzhZ0iXjyR7oyaOTsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/base-styles@9.1.0': - resolution: {integrity: sha512-QONqtlA7IRYb6cbCjwTEiXJwfkWPpHl6PSS+F1TDeDP0L7m+hXfpRbH1qfKjSffWlyDaBqLFWwXZ3evpeFw5bg==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/blob@4.46.0': resolution: {integrity: sha512-TUu6k4SFPThT86ek/O87/aQfwKVYAGG9Gt14uvYPXPyLz90/KeFLr15v9waZV2luk2xCZACMIa4OdBHQlkL/aQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11008,25 +10989,12 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/components@35.0.0': - resolution: {integrity: sha512-zXhErp2/alcdvQST6pq/kkZGkiOvTGbYqRc3FuoQIDpCJJE70r243PRxokDvZ5ikHvBtg26kARN+JhXdzN4qjw==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - '@wordpress/compose@7.46.0': resolution: {integrity: sha512-6Yv9Wb6tlA4JYU9bdWWuIWpTTzBAVA1zrYu1GY9x2/mCOckk9iLcEEfbKULxdjwwcMo3SKqvyby4f6kEUw/Wsw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 - '@wordpress/compose@8.1.0': - resolution: {integrity: sha512-CAEQxrh3f19ku0SAnYAAiKcUe1zqaK9f0c8vJh+6qrpQnUjl7xLXj5TJOukXlzFH3Z9VZn6fJVfXqmDNAYIhQA==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/core-data@7.46.0': resolution: {integrity: sha512-mfiqOrXcsv4rZJZFYjmUSc5goK1cKpuQ1lSoSBnuKMJNZAxTCVTwexIaj0XI5Qr/ngUjT5U1+w4I0Fzuv/qCMQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11046,32 +11014,16 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/data@10.48.0': - resolution: {integrity: sha512-6SjfTBlXu5fuJWmmlHlwV2wcrcsWL+M5O227AoEvrPSLo96UuMj2kAx3cKLtP3xyOMDyd38koQSf6+SS522bTA==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/dataviews@14.3.0': resolution: {integrity: sha512-2fFSgyatDldjPb2gO+vLDwkbI2Jw+8zd/O0/BwLftQ5QhrrRtAqECFp+eYzcQ8Onh8OMhxq0n7tsaIHE/jWqJQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} peerDependencies: react: ^18.0.0 - '@wordpress/dataviews@16.0.0': - resolution: {integrity: sha512-02rbslxalTNasLV8w/zAifCsUU5Pug8GiduWIEKRiNtazvJ8duz8fIcQ2Jgl31ruRItcu3fcG7XUk1OtwsdcZQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/date@5.46.0': resolution: {integrity: sha512-phbKy1siTFGwFet5hQzaSZJB1mMDIXflMLKj+oJ/mT/m9ughp3seFDPvKoL+UzukLxNJh3l5G5h1l9XQFfC2cA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/date@5.48.0': - resolution: {integrity: sha512-HgXtYAD2IOrPDY83xzkT/8abYj2nMlkbC+lfSpB4lExlSVrIbz5oYUtktH8k5EBZjVBMFsE7mdMQyQjUeCQbeQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/dependency-extraction-webpack-plugin@6.46.0': resolution: {integrity: sha512-Lm2JFEI4NrcEQFdnIXK+CsUQGK/LTiRxrDY0ocpTLt5hhb3DJm3Ds2HFn8fa//H0U5B3FvO3XyGMHOUf9Q12Pg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11090,18 +11042,10 @@ packages: resolution: {integrity: sha512-CQ6KPaCkMzAmbxmR4E4Fu99ngyPpkP9VGaIFu0xUgx0ubkYOzcvEfEEPuyEV3n7PY2Jg/XWzBilgWCa8PmaxWw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/dom-ready@4.48.0': - resolution: {integrity: sha512-jtH9/4FBTsfYLJDzgiXs41nceTrfvuLXqaWa5IN8drHvXZde6Dhz78m3KCZLrOB5DEE1tbyBNyZkcWM8HNVZ0Q==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/dom@4.46.0': resolution: {integrity: sha512-XngkvNJpf0JnpZuOcsbBl/cTprfYQTfSykttIL4laXcFXfZe8rU3bGgv8K7AEoYigDwxfw3g/yMPi4fn195Kpw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/dom@4.48.0': - resolution: {integrity: sha512-9UARZ0YQfmhx9VAi+QynSwu5fOJoG4mmPNTpYW8jDmtKh+9c2YIi1YSQFuOa1sipj78ZLPaBxaceZ7dbxKc3UA==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/e2e-test-utils-playwright@1.46.0': resolution: {integrity: sha512-Bls5BGRNda0Oo4biTZ/KIwO8iHBeovvfWNfrPXReIsrW1td1UqXw2Z9l0/LaP3euJZFNom2QExHCOba+8eN5lQ==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11127,18 +11071,10 @@ packages: resolution: {integrity: sha512-hjnrqZi0cZVdkmN0xQavKfSQJYAkb9pVSnDPpuX65OLxeD9/EWkIXvFzBb+nH8c4NzKKSqQU96XCTQrH37OCIA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/element@8.0.0': - resolution: {integrity: sha512-lQ8TB2vBr6lzcwQ2zh0xy+FC77Demb3FqL81fzpSsLbGUF9hZiTyyUuwc6SG21gCkGGjVm2TnU9BuHax/8nDfQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/escape-html@3.46.0': resolution: {integrity: sha512-SzrVQwLQBZdaSStYVpTKeYqp97NABz1w551T8me3msDDsfhWWPhSZiZTNaGZ6iqUNfOX2uKyZsqXedvkqwLHqA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/escape-html@3.48.0': - resolution: {integrity: sha512-phw399RofSqTqIM4DikmkDfgJ7exDYgPfDuxjv3D2YnUTTUsR+U9fA+pA+/rNUiZD1YOmVILQmkJt6oLaVM+nQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/eslint-plugin@25.2.0': resolution: {integrity: sha512-h3Yz5Qzo1v53Rw9i8WBm68P6SFpVSeqWDohowpEeuIz2RC8Jg1CT5j49tVpSZXGNCQGSf3SaPLjXmiyxTZXkSw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11204,20 +11140,11 @@ packages: resolution: {integrity: sha512-YJ/V9R2p4lwYkhc9/bQrXxoX0rNDtt1WQGInKAxRWqF1w1gYQk0iWiwGcNnahnFofwK2LJSVf4/jYFjJrS/sPw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/html-entities@4.48.0': - resolution: {integrity: sha512-KGxdaLC36wE10GybSfjYGcyWiy+KQCYheB6T8jhZhQ9mlf2Zwx6aJgfZm/L6BLwNN33Efx+sJY3nvMIxI5UwnA==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/i18n@6.19.0': resolution: {integrity: sha512-hRXd2E0SF9OQf22ZZWw7Ny/o+Q9u8jINiF1p0bF+rnSDKQUgoStihak6YiazWVRiIEYwctzotKXlt0HePJelXA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} hasBin: true - '@wordpress/i18n@6.21.0': - resolution: {integrity: sha512-IXGGUJqN6b7QddU0dZB3HLJKu6uDQuhLsrrzYpUYTjDhfa43XEaikA9xHNgZhqzRtOVYqsNHVliWcISvJ/xjZQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - hasBin: true - '@wordpress/icons@10.32.0': resolution: {integrity: sha512-1WvJdT361X1LnetYBpBWUjAVXZzl+pBdIwHbYRAp8ej47EI/igPmNxmq81nFd40s8fer/9qtipielcqSI6H2rA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11230,12 +11157,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/icons@13.3.0': - resolution: {integrity: sha512-Y/iE3aeHQ4XkX0fffiTPCUfjT8wNw1I7hDJkKqpaLmkD+C5NKWixRrDVfRnaJqU/MxY8RdyVC/nGng2MLPNH0A==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/image-cropper@1.10.0': resolution: {integrity: sha512-Aq4Wz2nGf+GYZPi+n+nWq7AtsGSjUbREzYKEUQIAnt2pPGn2ZkvooBUQE405WOqoJadE+tkLjKdyjY24iOtocA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11262,10 +11183,6 @@ packages: resolution: {integrity: sha512-46J36GNPw7q3c5HF0RurUx9yJHvBDYqOFVqbb8Td8bov9pVI6TGtcMKd+/O+Q89ZUVSTVx/NfxKjNwXpeQQCmg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/is-shallow-equal@5.48.0': - resolution: {integrity: sha512-7ipiZ1+m84RfuVhiMbtKm6RN571W3ERV/pTL+fSG2qOVhLqccFmliuFHTKQG+0KIhV8DegOlE6eoKOenf+I9ng==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/jest-console@8.46.0': resolution: {integrity: sha512-bD5FD/LDbDyfadZzxfUCOM6uBXlIfRFj+AAsgmCHuUBW3c7PrsZXDGh5KRaR8E0XLoeLpkxA78fpUaY8S1+XPw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11282,10 +11199,6 @@ packages: resolution: {integrity: sha512-+eW0b4bRrpmiOOfdmz1BtQsbTqWqCkgJyeiR5yMLJ+sGG2He9icVLjt/fSc4xCQ56MhT03Zypb33L6j+zJFEgA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/keycodes@4.48.0': - resolution: {integrity: sha512-u3Uxxe3rDAqEmerAiJ2X94s7iO3ZVgS+10MFyD4nWhfuB/C6m/M2TqHPgZiKvyDH04EIhe+pIF2KFO4pq7NWsw==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/latex-to-mathml@1.14.0': resolution: {integrity: sha512-kLzovQBKlBSHsqXICIclUBOFCm9ROEmXU+xUaV6UpS2pb4BIcsBew/z8URKMHtYA/jRhDM9uejEy45YMI8swZw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11351,28 +11264,14 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/primitives@4.48.0': - resolution: {integrity: sha512-dfF7IZotIqb6LUiGs7oPwKbSF8RPoC0JDSIrtxvgwFA/yvbc/pDIp/Zs0O8GvxZNxu4JIVnKskOhoLq7lAeziQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/priority-queue@3.46.0': resolution: {integrity: sha512-rjwzO/I7Os16VMJFVdzIeXMmyvwe+DbODrXl3mgW5LZZeIYob94d++pjQxUdWN1/0APnXPQP6zk4yFfSLOVkYg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/priority-queue@3.48.0': - resolution: {integrity: sha512-NuGrfSSnBC794erb3xSEKrzWLGCNLa+ukob0pyVRtnebU7fPgrhx4NCBCXYK1vTcAta3NAkOVRfUZgcmLFYA6g==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/private-apis@1.46.0': resolution: {integrity: sha512-l8dsEuxq6CrtsI7Twfpn6CbPHmGBUQoGN4oLPJG1Bqsr1yXXLU/bEx9KAQN9emxRjXaELPsn7x7TVx0TUoKyJw==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/private-apis@1.48.0': - resolution: {integrity: sha512-HHOSXLCAlBggfMozwWtX36wgsSt22g2tZwpka47Rjzr3hNY1BZ6SrrFJumiNxooy5PDKbRgcF092PAF82hdJXg==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/react-i18n@4.45.0': resolution: {integrity: sha512-9DchjKkdgw5r51br2fIqlMZ2zOd8Fk9xh3Kh8GRbTmeXfFKmgGz8okUrlbRgYPJKdh4b7slSkyQY26FrV7X/0w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11383,12 +11282,6 @@ packages: peerDependencies: redux: '>=4' - '@wordpress/redux-routine@5.48.0': - resolution: {integrity: sha512-MxRgJJyddivxvVhPrn8yEFXTH3WLtoRGNCMiBRJwoIr4GkY8iOFSfRaqOJEkE1zrP4JK6qGFmv1xMvWt78c7ow==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - redux: '>=4' - '@wordpress/reusable-blocks@5.46.0': resolution: {integrity: sha512-bAX9YGgb8OfTxNKimxzfuWj/39EP9jtJXKVYcChDgYPKH1gl02I8+OdyFx+RycWFxRjMTwsgL5WEjgkGC6nokA==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11402,12 +11295,6 @@ packages: peerDependencies: react: ^18.0.0 - '@wordpress/rich-text@7.48.0': - resolution: {integrity: sha512-rMiTTpRnpdynL9BnuI2MkSXzd12Js8gYSnlbVwxNNKNeFEXT+3Ah2oNCGvSb82pD/73Bl5BIGC5395D5a3X9yw==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - '@wordpress/route@0.12.0': resolution: {integrity: sha512-JQ55LLMnzxjWWCd0UWnFrp1pTTC7u4KEA8w/IT01oV27Lyw4BQighl7KTE/mpN5CYupXUHUELCFceWyHiMDV4w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11439,10 +11326,6 @@ packages: resolution: {integrity: sha512-9pLmilkgWqTvIlrnnXbW7ECfEPvCSYOve7btXgYGgMOzrGs12ijnG+kSGGg0aJhEV8OCzQ/QdVBh4s1zQZ0bLQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} - '@wordpress/style-runtime@0.4.0': - resolution: {integrity: sha512-frzAg1rsn8X0KNgrxxLxszLvWCKY0Nk2e8j8Mjm2pI2URmS8Et7NefuXP3JnHBD4U1L1Ug9yKO/FA65ojQ7CEA==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - '@wordpress/stylelint-config@23.38.0': resolution: {integrity: sha512-F1Bo45fhWFrpEXlkkwVfopmmgM8PwbzplrlBwu1FGm+9ohF890IXKhjjQ/CDphE9pMBCQnAyofF6ESymhbEm5A==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11465,17 +11348,6 @@ packages: stylelint: optional: true - '@wordpress/theme@0.15.0': - resolution: {integrity: sha512-qoozJ4YEPb0LvTBnTMj8a7kPlQtT2LeGL7b/vKJkvnB9dIEUOED5c0rpeRZJoK9b77fpUH5GwYzPE3IWiQ6l2w==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - stylelint: '>=16.8.2' - peerDependenciesMeta: - stylelint: - optional: true - '@wordpress/token-list@3.46.0': resolution: {integrity: sha512-g9UytUCFcLnj8LWNHFUK0c53FeokTEXDlZ3C3VrpDnxq0jC0BnNj0uJCAmbzfehg23LWI2O5xnQzmpAJ9ldAKg==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -11487,21 +11359,10 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@wordpress/ui@0.15.0': - resolution: {integrity: sha512-7aAx1ovnC6JOb4Qfcnfk8ESfB0RTm6rqsdFrUn7TEY3LON/aEQisCb/bd7Yb8s9txb1GfaJYkgjiTvrr0M6EWA==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - '@wordpress/undo-manager@1.46.0': resolution: {integrity: sha512-vAchoUrF97IdjqVD30Iz7NI9YvDtgeMNPshgjsrM8MF9nOCMq2tBWb3HS+ue/kQknfAuU73FEnn/UNKt0JPH4Q==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/undo-manager@1.48.0': - resolution: {integrity: sha512-HqPGxMvZeWZJ6AVaCqZhfGpH6tqq5+hMlaqh4aCO0SvZ2Gvc6fbXEoVpqWfKozO1DyJW2GnRf8At8PpPt2IopQ==} - engines: {node: '>=18.12.0', npm: '>=8.19.2'} - '@wordpress/upload-media@0.31.0': resolution: {integrity: sha512-fpg1wx1p04AEyemQ7EGsR1c2oHcHCgsVdKbSl27L4Nvw2YlhiZY6yb/BOqUNyBqTJ6OBS85vKace6JzW6KR27w==} engines: {node: '>=18.12.0', npm: '>=8.19.2'} @@ -12501,10 +12362,6 @@ packages: csp_evaluator@1.1.5: resolution: {integrity: sha512-EL/iN9etCTzw/fBnp0/uj0f5BOOGvZut2mzsiiBZ/FdT6gFQCKRO/tmcKOxn5drWZ2Ndm/xBb1SI4zwWbGtmIw==} - css-color-keywords@1.0.0: - resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} - engines: {node: '>=4'} - css-declaration-sorter@7.4.0: resolution: {integrity: sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==} engines: {node: ^14 || ^16 || >=18} @@ -12558,9 +12415,6 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - css-to-react-native@3.2.0: - resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} - css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -18687,7 +18541,7 @@ snapshots: '@automattic/api-core@1.0.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@automattic/shopping-cart': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/dataviews': 16.0.0(@types/react@18.3.28)(react@18.3.1) + '@wordpress/dataviews': 14.3.0(@types/react@18.3.28)(react@18.3.1) '@wordpress/i18n': 6.19.0 he: 1.2.0 tslib: 2.8.1 @@ -18696,7 +18550,6 @@ snapshots: - '@types/react' - react - react-dom - - react-native - stylelint - supports-color @@ -18774,7 +18627,7 @@ snapshots: '@wordpress/primitives': 4.46.0(react@18.3.1) '@wordpress/react-i18n': 4.45.0 '@wordpress/url': 4.46.0 - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 canvas-confetti: 1.9.4 clsx: 2.1.1 colord: 2.9.3 @@ -18813,7 +18666,7 @@ snapshots: '@wordpress/api-fetch': 7.46.0 '@wordpress/data': 10.46.0(react@18.3.1) '@wordpress/data-controls': 4.46.0(react@18.3.1) - '@wordpress/deprecated': 4.46.0 + '@wordpress/deprecated': 4.48.0 '@wordpress/element': 6.46.0 '@wordpress/i18n': 6.19.0 '@wordpress/primitives': 4.46.0(react@18.3.1) @@ -18830,7 +18683,6 @@ snapshots: transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react' - - react-native - stylelint - supports-color @@ -18912,7 +18764,6 @@ snapshots: transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react' - - react-native - stylelint - supports-color @@ -20234,19 +20085,6 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/native@11.11.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)': - dependencies: - '@emotion/primitives-core': 11.13.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) - transitivePeerDependencies: - - '@emotion/react' - - react - - '@emotion/primitives-core@11.13.2(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)': - dependencies: - '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) - css-to-react-native: 3.2.0 - react: 18.3.1 - '@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 @@ -23747,11 +23585,6 @@ snapshots: '@wordpress/dom-ready': 4.46.0 '@wordpress/i18n': 6.19.0 - '@wordpress/a11y@4.48.0': - dependencies: - '@wordpress/dom-ready': 4.48.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/admin-ui@2.1.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/components': 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -23819,7 +23652,7 @@ snapshots: '@babel/preset-env': 7.29.2(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@wordpress/browserslist-config': 6.46.0 - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 browserslist: 4.28.2 core-js: 3.38.1 react: 18.3.1 @@ -23828,8 +23661,6 @@ snapshots: '@wordpress/base-styles@8.0.0': {} - '@wordpress/base-styles@9.1.0': {} - '@wordpress/blob@4.46.0': {} '@wordpress/block-editor@15.19.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -24505,68 +24336,6 @@ snapshots: - '@emotion/is-prop-valid' - supports-color - '@wordpress/components@35.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@date-fns/utc': 2.1.1 - '@emotion/cache': 11.14.0 - '@emotion/css': 11.13.5 - '@emotion/native': 11.11.0(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) - '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) - '@emotion/serialize': 1.3.3 - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) - '@emotion/utils': 1.4.2 - '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/gradient-parser': 1.1.0 - '@types/highlight-words-core': 1.2.1 - '@types/react': 18.3.28 - '@use-gesture/react': 10.3.1(react@18.3.1) - '@wordpress/a11y': 4.48.0 - '@wordpress/base-styles': 9.1.0 - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/date': 5.48.0 - '@wordpress/deprecated': 4.48.0 - '@wordpress/dom': 4.48.0 - '@wordpress/element': 8.0.0 - '@wordpress/escape-html': 3.48.0 - '@wordpress/hooks': 4.48.0 - '@wordpress/html-entities': 4.48.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/icons': 13.3.0(react@18.3.1) - '@wordpress/is-shallow-equal': 5.48.0 - '@wordpress/keycodes': 4.48.0 - '@wordpress/primitives': 4.48.0(react@18.3.1) - '@wordpress/private-apis': 1.48.0 - '@wordpress/rich-text': 7.48.0(react@18.3.1) - '@wordpress/style-runtime': 0.4.0 - '@wordpress/ui': 0.15.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/warning': 3.48.0 - change-case: 4.1.2 - clsx: 2.1.1 - colord: 2.9.3 - csstype: 3.2.3 - date-fns: 4.1.0 - deepmerge: 4.3.1 - fast-deep-equal: 3.1.3 - framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - gradient-parser: 1.1.1 - highlight-words-core: 1.2.3 - is-plain-object: 5.0.0 - memize: 2.1.1 - path-to-regexp: 6.3.0 - re-resizable: 6.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-day-picker: 9.14.0(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - remove-accents: 0.5.0 - uuid: 14.0.0 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - react-native - - stylelint - - supports-color - '@wordpress/compose@7.46.0(react@18.3.1)': dependencies: '@types/mousetrap': 1.6.15 @@ -24582,22 +24351,6 @@ snapshots: react: 18.3.1 use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/compose@8.1.0(react@18.3.1)': - dependencies: - '@types/mousetrap': 1.6.15 - '@wordpress/deprecated': 4.48.0 - '@wordpress/dom': 4.48.0 - '@wordpress/element': 8.0.0 - '@wordpress/is-shallow-equal': 5.48.0 - '@wordpress/keycodes': 4.48.0 - '@wordpress/priority-queue': 3.48.0 - '@wordpress/private-apis': 1.48.0 - '@wordpress/undo-manager': 1.48.0 - change-case: 4.1.2 - mousetrap: 1.6.5 - react: 18.3.1 - use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/core-data@7.46.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/api-fetch': 7.46.0 @@ -24719,24 +24472,6 @@ snapshots: rememo: 4.0.2 use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/data@10.48.0(react@18.3.1)': - dependencies: - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/deprecated': 4.48.0 - '@wordpress/element': 8.0.0 - '@wordpress/is-shallow-equal': 5.48.0 - '@wordpress/priority-queue': 3.48.0 - '@wordpress/private-apis': 1.48.0 - '@wordpress/redux-routine': 5.48.0(redux@5.0.1) - deepmerge: 4.3.1 - equivalent-key-map: 0.2.2 - is-plain-object: 5.0.0 - is-promise: 4.0.0 - react: 18.3.1 - redux: 5.0.1 - rememo: 4.0.2 - use-memo-one: 1.1.3(react@18.3.1) - '@wordpress/dataviews@14.3.0(@types/react@18.3.28)(react@18.3.1)': dependencies: '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -24745,7 +24480,7 @@ snapshots: '@wordpress/components': 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/compose': 7.46.0(react@18.3.1) '@wordpress/data': 10.46.0(react@18.3.1) - '@wordpress/deprecated': 4.46.0 + '@wordpress/deprecated': 4.48.0 '@wordpress/element': 6.46.0 '@wordpress/i18n': 6.19.0 '@wordpress/icons': 13.1.0(react@18.3.1) @@ -24753,7 +24488,7 @@ snapshots: '@wordpress/primitives': 4.46.0(react@18.3.1) '@wordpress/private-apis': 1.46.0 '@wordpress/ui': 0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 clsx: 2.1.1 react: 18.3.1 remove-accents: 0.5.0 @@ -24796,7 +24531,7 @@ snapshots: '@wordpress/components': 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/compose': 7.46.0(react@18.3.1) '@wordpress/data': 10.46.0(react@18.3.1) - '@wordpress/deprecated': 4.46.0 + '@wordpress/deprecated': 4.48.0 '@wordpress/element': 6.46.0 '@wordpress/i18n': 6.19.0 '@wordpress/icons': 13.1.0(react@18.3.1) @@ -24804,7 +24539,7 @@ snapshots: '@wordpress/primitives': 4.46.0(react@18.3.1) '@wordpress/private-apis': 1.46.0 '@wordpress/ui': 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 clsx: 2.1.1 react: 18.3.1 remove-accents: 0.5.0 @@ -24839,122 +24574,12 @@ snapshots: - stylelint - supports-color - '@wordpress/dataviews@16.0.0(@types/react@18.3.28)(react@18.3.1)': - dependencies: - '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@date-fns/tz': 1.4.1 - '@wordpress/base-styles': 9.1.0 - '@wordpress/components': 35.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/data': 10.48.0(react@18.3.1) - '@wordpress/deprecated': 4.48.0 - '@wordpress/element': 8.0.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/icons': 13.3.0(react@18.3.1) - '@wordpress/keycodes': 4.48.0 - '@wordpress/primitives': 4.48.0(react@18.3.1) - '@wordpress/private-apis': 1.48.0 - '@wordpress/ui': 0.15.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/warning': 3.48.0 - clsx: 2.1.1 - react: 18.3.1 - remove-accents: 0.5.0 - optionalDependencies: - '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@emotion/cache': 11.14.0 - '@emotion/css': 11.13.5 - '@emotion/react': 11.14.0(@types/react@18.3.28)(react@18.3.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.28)(react@18.3.1))(@types/react@18.3.28)(react@18.3.1) - '@emotion/utils': 1.4.2 - '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@use-gesture/react': 10.3.1(react@18.3.1) - '@wordpress/date': 5.48.0 - '@wordpress/hooks': 4.48.0 - change-case: 4.1.2 - colord: 2.9.3 - date-fns: 4.1.0 - deepmerge: 4.3.1 - fast-deep-equal: 3.1.3 - framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - highlight-words-core: 1.2.3 - is-plain-object: 5.0.0 - memize: 2.1.1 - react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-day-picker: 9.14.0(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - use-memo-one: 1.1.3(react@18.3.1) - uuid: 14.0.0 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/react' - - react-native - - stylelint - - supports-color - - '@wordpress/dataviews@16.0.0(react@18.3.1)': - dependencies: - '@ariakit/react': 0.4.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@date-fns/tz': 1.4.1 - '@wordpress/base-styles': 9.1.0 - '@wordpress/components': 35.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/data': 10.48.0(react@18.3.1) - '@wordpress/deprecated': 4.48.0 - '@wordpress/element': 8.0.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/icons': 13.3.0(react@18.3.1) - '@wordpress/keycodes': 4.48.0 - '@wordpress/primitives': 4.48.0(react@18.3.1) - '@wordpress/private-apis': 1.48.0 - '@wordpress/ui': 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/warning': 3.48.0 - clsx: 2.1.1 - react: 18.3.1 - remove-accents: 0.5.0 - optionalDependencies: - '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@emotion/cache': 11.14.0 - '@emotion/css': 11.13.5 - '@emotion/react': 11.14.0(react@18.3.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(react@18.3.1))(react@18.3.1) - '@emotion/utils': 1.4.2 - '@floating-ui/react-dom': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@use-gesture/react': 10.3.1(react@18.3.1) - '@wordpress/date': 5.48.0 - '@wordpress/hooks': 4.48.0 - change-case: 4.1.2 - colord: 2.9.3 - date-fns: 4.1.0 - deepmerge: 4.3.1 - fast-deep-equal: 3.1.3 - framer-motion: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - highlight-words-core: 1.2.3 - is-plain-object: 5.0.0 - memize: 2.1.1 - react-colorful: 5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-day-picker: 9.14.0(react@18.3.1) - react-dom: 18.3.1(react@18.3.1) - use-memo-one: 1.1.3(react@18.3.1) - uuid: 14.0.0 - transitivePeerDependencies: - - '@emotion/is-prop-valid' - - '@types/react' - - react-native - - stylelint - - supports-color - '@wordpress/date@5.46.0': dependencies: '@wordpress/deprecated': 4.46.0 moment: 2.30.1 moment-timezone: 0.5.48 - '@wordpress/date@5.48.0': - dependencies: - '@wordpress/deprecated': 4.48.0 - moment: 2.30.1 - moment-timezone: 0.5.48 - '@wordpress/dependency-extraction-webpack-plugin@6.46.0(webpack@5.105.2)': dependencies: json2php: 0.0.7 @@ -24970,16 +24595,10 @@ snapshots: '@wordpress/dom-ready@4.46.0': {} - '@wordpress/dom-ready@4.48.0': {} - '@wordpress/dom@4.46.0': dependencies: '@wordpress/deprecated': 4.46.0 - '@wordpress/dom@4.48.0': - dependencies: - '@wordpress/deprecated': 4.48.0 - '@wordpress/e2e-test-utils-playwright@1.46.0(@playwright/test@1.60.0)(@types/node@24.12.3)': dependencies: '@playwright/test': 1.60.0 @@ -25340,21 +24959,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@wordpress/element@8.0.0': - dependencies: - '@types/react': 18.3.28 - '@types/react-dom': 18.3.7(@types/react@18.3.28) - '@wordpress/deprecated': 4.48.0 - '@wordpress/escape-html': 3.48.0 - change-case: 4.1.2 - is-plain-object: 5.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - '@wordpress/escape-html@3.46.0': {} - '@wordpress/escape-html@3.48.0': {} - '@wordpress/eslint-plugin@25.2.0(@babel/core@7.29.0)(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint-plugin-import@2.32.0)(eslint-plugin-jest@29.15.2(eslint@10.4.1)(jest@30.4.2)(typescript@5.9.3))(eslint-plugin-jsdoc@63.0.0(eslint@10.4.1))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.4.1))(eslint-plugin-playwright@2.10.4(eslint@10.4.1))(eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1))(eslint@10.4.1)(wp-prettier@3.0.3))(eslint-plugin-react-hooks@7.1.1(eslint@10.4.1))(eslint-plugin-react@7.37.5(eslint@10.4.1))(eslint@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint@17.7.0(typescript@5.9.3))(typescript@5.9.3)(wp-prettier@3.0.3)': dependencies: '@babel/core': 7.29.0 @@ -25415,7 +25021,7 @@ snapshots: '@wordpress/router': 1.46.0(react@18.3.1) '@wordpress/ui': 0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/url': 4.46.0 - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 '@wordpress/wordcount': 4.46.0 change-case: 4.1.2 client-zip: 2.5.0 @@ -25456,7 +25062,7 @@ snapshots: '@wordpress/router': 1.46.0(react@18.3.1) '@wordpress/ui': 0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/url': 4.46.0 - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 '@wordpress/wordcount': 4.46.0 change-case: 4.1.2 client-zip: 2.5.0 @@ -25497,7 +25103,7 @@ snapshots: '@wordpress/router': 1.46.0(react@18.3.1) '@wordpress/ui': 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@wordpress/url': 4.46.0 - '@wordpress/warning': 3.46.0 + '@wordpress/warning': 3.48.0 '@wordpress/wordcount': 4.46.0 change-case: 4.1.2 client-zip: 2.5.0 @@ -25648,8 +25254,6 @@ snapshots: '@wordpress/html-entities@4.46.0': {} - '@wordpress/html-entities@4.48.0': {} - '@wordpress/i18n@6.19.0': dependencies: '@tannin/sprintf': 1.3.3 @@ -25658,14 +25262,6 @@ snapshots: memize: 2.1.1 tannin: 1.2.0 - '@wordpress/i18n@6.21.0': - dependencies: - '@tannin/sprintf': 1.3.3 - '@wordpress/hooks': 4.48.0 - gettext-parser: 1.4.0 - memize: 2.1.1 - tannin: 1.2.0 - '@wordpress/icons@10.32.0(react@18.3.1)': dependencies: '@babel/runtime': 7.29.2 @@ -25680,13 +25276,6 @@ snapshots: change-case: 4.1.2 react: 18.3.1 - '@wordpress/icons@13.3.0(react@18.3.1)': - dependencies: - '@wordpress/element': 8.0.0 - '@wordpress/primitives': 4.48.0(react@18.3.1) - change-case: 4.1.2 - react: 18.3.1 - '@wordpress/image-cropper@1.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/components': 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -25762,8 +25351,6 @@ snapshots: '@wordpress/is-shallow-equal@5.46.0': {} - '@wordpress/is-shallow-equal@5.48.0': {} - '@wordpress/jest-console@8.46.0(jest@30.4.2)': dependencies: jest: 30.4.2 @@ -25781,10 +25368,6 @@ snapshots: dependencies: '@wordpress/i18n': 6.19.0 - '@wordpress/keycodes@4.48.0': - dependencies: - '@wordpress/i18n': 6.21.0 - '@wordpress/latex-to-mathml@1.14.0': dependencies: temml: 0.10.34 @@ -26204,24 +25787,12 @@ snapshots: clsx: 2.1.1 react: 18.3.1 - '@wordpress/primitives@4.48.0(react@18.3.1)': - dependencies: - '@wordpress/element': 8.0.0 - clsx: 2.1.1 - react: 18.3.1 - '@wordpress/priority-queue@3.46.0': dependencies: requestidlecallback: 0.3.0 - '@wordpress/priority-queue@3.48.0': - dependencies: - requestidlecallback: 0.3.0 - '@wordpress/private-apis@1.46.0': {} - '@wordpress/private-apis@1.48.0': {} - '@wordpress/react-i18n@4.45.0': dependencies: '@wordpress/element': 6.46.0 @@ -26235,13 +25806,6 @@ snapshots: redux: 5.0.1 rungen: 0.3.2 - '@wordpress/redux-routine@5.48.0(redux@5.0.1)': - dependencies: - is-plain-object: 5.0.0 - is-promise: 4.0.0 - redux: 5.0.1 - rungen: 0.3.2 - '@wordpress/reusable-blocks@5.46.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/base-styles': 8.0.0 @@ -26327,22 +25891,6 @@ snapshots: memize: 2.1.1 react: 18.3.1 - '@wordpress/rich-text@7.48.0(react@18.3.1)': - dependencies: - '@wordpress/a11y': 4.48.0 - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/data': 10.48.0(react@18.3.1) - '@wordpress/deprecated': 4.48.0 - '@wordpress/dom': 4.48.0 - '@wordpress/element': 8.0.0 - '@wordpress/escape-html': 3.48.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/keycodes': 4.48.0 - '@wordpress/private-apis': 1.48.0 - colord: 2.9.3 - memize: 2.1.1 - react: 18.3.1 - '@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.161.6 @@ -26389,8 +25937,6 @@ snapshots: '@wordpress/style-runtime@0.2.0': {} - '@wordpress/style-runtime@0.4.0': {} - '@wordpress/stylelint-config@23.38.0(postcss@8.5.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(stylelint-scss@7.0.0(stylelint@17.7.0(typescript@5.9.3)))(stylelint@17.7.0(typescript@5.9.3))': dependencies: '@stylistic/stylelint-plugin': 5.1.0(stylelint@17.7.0(typescript@5.9.3)) @@ -26438,16 +25984,6 @@ snapshots: optionalDependencies: stylelint: 17.7.0(typescript@5.9.3) - '@wordpress/theme@0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@wordpress/element': 8.0.0 - '@wordpress/private-apis': 1.48.0 - '@wordpress/style-runtime': 0.4.0 - colorjs.io: 0.6.1 - memize: 2.1.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - '@wordpress/token-list@3.46.0': {} '@wordpress/ui@0.13.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -26496,60 +26032,10 @@ snapshots: - '@types/react' - stylelint - '@wordpress/ui@0.15.0(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(@types/react@18.3.28)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@date-fns/tz': 1.4.1 - '@wordpress/a11y': 4.48.0 - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/element': 8.0.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/icons': 13.3.0(react@18.3.1) - '@wordpress/keycodes': 4.48.0 - '@wordpress/primitives': 4.48.0(react@18.3.1) - '@wordpress/private-apis': 1.48.0 - '@wordpress/style-runtime': 0.4.0 - '@wordpress/theme': 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - clsx: 2.1.1 - date-fns: 4.1.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tabbable: 6.4.0 - transitivePeerDependencies: - - '@types/react' - - stylelint - - '@wordpress/ui@0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@base-ui/react': 1.4.1(@date-fns/tz@1.4.1)(date-fns@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@date-fns/tz': 1.4.1 - '@wordpress/a11y': 4.48.0 - '@wordpress/compose': 8.1.0(react@18.3.1) - '@wordpress/element': 8.0.0 - '@wordpress/i18n': 6.21.0 - '@wordpress/icons': 13.3.0(react@18.3.1) - '@wordpress/keycodes': 4.48.0 - '@wordpress/primitives': 4.48.0(react@18.3.1) - '@wordpress/private-apis': 1.48.0 - '@wordpress/style-runtime': 0.4.0 - '@wordpress/theme': 0.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - clsx: 2.1.1 - date-fns: 4.1.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - tabbable: 6.4.0 - transitivePeerDependencies: - - '@types/react' - - stylelint - '@wordpress/undo-manager@1.46.0': dependencies: '@wordpress/is-shallow-equal': 5.46.0 - '@wordpress/undo-manager@1.48.0': - dependencies: - '@wordpress/is-shallow-equal': 5.48.0 - '@wordpress/upload-media@0.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@wordpress/blob': 4.46.0 @@ -27772,8 +27258,6 @@ snapshots: csp_evaluator@1.1.5: {} - css-color-keywords@1.0.0: {} - css-declaration-sorter@7.4.0(postcss@8.5.14): dependencies: postcss: 8.5.14 @@ -27819,12 +27303,6 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 - css-to-react-native@3.2.0: - dependencies: - camelize: 1.0.1 - css-color-keywords: 1.0.0 - postcss-value-parser: 4.2.0 - css-tree@2.2.1: dependencies: mdn-data: 2.0.28 diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index 1d2dd43892d8..d8c3a896dd27 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -42,7 +42,7 @@ "@wordpress/compose": "7.46.0", "@wordpress/core-data": "7.46.0", "@wordpress/data": "10.46.0", - "@wordpress/dataviews": "16.0.0", + "@wordpress/dataviews": "14.3.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^13.0.0", "@wordpress/primitives": "4.46.0", diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/package.json b/projects/packages/premium-analytics/packages/widgets-toolkit/package.json index 44af5e35cf4c..958f54b7b530 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/package.json +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/package.json @@ -18,7 +18,7 @@ "@jetpack-premium-analytics/ui": "workspace:*", "@wordpress/components": "33.1.0", "@wordpress/compose": "7.46.0", - "@wordpress/dataviews": "16.0.0", + "@wordpress/dataviews": "14.3.0", "@wordpress/i18n": "^6.9.0", "@wordpress/icons": "^13.0.0", "@wordpress/route": "0.12.0", From eb88cad374822e5837abb2c4d94ae5adedd0b71e Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 10 Jun 2026 16:35:00 +0900 Subject: [PATCH 32/32] feat(premium-analytics): add Top posts & pages stats widget Port the Jetpack Stats "Top posts & pages" card as the jpa/stats-top-posts dashboard widget (WOOA7S-1489): - widgets/top-posts: framed list widget rendering the leaderboard chart with post-title links, hardcoded period/date/num defaults for v1 - data package: useReportTopPosts hook (useQuery directly, no comparison), top-posts fetcher against the existing jetpack/v4/stats-app proxy, and getJpaConfig() reading the new window.jpaConfig boot config - Config_Data PHP class emits window.jpaConfig (siteId, apiRoot, nonce) before the boot script on both generated page variants Co-Authored-By: Claude Fable 5 --- pnpm-lock.yaml | 3 + .../wooa7s-1489-port-stats-top-posts-widget | 4 + .../packages/premium-analytics/package.json | 1 + .../packages/data/src/api/constants.ts | 19 +++ .../packages/data/src/api/index.ts | 7 + .../__tests__/report-top-posts-fetch.test.ts | 52 +++++++ .../src/api/report-top-posts-fetch/index.ts | 7 + .../report-top-posts-fetch.ts | 73 +++++++++ .../packages/data/src/hooks/index.ts | 1 + .../data/src/hooks/use-report-top-posts.ts | 142 ++++++++++++++++++ .../packages/data/src/index.ts | 7 + .../packages/data/src/utils/jpa-config.ts | 40 +++++ .../widgets-toolkit/src/components/index.ts | 1 + .../packages/widgets-toolkit/src/index.ts | 1 + .../premium-analytics/src/class-analytics.php | 4 + .../src/class-config-data.php | 93 ++++++++++++ .../premium-analytics/tests/jest.config.cjs | 4 + .../premium-analytics/tests/style-stub.cjs | 4 + .../packages/premium-analytics/tsconfig.json | 2 +- .../widgets/top-posts/package.json | 19 +++ .../widgets/top-posts/render.tsx | 31 ++++ .../widgets/top-posts/style.module.css | 13 ++ .../top-posts/tests/top-posts.test.tsx | 91 +++++++++++ .../widgets/top-posts/top-posts.tsx | 124 +++++++++++++++ .../widgets/top-posts/widget.json | 7 + .../widgets/top-posts/widget.ts | 42 ++++++ .../wooa7s-1489-port-stats-top-posts-widget | 5 + 27 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/__tests__/report-top-posts-fetch.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/report-top-posts-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-top-posts.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/jpa-config.ts create mode 100644 projects/packages/premium-analytics/src/class-config-data.php create mode 100644 projects/packages/premium-analytics/tests/style-stub.cjs create mode 100644 projects/packages/premium-analytics/widgets/top-posts/package.json create mode 100644 projects/packages/premium-analytics/widgets/top-posts/render.tsx create mode 100644 projects/packages/premium-analytics/widgets/top-posts/style.module.css create mode 100644 projects/packages/premium-analytics/widgets/top-posts/tests/top-posts.test.tsx create mode 100644 projects/packages/premium-analytics/widgets/top-posts/top-posts.tsx create mode 100644 projects/packages/premium-analytics/widgets/top-posts/widget.json create mode 100644 projects/packages/premium-analytics/widgets/top-posts/widget.ts create mode 100644 projects/plugins/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d4c70511a3a..755b48c3fe13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3954,6 +3954,9 @@ importers: '@testing-library/dom': specifier: 10.4.1 version: 10.4.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/jest': specifier: 30.0.0 version: 30.0.0 diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget b/projects/packages/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget new file mode 100644 index 000000000000..dbd3bed59b12 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add a Top posts & pages stats widget backed by the Jetpack Stats top-posts endpoint, with a useReportTopPosts data hook and a window.jpaConfig boot config emitter. diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json index d8c3a896dd27..14ee56081bd3 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -62,6 +62,7 @@ "@storybook/react": "10.3.6", "@tanstack/react-query-devtools": "5.90.2", "@testing-library/dom": "10.4.1", + "@testing-library/react": "16.3.2", "@types/jest": "30.0.0", "@typescript/native-preview": "7.0.0-dev.20260225.1", "@wordpress/base-styles": "8.0.0", diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts index eb04e41b4633..dfa2b51ed261 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/constants.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts @@ -1,4 +1,23 @@ +/** + * Internal dependencies + */ +import { getJpaConfig } from '../utils/jpa-config'; + /** * Constants for API endpoints */ export const reportsPath = '/wc/v3/woocommerce-analytics/proxy/reports'; + +/** + * Base path of the Jetpack Stats proxy REST API for the current site. + * + * A function rather than a constant because the site ID is only known at + * runtime (read from `window.jpaConfig`). + * + * @return The stats API base path, e.g. `/jetpack/v4/stats-app/sites/123/stats`. + */ +export function getStatsApiPath(): string { + const { siteId } = getJpaConfig(); + + return `/jetpack/v4/stats-app/sites/${ siteId }/stats`; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/index.ts b/projects/packages/premium-analytics/packages/data/src/api/index.ts index 771d51ddba7b..feaa5a0a60ce 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts @@ -41,5 +41,12 @@ export { fetchReportVisitors } from './report-visitors-fetch'; export { fetchReportVisitorsByLocation } from './report-visitors-by-location-fetch'; export { fetchReportBookings } from './report-bookings-fetch'; export { fetchReportSessionsByDevice } from './report-sessions-by-device-fetch'; +export { fetchReportTopPosts } from './report-top-posts-fetch'; +export type { + RequestReportTopPostsParams, + TopPostsPeriod, + TopPostsPostView, + TopPostsResponse, +} from './report-top-posts-fetch'; export { exportReport } from './report-export-fetch'; export type { ExportReportParams, ExportReportResponse } from './report-export-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/__tests__/report-top-posts-fetch.test.ts b/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/__tests__/report-top-posts-fetch.test.ts new file mode 100644 index 000000000000..301568e49e74 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/__tests__/report-top-posts-fetch.test.ts @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +/** + * Internal dependencies + */ +import { fetchReportTopPosts } from '../report-top-posts-fetch'; + +jest.mock( '@wordpress/api-fetch', () => jest.fn() ); + +const mockApiFetch = apiFetch as unknown as jest.Mock; + +describe( 'fetchReportTopPosts', () => { + beforeEach( () => { + mockApiFetch.mockReset(); + mockApiFetch.mockResolvedValue( { date: '2026-06-10', days: {} } ); + window.jpaConfig = { + siteId: 123, + apiRoot: 'https://example.com/wp-json/', + nonce: 'abc', + }; + } ); + + afterEach( () => { + delete window.jpaConfig; + } ); + + it( 'composes the stats proxy URL from the boot config site ID', async () => { + await fetchReportTopPosts( { period: 'day', date: '2026-06-10', num: 10 } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/jetpack/v4/stats-app/sites/123/stats/top-posts?period=day&date=2026-06-10&max=10', + } ); + } ); + + it( 'omits max when num is not provided', async () => { + await fetchReportTopPosts( { period: 'week', date: '2026-06-10' } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/jetpack/v4/stats-app/sites/123/stats/top-posts?period=week&date=2026-06-10', + } ); + } ); + + it( 'throws a clear error when the boot config is missing', async () => { + delete window.jpaConfig; + + await expect( fetchReportTopPosts( { period: 'day', date: '2026-06-10' } ) ).rejects.toThrow( + 'window.jpaConfig is not available' + ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/index.ts new file mode 100644 index 000000000000..3011256393a1 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/index.ts @@ -0,0 +1,7 @@ +export { fetchReportTopPosts } from './report-top-posts-fetch'; +export type { + RequestReportTopPostsParams, + TopPostsPeriod, + TopPostsPostView, + TopPostsResponse, +} from './report-top-posts-fetch'; diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/report-top-posts-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/report-top-posts-fetch.ts new file mode 100644 index 000000000000..3b7bb99e82de --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-top-posts-fetch/report-top-posts-fetch.ts @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +/** + * Internal dependencies + */ +import { getStatsApiPath } from '../constants'; + +export type TopPostsPeriod = 'day' | 'week' | 'month' | 'year'; + +export type TopPostsPostView = { + id: number; + href: string; + /** + * Publication date. `null` for pages without one (e.g. home, archives). + */ + date: string | null; + title: string; + type: string; + views: number; + video_play?: boolean; + public?: boolean; +}; + +type TopPostsBucket = { + postviews: TopPostsPostView[]; + total_views: number; +}; + +export type TopPostsResponse = { + date: string; + days: Record< string, TopPostsBucket >; + summary?: TopPostsBucket; +}; + +export type RequestReportTopPostsParams = { + period: TopPostsPeriod; + /** + * Reference date within the period, YYYY-MM-DD. + */ + date: string; + /** + * Maximum number of posts to return. + */ + num?: number; +}; + +/** + * Fetch the top-viewed posts/pages via the Jetpack Stats proxy + * (`jetpack/v4/stats-app`, provided by the jetpack-stats-admin package). + * + * @param params - Request parameters. + * @param params.period - Stats period granularity. + * @param params.date - Reference date within the period, YYYY-MM-DD. + * @param params.num - Maximum number of posts to return. + */ +export async function fetchReportTopPosts( { + period, + date, + num, +}: RequestReportTopPostsParams ): Promise< TopPostsResponse > { + // In the WPCOM stats API `max` caps the number of posts per period, while + // `num` counts periods — leave `num` at its server default of 1. + const path = addQueryArgs( `${ getStatsApiPath() }/top-posts`, { + period, + date, + max: num, + } ); + + return apiFetch( { path } ) as Promise< TopPostsResponse >; +} diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/index.ts b/projects/packages/premium-analytics/packages/data/src/hooks/index.ts index ceb26db69eed..5fa16b2b37ac 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/index.ts @@ -5,6 +5,7 @@ export { useReportCouponsByDate } from './use-report-coupons-by-date'; export { useReportCustomers } from './use-report-customers'; export { useReportConversionRate } from './use-report-conversion-rate'; export { useReportBookings } from './use-report-bookings'; +export { useReportTopPosts } from './use-report-top-posts'; /** * @deprecated Use individual hooks instead: useReportOrders, useReportOrderAttribution, useReportCoupons diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-top-posts.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-top-posts.ts new file mode 100644 index 000000000000..15555ad63acc --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-top-posts.ts @@ -0,0 +1,142 @@ +/** + * External dependencies + */ +import { useQuery } from '@tanstack/react-query'; +import { format, parseISO, startOfMonth, startOfWeek, startOfYear } from 'date-fns'; +import { useMemo } from 'react'; +/** + * Internal dependencies + */ +import { fetchReportTopPosts } from '../api/report-top-posts-fetch'; +import type { TopPostsPeriod, TopPostsResponse } from '../api/report-top-posts-fetch'; + +export type UseReportTopPostsParams = { + period: TopPostsPeriod; + /** + * Reference date within the period, YYYY-MM-DD. + */ + date: string; + /** + * Maximum number of posts to return. + */ + num?: number; + /** + * Filter kind. Only `postType` is supported in v1. + */ + kind?: 'postType'; + /** + * Post type(s) to keep, e.g. `'post'`, `'page'`, `[ 'post', 'page' ]`. + * When undefined, no filtering is applied. + */ + name?: string | string[]; +}; + +type UseReportTopPostsOptions = { + enabled?: boolean; +}; + +export type TopPostRow = { + label: string; + value: number; + href: string; + type: string; +}; + +/** + * Compute the start date of the stats period containing `date`, which is the + * key the WPCOM stats API uses for the matching bucket in `days`. Port of + * Calypso's `rangeOfPeriod` (wp-calypso `client/state/stats/lists/utils.js`), + * start side only. + * + * @param period - Stats period granularity. + * @param date - Reference date, YYYY-MM-DD. + */ +function periodStartDate( period: TopPostsPeriod, date: string ): string { + const parsed = parseISO( date ); + + switch ( period ) { + case 'week': + // WPCOM stats weeks run Monday through Sunday. + return format( startOfWeek( parsed, { weekStartsOn: 1 } ), 'yyyy-MM-dd' ); + case 'month': + return format( startOfMonth( parsed ), 'yyyy-MM-dd' ); + case 'year': + return format( startOfYear( parsed ), 'yyyy-MM-dd' ); + case 'day': + default: + return date; + } +} + +/** + * Normalize a WPCOM top-posts response into flat rows. Port of Calypso's + * `statsTopPosts` normalizer (wp-calypso `client/state/stats/lists/utils.js`) + * minus the Calypso UI fields. + * + * @param response - Raw top-posts response. + * @param period - Stats period granularity used in the request. + * @param date - Reference date used in the request, YYYY-MM-DD. + */ +function normalizeTopPosts( + response: TopPostsResponse, + period: TopPostsPeriod, + date: string +): TopPostRow[] { + const bucket = response.days?.[ periodStartDate( period, date ) ]; + + return ( bucket?.postviews ?? [] ).map( item => ( { + label: item.title, + value: item.views, + href: item.href, + type: item.type, + } ) ); +} + +/** + * Fetch the top-viewed posts/pages for the site. + * + * Unlike the WooCommerce report hooks this uses `useQuery` directly — stats + * has no comparison-period concept in v1, so `useReport` does not apply. + * + * @param params - Report parameters. + * @param options - Optional configuration. + * @return The react-query result fields plus `rows`, the normalized + * `{ label, value, href, type }` rows filtered by `params.name`. + */ +export function useReportTopPosts( + params: UseReportTopPostsParams, + options?: UseReportTopPostsOptions +) { + const { period, date, num, name } = params; + + const query = useQuery( { + queryKey: [ 'stats', 'top-posts', period, date, num ?? null ], + queryFn: () => fetchReportTopPosts( { period, date, num } ), + enabled: options?.enabled ?? true, + } ); + + const nameKey = Array.isArray( name ) ? name.join( ',' ) : name; + const { data } = query; + + const rows = useMemo( () => { + if ( ! data ) { + return []; + } + + const allowedTypes = nameKey === undefined ? null : nameKey.split( ',' ); + + return normalizeTopPosts( data, period, date ).filter( + row => ! allowedTypes || allowedTypes.includes( row.type ) + ); + }, [ data, period, date, nameKey ] ); + + return { + data: query.data, + isLoading: query.isLoading, + isFetching: query.isFetching, + isError: query.isError, + error: query.error, + refetch: query.refetch, + rows, + }; +} diff --git a/projects/packages/premium-analytics/packages/data/src/index.ts b/projects/packages/premium-analytics/packages/data/src/index.ts index 6f02580c031e..175c5ed70585 100644 --- a/projects/packages/premium-analytics/packages/data/src/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/index.ts @@ -14,6 +14,13 @@ export { useReportVisitors } from './hooks/use-report-visitors'; export { useReportVisitorsByLocation } from './hooks/use-report-visitors-by-location'; export { useReportBookings } from './hooks/use-report-bookings'; export { useReportSessionsByDevice } from './hooks/use-report-sessions-by-device'; +export { + useReportTopPosts, + type UseReportTopPostsParams, + type TopPostRow, +} from './hooks/use-report-top-posts'; +export { getJpaConfig, type JpaConfig } from './utils/jpa-config'; +export type { TopPostsPeriod, TopPostsResponse } from './api'; export { prefetchReport } from './prefetch'; export { normalizeReportParams, diff --git a/projects/packages/premium-analytics/packages/data/src/utils/jpa-config.ts b/projects/packages/premium-analytics/packages/data/src/utils/jpa-config.ts new file mode 100644 index 000000000000..8673ddd2aa93 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/jpa-config.ts @@ -0,0 +1,40 @@ +export type JpaConfig = { + /** + * WordPress.com blog ID of the connected site. + */ + siteId: number; + + /** + * REST API root URL, e.g. `https://example.com/wp-json/`. + */ + apiRoot: string; + + /** + * REST API nonce (`wp_rest`). + */ + nonce: string; +}; + +declare global { + interface Window { + jpaConfig?: JpaConfig; + } +} + +/** + * Read the boot configuration emitted by the PHP `Config_Data` class as + * `window.jpaConfig` ahead of the boot script on the Premium Analytics admin + * page. + * + * @return The boot configuration. + * @throws {Error} If called outside a browser context or before the config is emitted. + */ +export function getJpaConfig(): JpaConfig { + if ( typeof window === 'undefined' || ! window.jpaConfig ) { + throw new Error( + 'window.jpaConfig is not available. It is emitted by the Config_Data PHP class on the Premium Analytics admin page; outside that page (or in tests) it must be stubbed.' + ); + } + + return window.jpaConfig; +} diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts index 121c13e7227b..40aa6c4178e4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts @@ -18,3 +18,4 @@ export { } from './chart-leaderboard'; export { BarChart, type BarChartProps, type BarChartData, type BarChartStyle } from './chart-bar'; export { ChartEmptyState, type ChartEmptyStateProps } from './chart-empty-state'; +export { WidgetLoadingOverlay } from './widget-loading-overlay'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts index 9b561dd55f5b..87259b94517c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts @@ -20,6 +20,7 @@ export { type BarChartProps, type BarChartData, type BarChartStyle, + WidgetLoadingOverlay, } from './components'; /** diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index e3cc93a370e7..62aece91526a 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -56,6 +56,10 @@ public static function init( $options = array() ) { require_once $build_entry; } + // Emit `window.jpaConfig` (site ID, REST root, nonce) ahead of the boot script. + require_once __DIR__ . '/class-config-data.php'; + Config_Data::init(); + add_action( 'admin_menu', array( static::class, 'register_admin_menu' ) ); add_action( 'jetpack-premium-analytics_init', array( static::class, 'register_sidebar_items' ) ); } diff --git a/projects/packages/premium-analytics/src/class-config-data.php b/projects/packages/premium-analytics/src/class-config-data.php new file mode 100644 index 000000000000..6ff39faa0685 --- /dev/null +++ b/projects/packages/premium-analytics/src/class-config-data.php @@ -0,0 +1,93 @@ + $site_id, + 'apiRoot' => esc_url_raw( rest_url() ), + 'nonce' => wp_create_nonce( 'wp_rest' ), + ); + } +} diff --git a/projects/packages/premium-analytics/tests/jest.config.cjs b/projects/packages/premium-analytics/tests/jest.config.cjs index 621d39dcc87a..ac15edcd2b5e 100644 --- a/projects/packages/premium-analytics/tests/jest.config.cjs +++ b/projects/packages/premium-analytics/tests/jest.config.cjs @@ -9,5 +9,9 @@ module.exports = { // Resolve internal `packages/*` imports to their TypeScript source, // mirroring the tsconfig `paths` alias (see README → "Internal packages"). '^@jetpack-premium-analytics/(.*)$': path.join( __dirname, '..', 'packages', '$1', 'src' ), + // Plain CSS shipped by node_modules packages is not transformed by the + // base config (its asset stub only covers first-party files), so stub + // it out here. + '^@automattic/(ui|charts)/style\\.css$': path.join( __dirname, 'style-stub.cjs' ), }, }; diff --git a/projects/packages/premium-analytics/tests/style-stub.cjs b/projects/packages/premium-analytics/tests/style-stub.cjs new file mode 100644 index 000000000000..3b3cbb70317f --- /dev/null +++ b/projects/packages/premium-analytics/tests/style-stub.cjs @@ -0,0 +1,4 @@ +// Empty stand-in for plain CSS imports from node_modules packages +// (e.g. `@automattic/ui/style.css`), which jest would otherwise try to +// parse as JavaScript. +module.exports = {}; diff --git a/projects/packages/premium-analytics/tsconfig.json b/projects/packages/premium-analytics/tsconfig.json index ec28701d0d80..12425d0a415c 100644 --- a/projects/packages/premium-analytics/tsconfig.json +++ b/projects/packages/premium-analytics/tsconfig.json @@ -9,5 +9,5 @@ "@jetpack-premium-analytics/*": [ "./packages/*/src" ] } }, - "include": [ "routes/**/*", "packages/**/*" ] + "include": [ "routes/**/*", "packages/**/*", "widgets/**/*" ] } diff --git a/projects/packages/premium-analytics/widgets/top-posts/package.json b/projects/packages/premium-analytics/widgets/top-posts/package.json new file mode 100644 index 000000000000..702fd8b7d6b9 --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@automattic/jetpack-premium-analytics-widget-top-posts", + "version": "0.1.0-alpha", + "private": true, + "type": "module", + "dependencies": { + "@jetpack-premium-analytics/data": "workspace:*", + "@jetpack-premium-analytics/widgets-toolkit": "workspace:*", + "@wordpress/i18n": "^6.9.0", + "@wordpress/icons": "^13.0.0", + "@wordpress/ui": "0.13.0", + "date-fns": "4.1.0", + "react": "18.3.1" + }, + "devDependencies": { + "@testing-library/react": "16.3.2", + "@wordpress/api-fetch": "7.46.0" + } +} diff --git a/projects/packages/premium-analytics/widgets/top-posts/render.tsx b/projects/packages/premium-analytics/widgets/top-posts/render.tsx new file mode 100644 index 000000000000..aa32d4d96ad4 --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/render.tsx @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { WidgetRoot } from '@jetpack-premium-analytics/widgets-toolkit'; +/** + * Internal dependencies + */ +import { TopPosts, type TopPostsAttributes } from './top-posts'; + +type TopPostsRenderProps = { + attributes?: TopPostsAttributes; +}; + +/** + * Widget render entry point. + * + * Attributes flow to the inner component via props rather than + * `WidgetRootContext` — the context's report params are WC-Analytics-shaped + * and do not fit stats queries. + * + * @param props - Render props. + * @param props.attributes - Widget attributes. + * @return The rendered widget. + */ +export default function TopPostsWidget( { attributes }: TopPostsRenderProps ) { + return ( + + + + ); +} diff --git a/projects/packages/premium-analytics/widgets/top-posts/style.module.css b/projects/packages/premium-analytics/widgets/top-posts/style.module.css new file mode 100644 index 000000000000..921015676957 --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/style.module.css @@ -0,0 +1,13 @@ +.labelLink { + display: block; + overflow: hidden; + color: inherit; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.labelLink:hover, +.labelLink:focus { + text-decoration: underline; +} diff --git a/projects/packages/premium-analytics/widgets/top-posts/tests/top-posts.test.tsx b/projects/packages/premium-analytics/widgets/top-posts/tests/top-posts.test.tsx new file mode 100644 index 000000000000..3db79aeb3d8c --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/tests/top-posts.test.tsx @@ -0,0 +1,91 @@ +/** + * External dependencies + */ +import { queryClient } from '@jetpack-premium-analytics/data'; +import { render, screen } from '@testing-library/react'; +import apiFetch from '@wordpress/api-fetch'; +/** + * Internal dependencies + */ +import TopPostsWidget from '../render'; + +jest.mock( '@wordpress/api-fetch', () => jest.fn() ); + +// WidgetRoot reads URL search params as a fallback for report params; outside +// a matched route the real hook warns and throws. +jest.mock( '@wordpress/route', () => ( { + useSearch: () => ( {} ), +} ) ); + +const mockApiFetch = apiFetch as unknown as jest.Mock; + +const TOP_POSTS_RESPONSE = { + date: '2026-06-10', + days: { + '2026-06-10': { + postviews: [ + { + id: 1, + href: 'https://example.com/hello-world/', + date: '2026-06-01', + title: 'Hello World Post', + type: 'post', + views: 42, + }, + { + id: 2, + href: 'https://example.com/about/', + date: null, + title: 'About Page', + type: 'page', + views: 7, + }, + ], + total_views: 49, + }, + }, +}; + +describe( 'TopPostsWidget', () => { + beforeEach( () => { + // The data package's query client is a module-level singleton; drop its + // cache so each test starts from a fresh fetch. + queryClient.clear(); + mockApiFetch.mockReset(); + mockApiFetch.mockResolvedValue( TOP_POSTS_RESPONSE ); + window.jpaConfig = { + siteId: 123, + apiRoot: 'https://example.com/wp-json/', + nonce: 'abc', + }; + } ); + + afterEach( () => { + delete window.jpaConfig; + } ); + + it( 'renders the fetched top posts as links', async () => { + render( ); + + const link = await screen.findByRole( 'link', { name: 'Hello World Post' } ); + expect( link ).toHaveAttribute( 'href', 'https://example.com/hello-world/' ); + expect( screen.getByText( 'About Page' ) ).toBeInTheDocument(); + } ); + + it( 'filters rows by post type when the name attribute is set', async () => { + render( + + ); + + await expect( screen.findByText( 'About Page' ) ).resolves.toBeInTheDocument(); + expect( screen.queryByText( 'Hello World Post' ) ).not.toBeInTheDocument(); + } ); + + it( 'renders the empty state when there are no views', async () => { + mockApiFetch.mockResolvedValue( { date: '2026-06-10', days: {} } ); + + render( ); + + await expect( screen.findByText( 'No views in this period.' ) ).resolves.toBeInTheDocument(); + } ); +} ); diff --git a/projects/packages/premium-analytics/widgets/top-posts/top-posts.tsx b/projects/packages/premium-analytics/widgets/top-posts/top-posts.tsx new file mode 100644 index 000000000000..151597b624a1 --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/top-posts.tsx @@ -0,0 +1,124 @@ +/** + * External dependencies + */ +import { localTZDate, useReportTopPosts } from '@jetpack-premium-analytics/data'; +import { + LeaderboardChart, + WidgetLoadingOverlay, + type LeaderboardChartData, +} from '@jetpack-premium-analytics/widgets-toolkit'; +import { __ } from '@wordpress/i18n'; +import { Text } from '@wordpress/ui'; +import { format } from 'date-fns'; +import { useMemo } from 'react'; +/** + * Internal dependencies + */ +import styles from './style.module.css'; +import type { TopPostRow, UseReportTopPostsParams } from '@jetpack-premium-analytics/data'; + +export type TopPostsAttributes = { + period?: UseReportTopPostsParams[ 'period' ]; + /** + * Reference date, YYYY-MM-DD. Defaults to today in the site timezone. + */ + date?: string; + num?: number; + /** + * Post type(s) to keep. When undefined, all types are shown. + */ + name?: string | string[]; +}; + +type TopPostsProps = { + attributes?: TopPostsAttributes; +}; + +/** + * Leaderboard row label: the post title linking to the published post. + * + * @param props - Component props. + * @param props.label - Post title. + * @param props.href - Post URL. + * @return The label element. + */ +function TopPostLabel( { label, href }: { label: string; href: string } ) { + return ( + + { label } + + ); +} + +/** + * Map normalized top-posts rows to leaderboard entries. Bars scale relative + * to the most-viewed entry; there is no comparison period in v1. + * + * @param rows - Normalized top-posts rows. + * @return Leaderboard chart entries. + */ +function buildLeaderboardData( rows: TopPostRow[] ): LeaderboardChartData { + const maxViews = Math.max( ...rows.map( row => row.value ), 0 ); + + return rows.map( ( row, index ) => ( { + id: `${ index }-${ row.href }`, + label: , + currentValue: row.value, + previousValue: 0, + currentShare: maxViews > 0 ? ( row.value / maxViews ) * 100 : 0, + previousShare: 0, + delta: 0, + } ) ); +} + +/** + * Top posts & pages list. + * + * Attributes arrive via props, not WidgetRootContext — the context's report + * params are WC-Analytics-shaped and do not fit stats queries. + * + * @param props - Component props. + * @param props.attributes - Widget attributes. + * @return The widget content. + */ +export function TopPosts( { attributes }: TopPostsProps ) { + const period = attributes?.period ?? 'day'; + const date = attributes?.date ?? format( localTZDate(), 'yyyy-MM-dd' ); + const num = attributes?.num ?? 10; + + const { rows, isLoading, isError } = useReportTopPosts( { + period, + date, + num, + name: attributes?.name, + } ); + + const data = useMemo( () => buildLeaderboardData( rows ), [ rows ] ); + + if ( isError ) { + return { __( 'Unable to load top posts.', 'jetpack-premium-analytics' ) }; + } + + if ( isLoading ) { + return ; + } + + if ( data.length === 0 ) { + return { __( 'No views in this period.', 'jetpack-premium-analytics' ) }; + } + + return ( + + ); +} diff --git a/projects/packages/premium-analytics/widgets/top-posts/widget.json b/projects/packages/premium-analytics/widgets/top-posts/widget.json new file mode 100644 index 000000000000..f35d6ddf9d3e --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/widget.json @@ -0,0 +1,7 @@ +{ + "name": "jpa/stats-top-posts", + "title": "Top posts & pages", + "description": "Your most viewed posts and pages.", + "category": "stats", + "presentation": "framed" +} diff --git a/projects/packages/premium-analytics/widgets/top-posts/widget.ts b/projects/packages/premium-analytics/widgets/top-posts/widget.ts new file mode 100644 index 000000000000..d9ac36ce6c21 --- /dev/null +++ b/projects/packages/premium-analytics/widgets/top-posts/widget.ts @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +import { chartBar } from '@wordpress/icons'; + +/** + * Widget type definition. + * + * `example.attributes` doubles as the defaults applied to new instances: + * one day of stats, ten posts, all post types. The reference date is + * deliberately absent — it is computed at render time so the widget always + * shows "today" (see top-posts.tsx). + */ +export default { + name: 'jpa/stats-top-posts', + title: 'Top posts & pages', + icon: chartBar, + presentation: 'framed', + attributes: [ + { + id: 'period', + label: 'Period', + type: 'text', + }, + { + id: 'date', + label: 'Date', + type: 'text', + }, + { + id: 'num', + label: 'Number of results', + type: 'integer', + }, + ], + example: { + attributes: { + period: 'day', + num: 10, + }, + }, +}; diff --git a/projects/plugins/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget b/projects/plugins/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget new file mode 100644 index 000000000000..f432cece757a --- /dev/null +++ b/projects/plugins/premium-analytics/changelog/wooa7s-1489-port-stats-top-posts-widget @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Premium Analytics package version bump only; no plugin-side code changes. + +