Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | 90x 666x 2x 664x 664x 5312x 1598x 664x 90x 28x 28x 28x 28x 2x 28x 2x 28x 3x 28x 28x 60x 56x 28x 19x 1x 18x 17x 1x 1x 4x 4x 32x 32x 3x 4x 369x 369x 2952x 2952x 35x 369x 282x 148x 90x 3x 1x 2x 2x 8x 2x 10x 10x 80x 10x 1444x 1444x 1444x 120x 1324x 143x 143x 145x 143x 87x 87x 56x | /**
* External dependencies
*/
import { hasComparisonEnabled, normalizeReportParams } from '@jetpack-premium-analytics/data';
import {
PRESET_ALL_TIME,
isComparisonPresetId,
isSelectablePreset,
isYearPresetId,
} from '@jetpack-premium-analytics/datetime';
/**
* Internal dependencies
*/
import { deriveComparisonRange } from '../comparison';
import { DASHBOARD_ORIGIN_PARAM, pickDashboardOriginParams } from '../dashboard-origin';
type NormalizeInput = NonNullable< Parameters< typeof normalizeReportParams >[ 0 ] >;
/**
* The URL search params that describe the shared report window (date range,
* interval, and comparison) — the state every analytics surface has in common.
*
* Page-owned params such as `post_id`, `section`, and the report chart's
* `period` are deliberately excluded, so this set is safe to carry between
* routes without leaking one page's state onto another.
*/
export const REPORT_DATE_PARAM_KEYS = [
'from',
'to',
'interval',
'preset',
'compare_from',
'compare_to',
'compare_preset',
'comp',
] as const;
/**
* Pick only the shared report-window params from a URL search object.
*
* Reads the page's own window, never page-scoped params like `post_id` or `section`.
* A link out of a page that may be a detail page uses `pickReportNavigationParams()`
* instead, which returns a detail page to the window it was opened from.
*
* @param search - The current route search params.
* @return A new object with only the shared report-window params that are set.
*/
export function pickReportDateParams(
search: Record< string, unknown > | undefined
): Record< string, unknown > {
if ( ! search ) {
return {};
}
const picked: Record< string, unknown > = {};
for ( const key of REPORT_DATE_PARAM_KEYS ) {
if ( search[ key ] !== undefined ) {
picked[ key ] = search[ key ];
}
}
return picked;
}
/**
* Prefix of the params a detail page keeps the linking page's window under, so
* the page can open on its own range while its way back restores the original.
*/
const ORIGIN_WINDOW_PREFIX = 'ref_';
/**
* Store the linking page's window under the origin-window params, keeping only
* what `expandOriginWindow()` cannot rebuild: a named range is its preset alone.
* All time keeps its bounds too, since its start is the linking surface's own. A
* linking page without a window stores its default preset, so the way back can
* tell an origin from none.
*
* @param search - The search params the detail page was linked with.
* @return The origin-window params.
*/
export function toReportOriginWindowParams(
search: Record< string, unknown > | undefined
): Record< string, unknown > {
const linked = pickReportDateParams( search ) as NormalizeInput;
const origin: Record< string, unknown > =
isSelectablePreset( linked.preset ) || isYearPresetId( linked.preset )
? { preset: linked.preset }
: {
from: linked.from,
to: linked.to,
...( linked.preset === PRESET_ALL_TIME ? { preset: PRESET_ALL_TIME } : {} ),
};
const rebuilt = normalizeReportParams( origin as NormalizeInput );
if ( linked.preset === undefined && linked.from === undefined && linked.to === undefined ) {
origin.preset = rebuilt.preset;
}
if ( linked.interval !== undefined && linked.interval !== rebuilt.interval ) {
origin.interval = linked.interval;
}
if ( hasComparisonEnabled( linked ) ) {
Object.assign(
origin,
isComparisonPresetId( linked.compare_preset )
? { compare_preset: linked.compare_preset }
: { compare_from: linked.compare_from, compare_to: linked.compare_to }
);
}
const stored: Record< string, unknown > = {};
for ( const [ key, value ] of Object.entries( origin ) ) {
if ( value !== undefined ) {
stored[ ORIGIN_WINDOW_PREFIX + key ] = value;
}
}
return stored;
}
/**
* Rebuild the comparison a stored origin window names by preset; the destination
* route seeds the rest of the window, as it does for any link that omits dates.
*
* @param origin - The unprefixed origin-window params.
* @return The report-window params to carry.
*/
function expandOriginWindow( origin: Record< string, unknown > ): Record< string, unknown > {
if ( origin.compare_from !== undefined ) {
return { ...origin, comp: '1' };
}
if ( origin.compare_preset === undefined ) {
return origin;
}
const comparison = deriveComparisonRange( {
...normalizeReportParams( origin as NormalizeInput ),
comp: '1',
compare_preset: origin.compare_preset,
} as NormalizeInput );
return comparison ? { ...origin, ...comparison, comp: '1' } : origin;
}
/**
* Pick the origin-window params out of a search object, still prefixed, so a
* detail route that allowlists its params can keep them across a seed.
*
* @param search - The current route search params.
* @return Only the origin-window params that are set.
*/
export function pickReportOriginWindowParams(
search: Record< string, unknown > | undefined
): Record< string, unknown > {
const picked: Record< string, unknown > = {};
for ( const key of REPORT_DATE_PARAM_KEYS ) {
const value = search?.[ ORIGIN_WINDOW_PREFIX + key ];
if ( value !== undefined ) {
picked[ ORIGIN_WINDOW_PREFIX + key ] = value;
}
}
return picked;
}
/**
* Pick the window a link out of the current page returns to: on a page opened from
* another, the origin window its seed stored under the `ref_*` params, whichever
* param scopes the page; elsewhere, the page's own.
*
* @param search - The current route search params.
* @return The unprefixed report-window params to carry.
*/
function pickReturnDateParams(
search: Record< string, unknown > | undefined
): Record< string, unknown > {
const origin: Record< string, unknown > = {};
for ( const key of REPORT_DATE_PARAM_KEYS ) {
const value = search?.[ ORIGIN_WINDOW_PREFIX + key ];
if ( value !== undefined ) {
origin[ key ] = value;
}
}
return Object.keys( origin ).length
? expandOriginWindow( origin )
: pickReportDateParams( search );
}
/**
* Pick the params a link to another analytics route carries forward: the
* report window to return to plus the dashboard tab.
*
* @param search - The current route search params.
* @return A new object with the carried params that are set.
*/
export function pickReportNavigationParams(
search: Record< string, unknown > | undefined
): Record< string, unknown > {
return { ...pickReturnDateParams( search ), ...pickDashboardOriginParams( search ) };
}
/**
* The params a date picker edits, as opposed to the ones its neighbours own.
*/
type PrimaryDateParams = { from?: string; to?: string; preset?: string };
/**
* Whether the primary date picker holds an edit the store has not taken yet.
*
* The comparison and interval controls commit on their own, so both ask this
* first rather than committing a range draft along with their own change.
*
* @param applied - The window the widgets are querying with.
* @param draft - The window the picker is holding.
* @return Whether the two describe a different window.
*/
export function hasPrimaryDateDraft(
applied: PrimaryDateParams | undefined,
draft: PrimaryDateParams | undefined
): boolean {
return (
applied?.from !== draft?.from || applied?.to !== draft?.to || applied?.preset !== draft?.preset
);
}
/**
* The subset of `REPORT_DATE_PARAM_KEYS` that carries the period-over-period
* comparison.
*/
const COMPARISON_PARAM_KEYS = [ 'comp', 'compare_from', 'compare_to', 'compare_preset' ] as const;
/**
* Drop the comparison params from a search object, keeping everything else.
*
* Detail pages have no period-over-period comparison by design, so the page
* strips these from the `reportParams` it injects into its widgets: the
* invariant holds by construction instead of relying on every widget to
* ignore them.
*
* @param search - The current route search params.
* @return A new object without the comparison params.
*/
export function omitComparisonReportParams(
search: Record< string, unknown > | undefined
): Record< string, unknown > {
if ( ! search ) {
return {};
}
const stripped: Record< string, unknown > = { ...search };
for ( const key of COMPARISON_PARAM_KEYS ) {
delete stripped[ key ];
}
return stripped;
}
/**
* Drop the shared report-window params from a search object, keeping everything else.
*
* @param search - The current route search params.
* @return A new object without the report-window params.
*/
export function omitReportDateParams< T extends Record< string, unknown > >(
search: T
): Partial< T > {
const stripped: Partial< T > = { ...search };
for ( const key of REPORT_DATE_PARAM_KEYS ) {
delete stripped[ key ];
}
return stripped;
}
/**
* Serialize one search value the way the router does.
*
* The router JSON-parses every search value on read, so a string that itself
* parses as JSON (e.g. `comp: '1'`) must be written JSON-quoted (`comp="1"`)
* or it comes back as a different type (`comp: 1`) and strict checks like
* `comp === '1'` silently fail. Strings that don't parse (dates, presets)
* stay raw, matching the router's own stringifier.
*
* @param value - The search value to serialize.
* @return The querystring-ready value.
*/
export function stringifySearchValue( value: unknown ): string {
Eif ( typeof value === 'string' ) {
try {
JSON.parse( value );
return JSON.stringify( value );
} catch {
return value;
}
}
return String( value );
}
/**
* Add the shared report-window querystring and any page-specific params to a path.
*
* @param path - The path to link to.
* @param search - The current route search params.
* @param extraParams - Optional destination-specific query params.
* @return The path with its serialized querystring.
*/
function buildReportWindowLink(
path: string,
search: Record< string, unknown > | undefined,
extraParams: Record< string, string > = {}
): string {
const params = { ...pickReportNavigationParams( search ), ...extraParams };
const query = new URLSearchParams(
Object.entries( params ).map( ( [ key, value ] ) => [ key, stringifySearchValue( value ) ] )
).toString();
return query ? `${ path }?${ query }` : path;
}
/**
* Build the `to` link back to the dashboard, preserving the shared report window.
*
* Serializes the date range and comparison into a querystring, and reopens the
* dashboard tab the page was reached from, so returning restores the same view.
* Page-scoped params are dropped.
*
* @param search - The current route search params.
* @return A dashboard `to` path (e.g. `/?from=…&to=…`), or `/` when none are set.
*/
export function buildDashboardLink( search: Record< string, unknown > | undefined ): string {
const { [ DASHBOARD_ORIGIN_PARAM ]: section } = pickDashboardOriginParams( search );
return buildReportWindowLink( '/', pickReturnDateParams( search ), section ? { section } : {} );
}
/**
* Build the `to` link to a report, preserving the shared report window.
*
* @param reportId - The report registry id.
* @param search - The current route search params.
* @param section - The referring report's validated section.
* @return A report `to` path with the shared report-window querystring.
*/
export function buildReportLink(
reportId: string,
search: Record< string, unknown > | undefined,
section?: string
): string {
return buildReportWindowLink(
`/reports/${ reportId }`,
search,
section ? { section } : undefined
);
}
|