add defaultTarget to ExternalRouteRef
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
'@backstage/frontend-app-api': patch
|
||||
'@backstage/core-compat-api': patch
|
||||
---
|
||||
|
||||
Allow external route refs in the new system to have a `defaultTarget` pointing to a route that it'll resolve to by default if no explicit bindings were made by the adopter.
|
||||
@@ -3,7 +3,6 @@ app:
|
||||
packages: 'all' # ✨
|
||||
routes:
|
||||
bindings:
|
||||
pages.pageX: pages.pageX
|
||||
catalog.viewTechDoc: techdocs.docRoot
|
||||
catalog.createComponent: catalog-import.importPage
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
const indexRouteRef = createRouteRef();
|
||||
const page1RouteRef = createRouteRef();
|
||||
export const externalPageXRouteRef = createExternalRouteRef();
|
||||
export const externalPageXRouteRef = createExternalRouteRef({
|
||||
defaultTarget: 'pages.pageX',
|
||||
});
|
||||
export const pageXRouteRef = createRouteRef();
|
||||
// const page2RouteRef = createSubRouteRef({
|
||||
// id: 'page2',
|
||||
|
||||
@@ -199,6 +199,9 @@ export function convertLegacyRouteRef(
|
||||
getDescription() {
|
||||
return legacyRefStr;
|
||||
},
|
||||
getDefaultTarget() {
|
||||
return newRef.getDefaultTarget();
|
||||
},
|
||||
setId(id: string) {
|
||||
newRef.setId(id);
|
||||
},
|
||||
|
||||
@@ -117,4 +117,48 @@ describe('resolveRouteBindings', () => {
|
||||
"Invalid config at app.routes.bindings['mySource'], 'myTarget' is not a valid route",
|
||||
);
|
||||
});
|
||||
|
||||
it('can have default targets, but at the lowest priority', () => {
|
||||
const source = createExternalRouteRef({ defaultTarget: 'target1' });
|
||||
const target1 = createRouteRef();
|
||||
const target2 = createRouteRef();
|
||||
const routesById = {
|
||||
routes: new Map([
|
||||
['target1', target1],
|
||||
['target2', target2],
|
||||
]),
|
||||
externalRoutes: new Map([['source', source]]),
|
||||
};
|
||||
|
||||
// defaultTarget wins only if no bind or config matches
|
||||
let result = resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({}),
|
||||
routesById,
|
||||
);
|
||||
|
||||
expect(result.get(source)).toBe(target1);
|
||||
|
||||
// config wins over defaultTarget
|
||||
result = resolveRouteBindings(
|
||||
() => {},
|
||||
new ConfigReader({
|
||||
app: { routes: { bindings: { source: 'target2' } } },
|
||||
}),
|
||||
routesById,
|
||||
);
|
||||
|
||||
expect(result.get(source)).toBe(target2);
|
||||
|
||||
// bind wins over defaultTarget
|
||||
result = resolveRouteBindings(
|
||||
({ bind }) => {
|
||||
bind({ a: source }, { a: target2 });
|
||||
},
|
||||
new ConfigReader({}),
|
||||
routesById,
|
||||
);
|
||||
|
||||
expect(result.get(source)).toBe(target2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
import { RouteRefsById } from './collectRouteIds';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
|
||||
/**
|
||||
* Extracts a union of the keys in a map whose value extends the given type
|
||||
@@ -82,6 +84,7 @@ export function resolveRouteBindings(
|
||||
): Map<ExternalRouteRef, RouteRef | SubRouteRef> {
|
||||
const result = new Map<ExternalRouteRef, RouteRef | SubRouteRef>();
|
||||
|
||||
// Perform callback bindings first with highest priority
|
||||
if (bindRoutes) {
|
||||
const bind: CreateAppRouteBinder = (
|
||||
externalRoutes,
|
||||
@@ -105,37 +108,50 @@ export function resolveRouteBindings(
|
||||
bindRoutes({ bind });
|
||||
}
|
||||
|
||||
const bindingsConfig = config.getOptionalConfig('app.routes.bindings');
|
||||
if (!bindingsConfig) {
|
||||
return result;
|
||||
// Then perform config based bindings with lower priority
|
||||
const bindings = config
|
||||
.getOptionalConfig('app.routes.bindings')
|
||||
?.get<JsonObject>();
|
||||
if (bindings) {
|
||||
for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
|
||||
if (typeof targetRefId !== 'string' || targetRefId === '') {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`,
|
||||
);
|
||||
}
|
||||
|
||||
const externalRef = routesById.externalRoutes.get(externalRefId);
|
||||
if (!externalRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`,
|
||||
);
|
||||
}
|
||||
if (result.has(externalRef)) {
|
||||
continue;
|
||||
}
|
||||
const targetRef = routesById.routes.get(targetRefId);
|
||||
if (!targetRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`,
|
||||
);
|
||||
}
|
||||
|
||||
result.set(externalRef, targetRef);
|
||||
}
|
||||
}
|
||||
|
||||
const bindings = bindingsConfig.get<JsonObject>();
|
||||
for (const [externalRefId, targetRefId] of Object.entries(bindings)) {
|
||||
if (typeof targetRefId !== 'string' || targetRefId === '') {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`,
|
||||
);
|
||||
// Finally fall back to attempting to map defaults, at lowest priority
|
||||
for (const externalRef of routesById.externalRoutes.values()) {
|
||||
if (!result.has(externalRef)) {
|
||||
const defaultRefId =
|
||||
toInternalExternalRouteRef(externalRef).getDefaultTarget();
|
||||
if (defaultRefId) {
|
||||
const defaultRef = routesById.routes.get(defaultRefId);
|
||||
if (defaultRef) {
|
||||
result.set(externalRef, defaultRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const externalRef = routesById.externalRoutes.get(externalRefId);
|
||||
if (!externalRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`,
|
||||
);
|
||||
}
|
||||
// Route bindings defined in config have lower priority than those defined in code
|
||||
if (result.has(externalRef)) {
|
||||
continue;
|
||||
}
|
||||
const targetRef = routesById.routes.get(targetRefId);
|
||||
if (!targetRef) {
|
||||
throw new Error(
|
||||
`Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`,
|
||||
);
|
||||
}
|
||||
|
||||
result.set(externalRef, targetRef);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -573,6 +573,7 @@ export function createExternalRouteRef<
|
||||
? (keyof TParams)[]
|
||||
: TParamKeys[];
|
||||
optional?: TOptional;
|
||||
defaultTarget?: string;
|
||||
}): ExternalRouteRef<
|
||||
keyof TParams extends never
|
||||
? undefined
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface InternalExternalRouteRef<
|
||||
readonly version: 'v1';
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
getDefaultTarget(): string | undefined;
|
||||
|
||||
setId(id: string): void;
|
||||
}
|
||||
@@ -80,10 +81,15 @@ class ExternalRouteRefImpl
|
||||
constructor(
|
||||
readonly optional: boolean,
|
||||
readonly params: string[] = [],
|
||||
readonly defaultTarget: string | undefined,
|
||||
creationSite: string,
|
||||
) {
|
||||
super(params, creationSite);
|
||||
}
|
||||
|
||||
getDefaultTarget() {
|
||||
return this.defaultTarget;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +121,14 @@ export function createExternalRouteRef<
|
||||
* if they aren't, `useExternalRouteRef` will return `undefined`.
|
||||
*/
|
||||
optional?: TOptional;
|
||||
|
||||
/**
|
||||
* The route (typically in another plugin) that this should map to by default.
|
||||
*
|
||||
* The string is expected to be on the standard `<plugin id>.<route id>` form,
|
||||
* for example `techdocs.docRoot`.
|
||||
*/
|
||||
defaultTarget?: string;
|
||||
}): ExternalRouteRef<
|
||||
keyof TParams extends never
|
||||
? undefined
|
||||
@@ -126,6 +140,7 @@ export function createExternalRouteRef<
|
||||
return new ExternalRouteRefImpl(
|
||||
Boolean(options?.optional),
|
||||
options?.params as string[] | undefined,
|
||||
options?.defaultTarget,
|
||||
describeParentCallSite(),
|
||||
) as ExternalRouteRef<any, any>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user