Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions projects/packages/premium-analytics/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
}
);
138 changes: 138 additions & 0 deletions projects/packages/premium-analytics/packages/routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# @automattic/jetpack-premium-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 '@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
} );
};
}
```

### Comparison Parameter Management

```typescript
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,
} );
};
}
```

## 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
14 changes: 14 additions & 0 deletions projects/packages/premium-analytics/packages/routing/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "@automattic/jetpack-premium-analytics-routing",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"sideEffects": false,
"dependencies": {
"@jetpack-premium-analytics/data": "workspace:*",
"@jetpack-premium-analytics/datetime": "workspace:*",
"@wordpress/route": "0.12.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useStagedSearch } from './use-staged-search';
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# `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 '@jetpack-premium-analytics/routing';
import { localTZDate } from '@jetpack-premium-analytics/data';
import type { DateRange } from '@jetpack-premium-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.

---
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useStagedSearch } from './use-staged-search';
Loading
Loading