From 683a9208b53daad49c58d88968d0209b827ec34f Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:31:20 +0800 Subject: [PATCH 1/4] 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 73213e7c46be8c6a4b46a1deb0a67b352d4fea4a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 16:41:21 +0800 Subject: [PATCH 2/4] 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..5b5a9dce2790 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 +# @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 '@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 aafa41549847bd1588a0c9f4fbe9dd6e8f2f9041 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 16:41:52 +0800 Subject: [PATCH 3/4] style(premium-analytics): apply jetpack prettier and eslint fixes to routing package --- .../packages/routing/README.md | 51 ++++++------ .../src/hooks/use-staged-search/README.md | 77 +++++++++---------- .../use-staged-search/use-staged-search.tsx | 46 ++++------- .../comparison/derive-comparison-range.ts | 8 +- .../src/search/date-range/date-range.ts | 22 ++---- 5 files changed, 85 insertions(+), 119 deletions(-) diff --git a/projects/packages/premium-analytics/packages/routing/README.md b/projects/packages/premium-analytics/packages/routing/README.md index 5b5a9dce2790..bf6b61d9823e 100644 --- a/projects/packages/premium-analytics/packages/routing/README.md +++ b/projects/packages/premium-analytics/packages/routing/README.md @@ -31,16 +31,16 @@ import { writeDateRangeToSearch } from '@jetpack-premium-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 - } ); - }; + const navigate = useNavigate(); + + const handleRangeChange = nextRange => { + writeDateRangeToSearch( { + navigate, + to: '/wc-analytics/dashboard', + range: nextRange, + search: { interval: 'day' }, // Preserve other params + } ); + }; } ``` @@ -50,17 +50,17 @@ function DateRangeSelector() { import { writeComparisonToSearch } from '@jetpack-premium-analytics/routing'; function ComparisonSelector() { - const navigate = useNavigate(); - - const handleComparisonChange = ( range, presetId ) => { - writeComparisonToSearch( { - navigate, - to: '/wc-analytics/dashboard', - range, - presetId, - enabled: !!range - } ); - }; + const navigate = useNavigate(); + + const handleComparisonChange = ( range, presetId ) => { + writeComparisonToSearch( { + navigate, + to: '/wc-analytics/dashboard', + range, + presetId, + enabled: !! range, + } ); + }; } ``` @@ -71,13 +71,15 @@ function ComparisonSelector() { 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 +- **`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 @@ -86,6 +88,7 @@ Writes a `DateRange` to the URL using the provided `navigate` function. 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 @@ -95,6 +98,7 @@ Writes comparison parameters to the URL for period-over-period analysis. - **`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 @@ -105,6 +109,7 @@ Writes comparison parameters to the URL for period-over-period analysis. 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 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..4fb040e06e24 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 @@ -8,15 +8,15 @@ 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`). +- **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. +- `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. --- @@ -24,16 +24,16 @@ Atomic commit: ```ts type UseStagedSearchOptions< TFrom extends string > = { - from: TFrom; // TanStack route id/path + 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 + 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; @@ -43,8 +43,8 @@ type UseStagedSearchReturn< TSearch > = { Notes: -* Internally uses `useSearch( { from } )` and `useNavigate( { from } )`. -* No `to` is passed on commit, so the current route is preserved. +- Internally uses `useSearch( { from } )` and `useNavigate( { from } )`. +- No `to` is passed on commit, so the current route is preserved. --- @@ -52,10 +52,7 @@ Notes: ```tsx import { useMemo, useCallback } from 'react'; -import { - useStagedSearch, - encodeDateToSearchParam, -} from '@jetpack-premium-analytics/routing'; +import { useStagedSearch, encodeDateToSearchParam } from '@jetpack-premium-analytics/routing'; import { localTZDate } from '@jetpack-premium-analytics/data'; import type { DateRange } from '@jetpack-premium-analytics/datetime'; @@ -67,18 +64,18 @@ type Search = { }; export function DashboardHeader() { - const { effective, stage, commit } = useStagedSearch< - Search, - '/wc-analytics/dashboard' - >( { + 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 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 ) => { @@ -104,16 +101,16 @@ export function DashboardHeader() { **What to use when** -* Render and fetch: **`effective`** -* Inputs being edited: **`staged`** -* URL-driven side effects / analytics / share links: **`committed`** +- 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. +- 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** @@ -131,19 +128,19 @@ const query = useQuery( { **Debounce guidance** -* `autoCommitDebounceMs`: 200–300 ms works well for date pickers. -* During edits → debounced replace-commits. -* On confirm (Apply/close) → `commit( { replace: false } )`. +- `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 +- `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. +- 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/use-staged-search.tsx b/projects/packages/premium-analytics/packages/routing/src/hooks/use-staged-search/use-staged-search.tsx index 419a403f9216..bcfc24e7d504 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 @@ -84,10 +84,7 @@ function shallowEqual( a: AnyObject, b: AnyObject ) { return true; } -function mergeDefined< T extends AnyObject >( - base: T, - patch: Partial< T > -): T { +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 +95,9 @@ function mergeDefined< T extends AnyObject >( return out as T; } -export function useStagedSearch< - TSearch extends AnyObject, - TFrom extends string, ->( opts: UseStagedSearchOptions< TFrom > ): UseStagedSearchReturn< TSearch > { +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 +156,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 +164,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 +198,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 +223,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..421098bb12e0 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': 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..3bb31a216d42 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,16 @@ /** * 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. */ -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 +18,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; @@ -72,9 +62,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; From e93fdb67f7de09ec632f02089201c3c5e3f66bc2 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 16:42:00 +0800 Subject: [PATCH 4/4] chore(premium-analytics): add routing lint overrides and changelog entry --- ...7s-1317-integrate-routing-package-into-analytics | 4 ++++ .../packages/premium-analytics/eslint.config.mjs | 13 +++++++++++++ 2 files changed, 17 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. diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs index 7016c1fa7907..352e814ffb97 100644 --- a/projects/packages/premium-analytics/eslint.config.mjs +++ b/projects/packages/premium-analytics/eslint.config.mjs @@ -44,5 +44,18 @@ export default defineConfig( '@typescript-eslint/no-explicit-any': 'off', 'import/no-extraneous-dependencies': 'off', }, + }, + { + // The routing port also 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: { + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/check-indentation': 'off', + 'import/no-extraneous-dependencies': 'off', + }, } );