core-api: initial implementation of external route refs

Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
Patrik Oldsberg
2020-12-02 11:43:30 +01:00
parent ebbc133eaa
commit e2aa76f19b
3 changed files with 59 additions and 7 deletions
+10
View File
@@ -53,3 +53,13 @@ export function createRouteRef<
>(config: RouteRefConfig<Params>): RouteRef<Params> {
return new AbsoluteRouteRef<Params>(config);
}
export class ExternalRouteRef {
toString() {
return `externalRouteRef{}`;
}
}
export function createExternalRouteRef(): ExternalRouteRef {
return new ExternalRouteRef();
}
+33 -4
View File
@@ -35,7 +35,11 @@ import {
validateRoutes,
RouteFunc,
} from './hooks';
import { createRouteRef } from './RouteRef';
import {
createRouteRef,
createExternalRouteRef,
ExternalRouteRef,
} from './RouteRef';
import { RouteRef, RouteRefConfig } from './types';
const mockConfig = (extra?: Partial<RouteRefConfig<{}>>) => ({
@@ -52,10 +56,13 @@ const ref2 = createRouteRef(mockConfig({ path: '/wat2' }));
const ref3 = createRouteRef(mockConfig({ path: '/wat3' }));
const ref4 = createRouteRef(mockConfig({ path: '/wat4' }));
const ref5 = createRouteRef(mockConfig({ path: '/wat5' }));
const eRefA = createExternalRouteRef();
const eRefB = createExternalRouteRef();
const eRefC = createExternalRouteRef();
const MockRouteSource = <T extends { [name in string]: string }>(props: {
name: string;
routeRef: RouteRef<T>;
routeRef: RouteRef<T> | ExternalRouteRef;
params?: T;
}) => {
try {
@@ -90,7 +97,10 @@ const Extension5 = plugin.provide(
createRoutableExtension({ component: MockComponent, mountPoint: ref5 }),
);
function withRoutingProvider(root: ReactElement) {
function withRoutingProvider(
root: ReactElement,
routeBindings: [ExternalRouteRef, RouteRef][] = [],
) {
const { routePaths, routeParents, routeObjects } = traverseElementTree({
root,
discoverers: [childDiscoverer, routeElementDiscoverer],
@@ -106,6 +116,7 @@ function withRoutingProvider(root: ReactElement) {
routePaths={routePaths}
routeParents={routeParents}
routeObjects={routeObjects}
routeBindings={new Map(routeBindings)}
>
{root}
</RoutingProvider>
@@ -119,17 +130,35 @@ describe('discovery', () => {
<Routes>
<Extension1 path="/foo">
<Extension2 path="/bar" name="inside" routeRef={ref2} />
<MockRouteSource name="insideExternal" routeRef={eRefA} />
</Extension1>
<Extension3 path="/baz" />
</Routes>
<MockRouteSource name="outside" routeRef={ref2} />
<MockRouteSource name="outsideExternal1" routeRef={eRefB} />
<MockRouteSource name="outsideExternal2" routeRef={eRefC} />
</MemoryRouter>
);
const rendered = render(withRoutingProvider(root));
const rendered = render(
withRoutingProvider(root, [
[eRefA, ref3],
[eRefB, ref1],
[eRefC, ref2],
]),
);
expect(rendered.getByText('Path at inside: /foo/bar')).toBeInTheDocument();
expect(
rendered.getByText('Path at insideExternal: /baz'),
).toBeInTheDocument();
expect(rendered.getByText('Path at outside: /foo/bar')).toBeInTheDocument();
expect(
rendered.getByText('Path at outsideExternal1: /foo'),
).toBeInTheDocument();
expect(
rendered.getByText('Path at outsideExternal2: /foo/bar'),
).toBeInTheDocument();
});
it('should handle routeRefs with parameters', () => {
+16 -3
View File
@@ -17,6 +17,7 @@
import React, { createContext, ReactNode, useContext, useMemo } from 'react';
import { AnyRouteRef, BackstageRouteObject, RouteRef } from './types';
import { generatePath, matchRoutes, useLocation } from 'react-router-dom';
import { ExternalRouteRef } from './RouteRef';
// 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.
@@ -34,12 +35,17 @@ class RouteResolver {
private readonly routePaths: Map<AnyRouteRef, string>,
private readonly routeParents: Map<AnyRouteRef, AnyRouteRef | undefined>,
private readonly routeObjects: BackstageRouteObject[],
private readonly routeBindings: Map<ExternalRouteRef, RouteRef>,
) {}
resolve<Params extends { [param in string]: string }>(
routeRef: RouteRef<Params>,
routeRefOrExternalRouteRef: RouteRef<Params> | ExternalRouteRef,
sourceLocation: ReturnType<typeof useLocation>,
): RouteFunc<Params> {
const routeRef =
this.routeBindings.get(routeRefOrExternalRouteRef) ??
(routeRefOrExternalRouteRef as RouteRef<Params>);
const match = matchRoutes(this.routeObjects, sourceLocation) ?? [];
const lastPath = this.routePaths.get(routeRef);
@@ -106,7 +112,7 @@ class RouteResolver {
const RoutingContext = createContext<RouteResolver | undefined>(undefined);
export function useRouteRef<Params extends { [param in string]: string }>(
routeRef: RouteRef<Params>,
routeRef: RouteRef<Params> | ExternalRouteRef,
): RouteFunc<Params> {
const sourceLocation = useLocation();
const resolver = useContext(RoutingContext);
@@ -126,6 +132,7 @@ type ProviderProps = {
routePaths: Map<AnyRouteRef, string>;
routeParents: Map<AnyRouteRef, AnyRouteRef | undefined>;
routeObjects: BackstageRouteObject[];
routeBindings: Map<ExternalRouteRef, RouteRef>;
children: ReactNode;
};
@@ -133,9 +140,15 @@ export const RoutingProvider = ({
routePaths,
routeParents,
routeObjects,
routeBindings,
children,
}: ProviderProps) => {
const resolver = new RouteResolver(routePaths, routeParents, routeObjects);
const resolver = new RouteResolver(
routePaths,
routeParents,
routeObjects,
routeBindings,
);
return (
<RoutingContext.Provider value={resolver}>
{children}