Merge pull request #25918 from backstage/rugvip/optional
frontend-plugin-api: make all route refs optional at all times
This commit is contained in:
@@ -800,13 +800,11 @@ export function createExternalRouteRef<
|
||||
[param in TParamKeys]: string;
|
||||
}
|
||||
| undefined = undefined,
|
||||
TOptional extends boolean = false,
|
||||
TParamKeys extends string = string,
|
||||
>(options?: {
|
||||
readonly params?: string extends TParamKeys
|
||||
? (keyof TParams)[]
|
||||
: TParamKeys[];
|
||||
optional?: TOptional;
|
||||
defaultTarget?: string;
|
||||
}): ExternalRouteRef<
|
||||
keyof TParams extends never
|
||||
@@ -815,8 +813,7 @@ export function createExternalRouteRef<
|
||||
? TParams
|
||||
: {
|
||||
[param in TParamKeys]: string;
|
||||
},
|
||||
TOptional
|
||||
}
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -1301,13 +1298,10 @@ export interface ExtensionOverridesOptions {
|
||||
// @public
|
||||
export interface ExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> {
|
||||
// (undocumented)
|
||||
readonly $$type: '@backstage/ExternalRouteRef';
|
||||
// (undocumented)
|
||||
readonly optional: TOptional;
|
||||
// (undocumented)
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
@@ -1571,7 +1565,7 @@ export interface RouteResolutionApi {
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
| ExternalRouteRef<TParams>,
|
||||
options?: RouteResolutionApiResolveOptions,
|
||||
): RouteFunc<TParams> | undefined;
|
||||
}
|
||||
@@ -1632,18 +1626,13 @@ export function useComponentRef<T extends {}>(
|
||||
ref: ComponentRef<T>,
|
||||
): ComponentType<T>;
|
||||
|
||||
// @public
|
||||
export function useRouteRef<
|
||||
TOptional extends boolean,
|
||||
TParams extends AnyRouteRefParams,
|
||||
>(
|
||||
routeRef: ExternalRouteRef<TParams, TOptional>,
|
||||
): TOptional extends true ? RouteFunc<TParams> | undefined : RouteFunc<TParams>;
|
||||
|
||||
// @public
|
||||
export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
routeRef: RouteRef<TParams> | SubRouteRef<TParams>,
|
||||
): RouteFunc<TParams>;
|
||||
routeRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams>,
|
||||
): RouteFunc<TParams> | undefined;
|
||||
|
||||
// @public
|
||||
export function useRouteRefParams<Params extends AnyRouteRefParams>(
|
||||
|
||||
@@ -60,7 +60,7 @@ export interface RouteResolutionApi {
|
||||
anyRouteRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
| ExternalRouteRef<TParams>,
|
||||
options?: RouteResolutionApiResolveOptions,
|
||||
): RouteFunc<TParams> | undefined;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ describe('ExternalRouteRef', () => {
|
||||
const routeRef: ExternalRouteRef<undefined> = createExternalRouteRef();
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(internal.optional).toBe(false);
|
||||
|
||||
expect(String(internal)).toMatch(
|
||||
/^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/,
|
||||
@@ -35,16 +34,6 @@ describe('ExternalRouteRef', () => {
|
||||
expect(String(internal)).toBe('ExternalRouteRef{some-id}');
|
||||
});
|
||||
|
||||
it('should be created as optional', () => {
|
||||
const routeRef: ExternalRouteRef<undefined, true> = createExternalRouteRef({
|
||||
params: [],
|
||||
optional: true,
|
||||
});
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(internal.optional).toEqual(true);
|
||||
});
|
||||
|
||||
it('should be created with params', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
@@ -52,63 +41,39 @@ describe('ExternalRouteRef', () => {
|
||||
}> = createExternalRouteRef({ params: ['x', 'y'] });
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
expect(internal.optional).toEqual(false);
|
||||
});
|
||||
|
||||
it('should be created as optional with params', () => {
|
||||
const routeRef: ExternalRouteRef<{
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({ params: ['x', 'y'], optional: true });
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
expect(internal.optional).toEqual(true);
|
||||
});
|
||||
|
||||
it('should properly infer and validate parameter types and assignments', () => {
|
||||
function checkRouteRef<
|
||||
T extends AnyRouteRefParams,
|
||||
TOptional extends boolean,
|
||||
TCheck extends TOptional,
|
||||
>(
|
||||
_ref: ExternalRouteRef<T, TOptional>,
|
||||
function checkRouteRef<T extends AnyRouteRefParams>(
|
||||
_ref: ExternalRouteRef<T>,
|
||||
_params: T extends undefined ? undefined : T,
|
||||
_optional: TCheck,
|
||||
) {}
|
||||
|
||||
const _1 = createExternalRouteRef({ params: ['notX'] });
|
||||
checkRouteRef(_1, { notX: '' }, false);
|
||||
const _1 = createExternalRouteRef();
|
||||
checkRouteRef(_1, undefined);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_1, { x: '' }, false);
|
||||
checkRouteRef(_1, { x: '' });
|
||||
|
||||
const _2 = createExternalRouteRef({ params: ['x'], optional: true });
|
||||
checkRouteRef(_2, { x: '' }, true);
|
||||
const _2 = createExternalRouteRef({ params: ['x'] });
|
||||
checkRouteRef(_2, { x: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, undefined, false);
|
||||
checkRouteRef(_2, { notX: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_2, undefined);
|
||||
|
||||
const _3 = createExternalRouteRef({ params: ['x', 'y'] });
|
||||
checkRouteRef(_3, { x: '', y: '' }, false);
|
||||
checkRouteRef(_3, { x: '', y: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_3, { x: '' }, false);
|
||||
checkRouteRef(_3, { x: '' });
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_3, { x: '', y: '', z: '' }, false);
|
||||
checkRouteRef(_3, { x: '', y: '', z: '' });
|
||||
|
||||
const _4 = createExternalRouteRef({ params: [] });
|
||||
checkRouteRef(_4, undefined, false);
|
||||
checkRouteRef(_4, undefined);
|
||||
// @ts-expect-error
|
||||
checkRouteRef<any>(_4, { x: '' });
|
||||
|
||||
const _5 = createExternalRouteRef();
|
||||
checkRouteRef(_5, undefined, false);
|
||||
// @ts-expect-error
|
||||
checkRouteRef<any>(_5, { x: '' });
|
||||
|
||||
const _6 = createExternalRouteRef({ optional: true });
|
||||
checkRouteRef(_6, undefined, true);
|
||||
// @ts-expect-error
|
||||
checkRouteRef(_6, undefined, false);
|
||||
checkRouteRef(_4, { x: '' });
|
||||
|
||||
// To avoid complains about missing expectations and unused vars
|
||||
expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String));
|
||||
expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,18 +29,15 @@ import { AnyRouteRefParams } from './types';
|
||||
*/
|
||||
export interface ExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> {
|
||||
readonly $$type: '@backstage/ExternalRouteRef';
|
||||
readonly T: TParams;
|
||||
readonly optional: TOptional;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
> extends ExternalRouteRef<TParams, TOptional> {
|
||||
> extends ExternalRouteRef<TParams> {
|
||||
readonly version: 'v1';
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
@@ -52,11 +49,8 @@ export interface InternalExternalRouteRef<
|
||||
/** @internal */
|
||||
export function toInternalExternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
TOptional extends boolean = boolean,
|
||||
>(
|
||||
resource: ExternalRouteRef<TParams, TOptional>,
|
||||
): InternalExternalRouteRef<TParams, TOptional> {
|
||||
const r = resource as InternalExternalRouteRef<TParams, TOptional>;
|
||||
>(resource: ExternalRouteRef<TParams>): InternalExternalRouteRef<TParams> {
|
||||
const r = resource as InternalExternalRouteRef<TParams>;
|
||||
if (r.$$type !== '@backstage/ExternalRouteRef') {
|
||||
throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`);
|
||||
}
|
||||
@@ -79,7 +73,6 @@ class ExternalRouteRefImpl
|
||||
readonly $$type = '@backstage/ExternalRouteRef' as any;
|
||||
|
||||
constructor(
|
||||
readonly optional: boolean,
|
||||
readonly params: string[] = [],
|
||||
readonly defaultTarget: string | undefined,
|
||||
creationSite: string,
|
||||
@@ -104,7 +97,6 @@ class ExternalRouteRefImpl
|
||||
*/
|
||||
export function createExternalRouteRef<
|
||||
TParams extends { [param in TParamKeys]: string } | undefined = undefined,
|
||||
TOptional extends boolean = false,
|
||||
TParamKeys extends string = string,
|
||||
>(options?: {
|
||||
/**
|
||||
@@ -114,14 +106,6 @@ export function createExternalRouteRef<
|
||||
? (keyof TParams)[]
|
||||
: TParamKeys[];
|
||||
|
||||
/**
|
||||
* Whether or not this route is optional, defaults to false.
|
||||
*
|
||||
* Optional external routes are not required to be bound in the app, and
|
||||
* if they aren't, `useExternalRouteRef` will return `undefined`.
|
||||
*/
|
||||
optional?: TOptional;
|
||||
|
||||
/**
|
||||
* The route (typically in another plugin) that this should map to by default.
|
||||
*
|
||||
@@ -134,13 +118,11 @@ export function createExternalRouteRef<
|
||||
? undefined
|
||||
: string extends TParamKeys
|
||||
? TParams
|
||||
: { [param in TParamKeys]: string },
|
||||
TOptional
|
||||
: { [param in TParamKeys]: string }
|
||||
> {
|
||||
return new ExternalRouteRefImpl(
|
||||
Boolean(options?.optional),
|
||||
options?.params as string[] | undefined,
|
||||
options?.defaultTarget,
|
||||
describeParentCallSite(),
|
||||
) as ExternalRouteRef<any, any>;
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('v1 consumer', () => {
|
||||
});
|
||||
|
||||
const routeFunc = renderedHook.result.current;
|
||||
expect(routeFunc()).toBe('/hello');
|
||||
expect(routeFunc?.()).toBe('/hello');
|
||||
expect(resolve).toHaveBeenCalledWith(
|
||||
routeRef,
|
||||
expect.objectContaining({
|
||||
@@ -54,6 +54,23 @@ describe('v1 consumer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore missing routes', () => {
|
||||
const routeRef = createRouteRef();
|
||||
|
||||
const renderedHook = renderHook(() => useRouteRef(routeRef), {
|
||||
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<TestApiProvider
|
||||
apis={[[routeResolutionApiRef, { resolve: () => undefined }]]}
|
||||
>
|
||||
<MemoryRouter initialEntries={['/my-page']} children={children} />
|
||||
</TestApiProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const routeFunc = renderedHook.result.current;
|
||||
expect(routeFunc).toBeUndefined();
|
||||
});
|
||||
|
||||
it('re-resolves the routeFunc when the search parameters change', () => {
|
||||
const resolve = jest.fn(() => () => '/hello');
|
||||
|
||||
|
||||
@@ -30,47 +30,14 @@ import { RouteFunc, routeResolutionApiRef, useApi } from '../apis';
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
|
||||
*
|
||||
* @param routeRef - The ref to route that should be converted to URL.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRef<
|
||||
TOptional extends boolean,
|
||||
TParams extends AnyRouteRefParams,
|
||||
>(
|
||||
routeRef: ExternalRouteRef<TParams, TOptional>,
|
||||
): TOptional extends true ? RouteFunc<TParams> | undefined : RouteFunc<TParams>;
|
||||
|
||||
/**
|
||||
* React hook for constructing URLs to routes.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
|
||||
*
|
||||
* @param routeRef - The ref to route that should be converted to URL.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
routeRef: RouteRef<TParams> | SubRouteRef<TParams>,
|
||||
): RouteFunc<TParams>;
|
||||
|
||||
/**
|
||||
* React hook for constructing URLs to routes.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* See {@link https://backstage.io/docs/plugins/composability#routing-system}
|
||||
*
|
||||
* @param routeRef - The ref to route that should be converted to URL.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`.
|
||||
* @returns A function that will in turn return the concrete URL of the `routeRef`, or `undefined` if the route is not available.
|
||||
* @public
|
||||
*/
|
||||
export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
routeRef:
|
||||
| RouteRef<TParams>
|
||||
| SubRouteRef<TParams>
|
||||
| ExternalRouteRef<TParams, any>,
|
||||
| ExternalRouteRef<TParams>,
|
||||
): RouteFunc<TParams> | undefined {
|
||||
const { pathname } = useLocation();
|
||||
const routeResolutionApi = useApi(routeResolutionApiRef);
|
||||
@@ -80,10 +47,5 @@ export function useRouteRef<TParams extends AnyRouteRefParams>(
|
||||
[routeResolutionApi, routeRef, pathname],
|
||||
);
|
||||
|
||||
const isOptional = 'optional' in routeRef && routeRef.optional;
|
||||
if (!routeFunc && !isOptional) {
|
||||
throw new Error(`No path for ${routeRef}`);
|
||||
}
|
||||
|
||||
return routeFunc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user