core-api: implement mount point gathering for entity page tabs
Co-authored-by: blam <ben@blam.sh>
This commit is contained in:
@@ -24,21 +24,22 @@ import {
|
||||
} from '../extensions/traversal';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createPlugin } from '../plugin';
|
||||
import { createRoutableExtension } from '../extensions';
|
||||
import { attachComponentData, createRoutableExtension } from '../extensions';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { RouteRef } from './types';
|
||||
|
||||
const mockConfig = () => ({ path: '/foo', title: 'Foo' });
|
||||
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
|
||||
<>{children}</>
|
||||
);
|
||||
|
||||
const plugin = createPlugin({ id: 'my-plugin' });
|
||||
|
||||
const ref1 = createRouteRef(mockConfig());
|
||||
const ref2 = createRouteRef(mockConfig());
|
||||
const ref3 = createRouteRef(mockConfig());
|
||||
const ref4 = createRouteRef(mockConfig());
|
||||
const ref5 = createRouteRef(mockConfig());
|
||||
const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' });
|
||||
const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' });
|
||||
const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' });
|
||||
const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' });
|
||||
const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' });
|
||||
const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
const Extension1 = plugin.provide(
|
||||
createRoutableExtension({
|
||||
@@ -71,6 +72,20 @@ const Extension5 = plugin.provide(
|
||||
}),
|
||||
);
|
||||
|
||||
const AggregationComponent = ({
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
path: string;
|
||||
}>) => <>{children}</>;
|
||||
|
||||
attachComponentData(AggregationComponent, 'core.gatherMountPoints', true);
|
||||
|
||||
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
|
||||
return Array.from(map).sort(
|
||||
([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b),
|
||||
);
|
||||
}
|
||||
|
||||
describe('discovery', () => {
|
||||
it('should collect routes', () => {
|
||||
const list = [
|
||||
@@ -117,25 +132,20 @@ describe('discovery', () => {
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
expect(routes).toEqual(
|
||||
new Map([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]),
|
||||
);
|
||||
|
||||
expect(routeParents).toEqual(
|
||||
new Map([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref2],
|
||||
[ref4, undefined],
|
||||
[ref5, ref1],
|
||||
]),
|
||||
);
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref2],
|
||||
[ref4, undefined],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle all react router Route patterns', () => {
|
||||
@@ -168,24 +178,135 @@ describe('discovery', () => {
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
expect(routes).toEqual(
|
||||
new Map([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]),
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar/:id'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/divsoup'],
|
||||
[ref5, '/blop'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the route aggregator key to bind child routes to the same path', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<AggregationComponent path="/foo">
|
||||
<Extension1 />
|
||||
<div>
|
||||
<Extension2 />
|
||||
</div>
|
||||
HELLO
|
||||
</AggregationComponent>
|
||||
<Extension3 path="/bar">
|
||||
<AggregationComponent path="/baz">
|
||||
<Extension4>
|
||||
<Extension5 />
|
||||
</Extension4>
|
||||
</AggregationComponent>
|
||||
</Extension3>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(routeParents).toEqual(
|
||||
new Map([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]),
|
||||
|
||||
const { routes, routeParents } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/foo'],
|
||||
[ref3, '/bar'],
|
||||
[ref4, '/baz'],
|
||||
[ref5, '/baz'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, undefined],
|
||||
[ref3, undefined],
|
||||
[ref4, ref3],
|
||||
[ref5, ref3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use the route aggregator but stop when encountering explicit path', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<Extension1 path="/foo">
|
||||
<AggregationComponent path="/bar">
|
||||
<Extension2>
|
||||
<Extension3 path="/baz">
|
||||
<Extension4 path="/blop" />
|
||||
</Extension3>
|
||||
<Extension5 />
|
||||
</Extension2>
|
||||
</AggregationComponent>
|
||||
</Extension1>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const { routes, routeParents } = traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
expect(sortedEntries(routes)).toEqual([
|
||||
[ref1, '/foo'],
|
||||
[ref2, '/bar'],
|
||||
[ref3, '/baz'],
|
||||
[ref4, '/blop'],
|
||||
[ref5, '/bar'],
|
||||
]);
|
||||
expect(sortedEntries(routeParents)).toEqual([
|
||||
[ref1, undefined],
|
||||
[ref2, ref1],
|
||||
[ref3, ref1],
|
||||
[ref4, ref3],
|
||||
[ref5, ref1],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stop gathering mount points after encountering explicit path', () => {
|
||||
const root = (
|
||||
<MemoryRouter>
|
||||
<Routes>
|
||||
<Extension1 path="/foo">
|
||||
<AggregationComponent path="/bar">
|
||||
<Extension2 path="/baz">
|
||||
<Extension3 />
|
||||
</Extension2>
|
||||
</AggregationComponent>
|
||||
</Extension1>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(() => {
|
||||
traverseElementTree({
|
||||
root,
|
||||
discoverers: [childDiscoverer, routeElementDiscoverer],
|
||||
collectors: {
|
||||
routes: routePathCollector,
|
||||
routeParents: routeParentCollector,
|
||||
},
|
||||
});
|
||||
}).toThrow('Mounted routable extension must have a path');
|
||||
});
|
||||
|
||||
it('should not visit the same element twice', () => {
|
||||
|
||||
@@ -32,25 +32,48 @@ function getMountPoint(node: ReactElement): RouteRef | undefined {
|
||||
|
||||
export const routePathCollector = createCollector(
|
||||
() => new Map<RouteRef, string>(),
|
||||
(acc, node, parent) => {
|
||||
(acc, node, parent, ctxPath: string | undefined) => {
|
||||
// The context path is used during mount point gathering to assign the same path
|
||||
// to all discovered mount points
|
||||
let currentCtxPath = ctxPath;
|
||||
|
||||
if (parent?.props.element === node) {
|
||||
return;
|
||||
return currentCtxPath;
|
||||
}
|
||||
|
||||
// Start gathering mount points when we encounter a mount point gathering flag
|
||||
if (getComponentData<boolean>(node, 'core.gatherMountPoints')) {
|
||||
const path: string | undefined = node.props?.path;
|
||||
if (!path) {
|
||||
throw new Error('Mount point gatherer must have a path');
|
||||
}
|
||||
currentCtxPath = path;
|
||||
}
|
||||
|
||||
const routeRef = getMountPoint(node);
|
||||
if (routeRef) {
|
||||
const path: string | undefined = node.props?.path;
|
||||
let path: string | undefined = node.props?.path;
|
||||
// If we're gathering mount points we use the context path as out path, unless
|
||||
// the element has its own path, in which case we use that instead and stop gathering
|
||||
if (currentCtxPath) {
|
||||
if (path) {
|
||||
currentCtxPath = undefined;
|
||||
} else {
|
||||
path = currentCtxPath;
|
||||
}
|
||||
}
|
||||
if (!path) {
|
||||
throw new Error('Mounted routable extension must have a path');
|
||||
}
|
||||
acc.set(routeRef, path);
|
||||
}
|
||||
return currentCtxPath;
|
||||
},
|
||||
);
|
||||
|
||||
export const routeParentCollector = createCollector(
|
||||
() => new Map<RouteRef, RouteRef | undefined>(),
|
||||
(acc, node, parent, parentRouteRef?: RouteRef) => {
|
||||
(acc, node, parent, parentRouteRef?: RouteRef | { sticky: RouteRef }) => {
|
||||
if (parent?.props.element === node) {
|
||||
return parentRouteRef;
|
||||
}
|
||||
@@ -59,8 +82,27 @@ export const routeParentCollector = createCollector(
|
||||
|
||||
const routeRef = getMountPoint(node);
|
||||
if (routeRef) {
|
||||
acc.set(routeRef, parentRouteRef);
|
||||
nextParent = routeRef;
|
||||
// "sticky" route ref is when we've encountered a mount point gatherer, and we want a
|
||||
// mount points beneath it to have the same parent, regardless of internal structure
|
||||
if (parentRouteRef && 'sticky' in parentRouteRef) {
|
||||
acc.set(routeRef, parentRouteRef.sticky);
|
||||
|
||||
// When we encounter a mount point with an explicit path, we stop gathering
|
||||
// mount points withing the children and remove the sticky state
|
||||
if (node.props?.path) {
|
||||
nextParent = routeRef;
|
||||
} else {
|
||||
nextParent = parentRouteRef;
|
||||
}
|
||||
} else {
|
||||
acc.set(routeRef, parentRouteRef);
|
||||
nextParent = routeRef;
|
||||
}
|
||||
}
|
||||
|
||||
// Mount point gatherers are marked as "sticky"
|
||||
if (getComponentData<boolean>(node, 'core.gatherMountPoints')) {
|
||||
return { sticky: nextParent };
|
||||
}
|
||||
|
||||
return nextParent;
|
||||
|
||||
Reference in New Issue
Block a user