From 9838c202d3ffbd14cb9bdfb57fe77f4a262ea1cf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 21 Dec 2020 18:14:25 +0100 Subject: [PATCH] core-api: implement mount point gathering for entity page tabs Co-authored-by: blam --- .../core-api/src/routing/collectors.test.tsx | 205 ++++++++++++++---- packages/core-api/src/routing/collectors.tsx | 54 ++++- 2 files changed, 211 insertions(+), 48 deletions(-) diff --git a/packages/core-api/src/routing/collectors.test.tsx b/packages/core-api/src/routing/collectors.test.tsx index 200846f175..7622cdc70a 100644 --- a/packages/core-api/src/routing/collectors.test.tsx +++ b/packages/core-api/src/routing/collectors.test.tsx @@ -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(map: Map): [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 = ( + + + + +
+ +
+ HELLO +
+ + + + + + + +
+
); - 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 = ( + + + + + + + + + + + + + + ); + + 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 = ( + + + + + + + + + + + + ); + + 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', () => { diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index e47a59aa85..1bf9558d06 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -32,25 +32,48 @@ function getMountPoint(node: ReactElement): RouteRef | undefined { export const routePathCollector = createCollector( () => new Map(), - (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(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(), - (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(node, 'core.gatherMountPoints')) { + return { sticky: nextParent }; } return nextParent;