frontend-*: add and use RouteResolutionsApi
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -1079,6 +1079,26 @@ export interface RouteRef<
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RouteResolutionApi {
|
||||
// (undocumented)
|
||||
resolve<TParams extends AnyRouteRefParams>(
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
options?: RouteResolutionApiResolveOptions,
|
||||
): RouteFunc<TParams> | undefined;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const routeResolutionApiRef: ApiRef<RouteResolutionApi>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type RouteResolutionApiResolveOptions = {
|
||||
sourcePath?: string;
|
||||
};
|
||||
|
||||
export { SessionApi };
|
||||
|
||||
export { SessionState };
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AnyRouteRefParams,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
} from '../../routing';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* TS magic for handling route parameters.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RouteFunc<TParams extends AnyRouteRefParams> = (
|
||||
...[params]: TParams extends undefined
|
||||
? readonly []
|
||||
: readonly [params: TParams]
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type RouteResolutionApiResolveOptions = {
|
||||
/**
|
||||
* An absolute path to use as a starting point when resolving the route.
|
||||
* If no path is provided the route will be resolved from the root of the app.
|
||||
*/
|
||||
sourcePath?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface RouteResolutionApi {
|
||||
resolve<TParams extends AnyRouteRefParams>(
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
options?: RouteResolutionApiResolveOptions,
|
||||
): RouteFunc<TParams> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `ApiRef` of {@link RouteResolutionApi}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const routeResolutionApiRef = createApiRef<RouteResolutionApi>({
|
||||
id: 'core.route-resolution',
|
||||
});
|
||||
@@ -43,5 +43,6 @@ export * from './FetchApi';
|
||||
export * from './IconsApi';
|
||||
export * from './IdentityApi';
|
||||
export * from './OAuthRequestApi';
|
||||
export * from './RouteResolutionApi';
|
||||
export * from './StorageApi';
|
||||
export * from './AnalyticsApi';
|
||||
|
||||
@@ -21,5 +21,5 @@ export {
|
||||
createExternalRouteRef,
|
||||
type ExternalRouteRef,
|
||||
} from './ExternalRouteRef';
|
||||
export { useRouteRef, type RouteFunc } from './useRouteRef';
|
||||
export { useRouteRef } from './useRouteRef';
|
||||
export { useRouteRefParams } from './useRouteRefParams';
|
||||
|
||||
@@ -21,6 +21,8 @@ import { createVersionedContextForTesting } from '@backstage/version-bridge';
|
||||
import { useRouteRef } from './useRouteRef';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { routeResolutionApiRef } from '../apis';
|
||||
|
||||
describe('v1 consumer', () => {
|
||||
const context = createVersionedContextForTesting('routing-context');
|
||||
@@ -31,13 +33,14 @@ describe('v1 consumer', () => {
|
||||
|
||||
it('should resolve routes', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
|
||||
const renderedHook = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<MemoryRouter initialEntries={['/my-page']} children={children} />
|
||||
<TestApiProvider apis={[[routeResolutionApiRef, { resolve }]]}>
|
||||
<MemoryRouter initialEntries={['/my-page']} children={children} />
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -46,14 +49,13 @@ describe('v1 consumer', () => {
|
||||
expect(resolve).toHaveBeenCalledWith(
|
||||
routeRef,
|
||||
expect.objectContaining({
|
||||
pathname: '/my-page',
|
||||
sourcePath: '/my-page',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('re-resolves the routeFunc when the search parameters change', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
@@ -61,11 +63,13 @@ describe('v1 consumer', () => {
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
<TestApiProvider apis={[[routeResolutionApiRef, { resolve }]]}>
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -79,7 +83,7 @@ describe('v1 consumer', () => {
|
||||
|
||||
it('does not re-resolve the routeFunc the location pathname does not change', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
const api = { resolve };
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
@@ -87,11 +91,13 @@ describe('v1 consumer', () => {
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
<TestApiProvider apis={[[routeResolutionApiRef, api]]}>
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -105,7 +111,7 @@ describe('v1 consumer', () => {
|
||||
|
||||
it('does not re-resolve the routeFunc when the search parameter changes', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
const api = { resolve };
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
@@ -113,11 +119,13 @@ describe('v1 consumer', () => {
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
<TestApiProvider apis={[[routeResolutionApiRef, api]]}>
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -131,7 +139,7 @@ describe('v1 consumer', () => {
|
||||
|
||||
it('does not re-resolve the routeFunc when the hash parameter changes', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
context.set({ 1: { resolve } });
|
||||
const api = { resolve };
|
||||
|
||||
const routeRef = createRouteRef();
|
||||
const history = createBrowserHistory();
|
||||
@@ -139,11 +147,13 @@ describe('v1 consumer', () => {
|
||||
|
||||
const { rerender } = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
<TestApiProvider apis={[[routeResolutionApiRef, api]]}>
|
||||
<Router
|
||||
location={history.location}
|
||||
navigator={history}
|
||||
children={children}
|
||||
/>
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -15,44 +15,12 @@
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { matchRoutes, useLocation } from 'react-router-dom';
|
||||
import { useVersionedContext } from '@backstage/version-bridge';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
import { RouteRef } from './RouteRef';
|
||||
import { SubRouteRef } from './SubRouteRef';
|
||||
import { ExternalRouteRef } from './ExternalRouteRef';
|
||||
|
||||
/**
|
||||
* TS magic for handling route parameters.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RouteFunc<TParams extends AnyRouteRefParams> = (
|
||||
...[params]: TParams extends undefined
|
||||
? readonly []
|
||||
: readonly [params: TParams]
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface RouteResolver {
|
||||
resolve<TParams extends AnyRouteRefParams>(
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
sourceLocation: Parameters<typeof matchRoutes>[1],
|
||||
): RouteFunc<TParams> | undefined;
|
||||
}
|
||||
import { RouteFunc, routeResolutionApiRef, useApi } from '../apis';
|
||||
|
||||
/**
|
||||
* React hook for constructing URLs to routes.
|
||||
@@ -105,26 +73,13 @@ export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
): RouteFunc<TParams> | undefined {
|
||||
const { pathname } = useLocation();
|
||||
const versionedContext = useVersionedContext<{ 1: RouteResolver }>(
|
||||
'routing-context',
|
||||
);
|
||||
if (!versionedContext) {
|
||||
throw new Error('Routing context is not available');
|
||||
}
|
||||
const routeResolutionApi = useApi(routeResolutionApiRef);
|
||||
|
||||
const resolver = versionedContext.atVersion(1);
|
||||
const routeFunc = useMemo(
|
||||
() => resolver && resolver.resolve(routeRef, { pathname }),
|
||||
[resolver, routeRef, pathname],
|
||||
() => routeResolutionApi.resolve(routeRef, { sourcePath: pathname }),
|
||||
[routeResolutionApi, routeRef, pathname],
|
||||
);
|
||||
|
||||
if (!versionedContext) {
|
||||
throw new Error('useRouteRef used outside of routing context');
|
||||
}
|
||||
if (!resolver) {
|
||||
throw new Error('RoutingContext v1 not available');
|
||||
}
|
||||
|
||||
const isOptional = 'optional' in routeRef && routeRef.optional;
|
||||
if (!routeFunc && !isOptional) {
|
||||
throw new Error(`No path for ${routeRef}`);
|
||||
|
||||
Reference in New Issue
Block a user