Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

153 lines
4.4 KiB
TypeScript

import { useCallback, useContext } from 'react';
import { UNSAFE_NavigationContext, useLocation, useNavigate } from 'react-router-dom';
import { NAVIGATION_DECLARATION, TransitionId } from './navigation.declaration';
import type { TransitionDeclaration, FromConstraint } from './navigation.types';
import { memoryHistoryPopTo } from '../../os/utils/memoryHistoryPopTo';
export function useAppNavigate() {
const navigate = useNavigate();
const location = useLocation();
const { navigator } = useContext(UNSAFE_NavigationContext);
const go = useCallback(
(
id: TransitionId,
params: Record<string, string | number> = {},
options: { mode?: 'push' | 'replace'; popTo?: string; popToInclusive?: boolean } = {}
) => {
const searchParams = new URLSearchParams(location.search);
const t = NAVIGATION_DECLARATION.transitions.find(
transition => transition.id === id,
) as TransitionDeclaration | undefined;
if (!t) {
throw new Error(`Transition not found: ${id}`);
}
if (!matchFrom(t.from, location.pathname, searchParams)) {
throw new Error(
`Transition "${id}" not allowed from "${location.pathname}${location.search}"`,
);
}
const targetPathname = replaceParams(t.to, params);
const newParams = buildSearchParams({
currentSearchParams: searchParams,
preserveParams: t.preserveParams || [],
staticSearch: t.search,
dynamicSearchParams: t.searchParams,
runtimeParams: params,
});
const searchStr = newParams.toString();
const targetUrl = searchStr ? `${targetPathname}?${searchStr}` : targetPathname;
const finalMode = options.mode || t.mode;
if (options.popTo) {
memoryHistoryPopTo(navigator, options.popTo, { inclusive: options.popToInclusive });
}
navigate(targetUrl, finalMode === 'replace' ? { replace: true } : undefined);
},
[navigate, location.pathname, location.search, navigator],
);
const back = useCallback(
(steps: number = 1) => {
navigate(-steps);
},
[navigate],
);
return { go, back };
}
function matchFrom(
from: TransitionDeclaration['from'],
pathname: string,
searchParams: URLSearchParams,
): boolean {
if (from === '*') return true;
const constraints = Array.isArray(from) ? from : [from];
return constraints.some(constraint => {
if (constraint === '*') return true;
if (typeof constraint === 'string') {
return matchRoute(constraint, pathname);
}
if (!matchRoute(constraint.path, pathname)) {
return false;
}
if (constraint.search) {
for (const [key, expected] of Object.entries(constraint.search)) {
const actual = searchParams.get(key);
if (expected === '*') {
if (!actual) return false;
} else if (expected === null) {
if (actual !== null) return false;
} else if (actual !== expected) {
return false;
}
}
}
return true;
});
}
function buildSearchParams(options: {
currentSearchParams: URLSearchParams;
preserveParams: string[];
staticSearch: Record<string, string | null>;
dynamicSearchParams: Record<string, 'string' | 'number'>;
runtimeParams: Record<string, string | number>;
}): URLSearchParams {
const { currentSearchParams, preserveParams, staticSearch, dynamicSearchParams, runtimeParams } =
options;
const newParams = new URLSearchParams();
for (const key of preserveParams) {
const value = currentSearchParams.get(key);
if (value !== null) {
newParams.set(key, value);
}
}
for (const [key, value] of Object.entries(staticSearch)) {
if (value === null) {
newParams.delete(key);
} else {
newParams.set(key, value);
}
}
for (const key of Object.keys(dynamicSearchParams)) {
const value = runtimeParams[key];
if (value !== undefined) {
newParams.set(key, String(value));
}
}
return newParams;
}
function replaceParams(path: string, params: Record<string, string | number>): string {
return path.replace(/:(\w+)/g, (_, key) => {
const value = params[key];
if (value === undefined) {
throw new Error(`Missing param "${key}" for path "${path}"`);
}
return String(value);
});
}
function matchRoute(template: string, path: string): boolean {
if (template === '*') return true;
const regex = new RegExp('^' + template.replace(/:\w+/g, '[^/]+') + '$');
return regex.test(path);
}