core-api: add optional typed params for RouteRefs and useRouteRef
Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRefConfig } from './types';
|
||||
import { RouteRefConfig, RouteRef } from './types';
|
||||
|
||||
export class AbsoluteRouteRef {
|
||||
constructor(private readonly config: RouteRefConfig) {}
|
||||
export class AbsoluteRouteRef<Params extends { [param in string]: string }> {
|
||||
constructor(private readonly config: RouteRefConfig<Params>) {}
|
||||
|
||||
get icon() {
|
||||
return this.config.icon;
|
||||
@@ -32,16 +32,18 @@ export class AbsoluteRouteRef {
|
||||
return this.config.title;
|
||||
}
|
||||
|
||||
get P(): Params {
|
||||
throw new Error("Don't call me maybe");
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `routeRef{path=${this.path}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef {
|
||||
return new AbsoluteRouteRef(config);
|
||||
export function createRouteRef<
|
||||
ParamKeys extends string,
|
||||
Params extends { [param in string]: string } = { [name in ParamKeys]: string }
|
||||
>(config: RouteRefConfig<Params>): RouteRef<Params> {
|
||||
return new AbsoluteRouteRef<Params>(config);
|
||||
}
|
||||
|
||||
// TODO(Rugvip): Added for backwards compatibility, remove once old usage is gone
|
||||
// We may want to avoid exporting the AbsoluteRouteRef itself though, and consider
|
||||
// a different model for how to create sub routes, just avoid this
|
||||
export type MutableRouteRef = AbsoluteRouteRef;
|
||||
|
||||
@@ -29,11 +29,16 @@ import {
|
||||
routeParentCollector,
|
||||
routeObjectCollector,
|
||||
} from './collectors';
|
||||
import { useRouteRef, RoutingProvider, validateRoutes } from './hooks';
|
||||
import {
|
||||
useRouteRef,
|
||||
RoutingProvider,
|
||||
validateRoutes,
|
||||
RouteFunc,
|
||||
} from './hooks';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { RouteRef, RouteRefConfig } from './types';
|
||||
|
||||
const mockConfig = (extra?: Partial<RouteRefConfig>) => ({
|
||||
const mockConfig = (extra?: Partial<RouteRefConfig<{}>>) => ({
|
||||
path: '/unused',
|
||||
title: 'Unused',
|
||||
...extra,
|
||||
@@ -48,13 +53,13 @@ const ref3 = createRouteRef(mockConfig({ path: '/wat3' }));
|
||||
const ref4 = createRouteRef(mockConfig({ path: '/wat4' }));
|
||||
const ref5 = createRouteRef(mockConfig({ path: '/wat5' }));
|
||||
|
||||
const MockRouteSource = (props: {
|
||||
const MockRouteSource = <T extends { [name in string]: string }>(props: {
|
||||
name: string;
|
||||
routeRef: RouteRef;
|
||||
params?: Record<string, string>;
|
||||
routeRef: RouteRef<T>;
|
||||
params?: T;
|
||||
}) => {
|
||||
try {
|
||||
const routeFunc = useRouteRef(props.routeRef);
|
||||
const routeFunc = useRouteRef(props.routeRef) as RouteFunc<any>;
|
||||
return (
|
||||
<div>
|
||||
Path at {props.name}: {routeFunc(props.params)}
|
||||
|
||||
@@ -15,33 +15,42 @@
|
||||
*/
|
||||
|
||||
import React, { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
import { BackstageRouteObject, RouteRef } from './types';
|
||||
import { AnyRouteRef, BackstageRouteObject, RouteRef } from './types';
|
||||
import { generatePath, matchRoutes, useLocation } from 'react-router-dom';
|
||||
|
||||
export type RouteFunc = (params?: Record<string, string>) => string;
|
||||
// The extra TS magic here is to require a single params argument if the RouteRef
|
||||
// had at least one param defined, but require 0 arguments if there are no params defined.
|
||||
// Without this we'd have to pass in empty object to all parameter-less RouteRefs
|
||||
// just to make TypeScript happy, or we would have to make the argument optional in
|
||||
// which case you might forget to pass it in when it is actually required.
|
||||
export type RouteFunc<Params extends { [param in string]: string }> = (
|
||||
...[params]: Params[keyof Params] extends never
|
||||
? readonly []
|
||||
: readonly [Params]
|
||||
) => string;
|
||||
|
||||
class RouteResolver {
|
||||
constructor(
|
||||
private readonly routePaths: Map<RouteRef, string>,
|
||||
private readonly routeParents: Map<RouteRef, RouteRef | undefined>,
|
||||
private readonly routePaths: Map<AnyRouteRef, string>,
|
||||
private readonly routeParents: Map<AnyRouteRef, AnyRouteRef | undefined>,
|
||||
private readonly routeObjects: BackstageRouteObject[],
|
||||
) {}
|
||||
|
||||
resolve(
|
||||
routeRef: RouteRef,
|
||||
resolve<Params extends { [param in string]: string }>(
|
||||
routeRef: RouteRef<Params>,
|
||||
sourceLocation: ReturnType<typeof useLocation>,
|
||||
): RouteFunc {
|
||||
): RouteFunc<Params> {
|
||||
const match = matchRoutes(this.routeObjects, sourceLocation) ?? [];
|
||||
|
||||
const lastPath = this.routePaths.get(routeRef);
|
||||
if (!lastPath) {
|
||||
throw new Error(`No path for ${routeRef}`);
|
||||
}
|
||||
const targetRefStack = Array<RouteRef>();
|
||||
const targetRefStack = Array<AnyRouteRef>();
|
||||
let matchIndex = -1;
|
||||
|
||||
for (
|
||||
let currentRouteRef: RouteRef | undefined = routeRef;
|
||||
let currentRouteRef: AnyRouteRef | undefined = routeRef;
|
||||
currentRouteRef;
|
||||
currentRouteRef = this.routeParents.get(currentRouteRef)
|
||||
) {
|
||||
@@ -87,15 +96,18 @@ class RouteResolver {
|
||||
.join('/')
|
||||
.replace(/\/\/+/g, '/'); // Normalize path to not contain repeated /'s
|
||||
|
||||
return (params?: Record<string, string>) => {
|
||||
const routeFunc: RouteFunc<Params> = (...[params]) => {
|
||||
return `${parentPath}${prefixPath}${generatePath(lastPath, params)}`;
|
||||
};
|
||||
return routeFunc;
|
||||
}
|
||||
}
|
||||
|
||||
const RoutingContext = createContext<RouteResolver | undefined>(undefined);
|
||||
|
||||
export function useRouteRef(routeRef: RouteRef): RouteFunc {
|
||||
export function useRouteRef<Params extends { [param in string]: string }>(
|
||||
routeRef: RouteRef<Params>,
|
||||
): RouteFunc<Params> {
|
||||
const sourceLocation = useLocation();
|
||||
const resolver = useContext(RoutingContext);
|
||||
const routeFunc = useMemo(
|
||||
@@ -111,8 +123,8 @@ export function useRouteRef(routeRef: RouteRef): RouteFunc {
|
||||
}
|
||||
|
||||
type ProviderProps = {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>;
|
||||
routePaths: Map<AnyRouteRef, string>;
|
||||
routeParents: Map<AnyRouteRef, AnyRouteRef | undefined>;
|
||||
routeObjects: BackstageRouteObject[];
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -132,8 +144,8 @@ export const RoutingProvider = ({
|
||||
};
|
||||
|
||||
export function validateRoutes(
|
||||
routePaths: Map<RouteRef, string>,
|
||||
routeParents: Map<RouteRef, RouteRef | undefined>,
|
||||
routePaths: Map<AnyRouteRef, string>,
|
||||
routeParents: Map<AnyRouteRef, AnyRouteRef | undefined>,
|
||||
) {
|
||||
const notLeafRoutes = new Set(routeParents.values());
|
||||
notLeafRoutes.delete(undefined);
|
||||
@@ -143,7 +155,7 @@ export function validateRoutes(
|
||||
continue;
|
||||
}
|
||||
|
||||
let currentRouteRef: RouteRef | undefined = route;
|
||||
let currentRouteRef: AnyRouteRef | undefined = route;
|
||||
|
||||
let fullPath = '';
|
||||
while (currentRouteRef) {
|
||||
|
||||
@@ -15,5 +15,4 @@
|
||||
*/
|
||||
|
||||
export type { RouteRef, RouteRefConfig } from './types';
|
||||
export type { MutableRouteRef, AbsoluteRouteRef } from './RouteRef';
|
||||
export { createRouteRef } from './RouteRef';
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
|
||||
import { IconComponent } from '../icons';
|
||||
|
||||
export type RouteRef = {
|
||||
export type RouteRef<Params extends { [param in string]: string } = {}> = {
|
||||
// TODO(Rugvip): Remove path, look up via registry instead
|
||||
path: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
P: Params;
|
||||
};
|
||||
|
||||
export type RouteRefConfig = {
|
||||
export type AnyRouteRef = RouteRef<any>;
|
||||
|
||||
export type RouteRefConfig<Params extends { [param in string]: string }> = {
|
||||
params?: Array<keyof Params>;
|
||||
path: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
@@ -35,5 +39,5 @@ export interface BackstageRouteObject {
|
||||
children?: BackstageRouteObject[];
|
||||
element: React.ReactNode;
|
||||
path: string;
|
||||
routeRef: RouteRef;
|
||||
routeRef: AnyRouteRef;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user