core-api: add optional typed params for RouteRefs and useRouteRef

Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
Patrik Oldsberg
2020-12-01 18:39:23 +01:00
parent ef63f951ea
commit 3e7378953e
5 changed files with 58 additions and 36 deletions
+12 -10
View File
@@ -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;
+11 -6
View File
@@ -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)}
+28 -16
View File
@@ -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) {
-1
View File
@@ -15,5 +15,4 @@
*/
export type { RouteRef, RouteRefConfig } from './types';
export type { MutableRouteRef, AbsoluteRouteRef } from './RouteRef';
export { createRouteRef } from './RouteRef';
+7 -3
View File
@@ -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;
}