add replace option to navigateToApp API (#68700) (#68924)

* add `replace` option to `navigateToApp`

* use `unknown` type for state

* add test when `replace` is false
This commit is contained in:
Pierre Gayvallet 2020-06-11 22:20:59 +02:00 committed by GitHub
parent fb64e71a76
commit 0d78bfcf22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 219 additions and 23 deletions

View file

@ -9,10 +9,7 @@ Navigate to a given app
<b>Signature:</b>
```typescript
navigateToApp(appId: string, options?: {
path?: string;
state?: any;
}): Promise<void>;
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise<void>;
```
## Parameters
@ -20,7 +17,7 @@ navigateToApp(appId: string, options?: {
| Parameter | Type | Description |
| --- | --- | --- |
| appId | <code>string</code> | |
| options | <code>{</code><br/><code> path?: string;</code><br/><code> state?: any;</code><br/><code> }</code> | |
| options | <code>NavigateToAppOptions</code> | navigation options |
<b>Returns:</b>

View file

@ -94,6 +94,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) | |
| [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) | Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) |
| [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | |
| [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | |
| [OverlayBannersStart](./kibana-plugin-core-public.overlaybannersstart.md) | |

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md)
## NavigateToAppOptions interface
Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)
<b>Signature:</b>
```typescript
export interface NavigateToAppOptions
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | <code>string</code> | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.appbase.defaultpath.md)<!-- -->\` as default. |
| [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | <code>boolean</code> | if true, will not create a new history entry when navigating (using <code>replace</code> instead of <code>push</code>) |
| [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | <code>unknown</code> | optional state to forward to the application |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) &gt; [path](./kibana-plugin-core-public.navigatetoappoptions.path.md)
## NavigateToAppOptions.path property
optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.appbase.defaultpath.md)<!-- -->\` as default.
<b>Signature:</b>
```typescript
path?: string;
```

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) &gt; [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md)
## NavigateToAppOptions.replace property
if true, will not create a new history entry when navigating (using `replace` instead of `push`<!-- -->)
<b>Signature:</b>
```typescript
replace?: boolean;
```
## Remarks
This option not be used when navigating from and/or to legacy applications.

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) &gt; [state](./kibana-plugin-core-public.navigatetoappoptions.state.md)
## NavigateToAppOptions.state property
optional state to forward to the application
<b>Signature:</b>
```typescript
state?: unknown;
```

View file

@ -76,6 +76,7 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = `
history={
Object {
"push": [MockFunction],
"replace": [MockFunction],
}
}
mounters={Map {}}

View file

@ -29,6 +29,7 @@ jest.doMock('./capabilities', () => ({
export const MockHistory = {
push: jest.fn(),
replace: jest.fn(),
};
export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory);
jest.doMock('history', () => ({

View file

@ -482,9 +482,6 @@ describe('#setup()', () => {
describe('#start()', () => {
beforeEach(() => {
MockHistory.push.mockReset();
parseAppUrlMock.mockReset();
const http = httpServiceMock.createSetupContract({ basePath: '/base-path' });
setupDeps = {
http,
@ -497,6 +494,12 @@ describe('#start()', () => {
service = new ApplicationService();
});
afterEach(() => {
MockHistory.push.mockReset();
MockHistory.replace.mockReset();
parseAppUrlMock.mockReset();
});
it('rejects if called prior to #setup()', async () => {
await expect(service.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot(
`"ApplicationService#setup() must be invoked before start."`
@ -924,6 +927,79 @@ describe('#start()', () => {
await navigateToApp('baseApp:legacyApp1');
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/baseApp');
});
describe('when `replace` option is true', () => {
it('use `history.replace` instead of `history.push`', async () => {
service.setup(setupDeps);
const { navigateToApp } = await service.start(startDeps);
await navigateToApp('myTestApp', { replace: true });
expect(MockHistory.replace).toHaveBeenCalledWith('/app/myTestApp', undefined);
await navigateToApp('myOtherApp', { replace: true });
expect(MockHistory.replace).toHaveBeenCalledWith('/app/myOtherApp', undefined);
});
it('includes state if specified', async () => {
const { register } = service.setup(setupDeps);
register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
const { navigateToApp } = await service.start(startDeps);
await navigateToApp('myTestApp', { state: 'my-state', replace: true });
expect(MockHistory.replace).toHaveBeenCalledWith('/app/myTestApp', 'my-state');
await navigateToApp('app2', { state: 'my-state', replace: true });
expect(MockHistory.replace).toHaveBeenCalledWith('/custom/path', 'my-state');
});
it('appends a path if specified', async () => {
const { register } = service.setup(setupDeps);
register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' }));
const { navigateToApp } = await service.start(startDeps);
await navigateToApp('myTestApp', { path: 'deep/link/to/location/2', replace: true });
expect(MockHistory.replace).toHaveBeenCalledWith(
'/app/myTestApp/deep/link/to/location/2',
undefined
);
await navigateToApp('app2', { path: 'deep/link/to/location/2', replace: true });
expect(MockHistory.replace).toHaveBeenCalledWith(
'/custom/path/deep/link/to/location/2',
undefined
);
});
it('do not change the behavior when in legacy mode', async () => {
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
service.setup(setupDeps);
const { navigateToApp } = await service.start(startDeps);
await navigateToApp('alpha', { replace: true });
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/alpha');
});
});
describe('when `replace` option is false', () => {
it('behave as when the option is unspecified', async () => {
service.setup(setupDeps);
const { navigateToApp } = await service.start(startDeps);
await navigateToApp('myTestApp', { replace: false });
expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined);
await navigateToApp('myOtherApp', { replace: false });
expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined);
expect(MockHistory.replace).not.toHaveBeenCalled();
});
});
});
describe('navigateToUrl', () => {

View file

@ -44,6 +44,7 @@ import {
LegacyApp,
LegacyAppMounter,
Mounter,
NavigateToAppOptions,
} from './types';
import { getLeaveAction, isConfirmAction } from './application_leave';
import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils';
@ -105,7 +106,7 @@ export class ApplicationService {
private registrationClosed = false;
private history?: History<any>;
private mountContext?: IContextContainer<AppMountDeprecated>;
private navigate?: (url: string, state: any) => void;
private navigate?: (url: string, state: unknown, replace: boolean) => void;
private redirectTo?: (url: string) => void;
public setup({
@ -125,10 +126,16 @@ export class ApplicationService {
this.history = history || createBrowserHistory({ basename });
}
// If we do not have history available, use redirectTo to do a full page refresh.
this.navigate = (url, state) =>
// basePath not needed here because `history` is configured with basename
this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url));
this.navigate = (url, state, replace) => {
if (this.history) {
// basePath not needed here because `history` is configured with basename
return replace ? this.history.replace(url, state) : this.history.push(url, state);
} else {
// If we do not have history available (legacy mode), use redirectTo to do a full page refresh.
return redirectTo(basePath.prepend(url));
}
};
this.redirectTo = redirectTo;
this.mountContext = context.createContextContainer();
@ -278,14 +285,14 @@ export class ApplicationService {
const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
appId,
{ path, state }: { path?: string; state?: any } = {}
{ path, state, replace = false }: NavigateToAppOptions = {}
) => {
if (await this.shouldNavigate(overlays)) {
if (path === undefined) {
path = applications$.value.get(appId)?.defaultPath;
}
this.appLeaveHandlers.delete(this.currentAppId$.value!);
this.navigate!(getAppUrl(availableMounters, appId, path), state);
this.navigate!(getAppUrl(availableMounters, appId, path), state, replace);
this.currentAppId$.next(appId);
}
};

View file

@ -40,6 +40,7 @@ export {
AppLeaveDefaultAction,
AppLeaveConfirmAction,
LegacyApp,
NavigateToAppOptions,
PublicAppInfo,
PublicLegacyAppInfo,
// Internal types

View file

@ -125,6 +125,25 @@ describe('ApplicationService', () => {
expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1');
});
it('replaces the current history entry when the `replace` option is true', async () => {
const { register } = service.setup(setupDeps);
register(Symbol(), {
id: 'app1',
title: 'App1',
mount: async ({}: AppMountParameters) => {
return () => undefined;
},
});
const { navigateToApp } = await service.start(startDeps);
await navigateToApp('app1', { path: '/foo' });
await navigateToApp('app1', { path: '/bar', replace: true });
expect(history.entries.map((entry) => entry.pathname)).toEqual(['/', '/app/app1/bar']);
});
});
});

View file

@ -661,6 +661,28 @@ export interface InternalApplicationSetup extends Pick<ApplicationSetup, 'regist
): void;
}
/**
* Options for the {@link ApplicationStart.navigateToApp | navigateToApp API}
*/
export interface NavigateToAppOptions {
/**
* optional path inside application to deep link to.
* If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default.
*/
path?: string;
/**
* optional state to forward to the application
*/
state?: unknown;
/**
* if true, will not create a new history entry when navigating (using `replace` instead of `push`)
*
* @remarks
* This option not be used when navigating from and/or to legacy applications.
*/
replace?: boolean;
}
/** @public */
export interface ApplicationStart {
/**
@ -681,11 +703,9 @@ export interface ApplicationStart {
* Navigate to a given app
*
* @param appId
* @param options.path - optional path inside application to deep link to.
* If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default.
* @param options.state - optional state to forward to the application
* @param options - navigation options
*/
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise<void>;
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise<void>;
/**
* Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.

View file

@ -126,6 +126,7 @@ export {
ScopedHistory,
LegacyApp,
PublicLegacyAppInfo,
NavigateToAppOptions,
} from './application';
export {

View file

@ -238,10 +238,7 @@ export interface ApplicationStart {
path?: string;
absolute?: boolean;
}): string;
navigateToApp(appId: string, options?: {
path?: string;
state?: any;
}): Promise<void>;
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise<void>;
navigateToUrl(url: string): Promise<void>;
// @deprecated
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
@ -1035,6 +1032,15 @@ export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulPart
// @public
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
// Warning: (ae-missing-release-tag) "NavigateToAppOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export interface NavigateToAppOptions {
path?: string;
replace?: boolean;
state?: unknown;
}
// Warning: (ae-missing-release-tag) "NavType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)