Support handling deeplinks in plugins
Summary:
This adds support for handling incoming deeplinks in a Sandy plugin, which can be done by using a `client.onDeepLink(deepLink => { } )` listener
Also generalized deeplinks to not just support strings, but also richer objects, which is beneficial to plugin to plugin linking.
Reviewed By: jknoxville
Differential Revision: D22524749
fbshipit-source-id: 2cbe8d52f6eac91a1c1c8c8494706952920b9181
This commit is contained in:
committed by
Facebook GitHub Bot
parent
485b4c9827
commit
f0c54667e0
@@ -55,7 +55,7 @@ type DispatchFromProps = {
|
||||
selectPlugin: (payload: {
|
||||
selectedPlugin: string | null;
|
||||
selectedApp: string | null;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
}) => any;
|
||||
updatePluginBlacklist: (blacklist: Array<string>) => any;
|
||||
updateCategoryBlacklist: (blacklist: Array<string>) => any;
|
||||
@@ -414,7 +414,7 @@ type ItemProps = {
|
||||
selectPlugin?: (payload: {
|
||||
selectedPlugin: string | null;
|
||||
selectedApp: string | null;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
}) => any;
|
||||
logger?: Logger;
|
||||
plugin: PluginDefinition | null | undefined;
|
||||
|
||||
@@ -101,7 +101,7 @@ type StateFromProps = {
|
||||
activePlugin: PluginDefinition | undefined;
|
||||
target: Client | BaseDevice | null;
|
||||
pluginKey: string | null;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
selectedApp: string | null;
|
||||
isArchivedDevice: boolean;
|
||||
pendingMessages: Message[] | undefined;
|
||||
@@ -113,7 +113,7 @@ type DispatchFromProps = {
|
||||
selectPlugin: (payload: {
|
||||
selectedPlugin: string | null;
|
||||
selectedApp?: string | null;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
}) => any;
|
||||
setPluginState: (payload: {pluginKey: string; state: any}) => void;
|
||||
setStaticView: (payload: StaticView) => void;
|
||||
@@ -178,6 +178,13 @@ class PluginContainer extends PureComponent<Props, State> {
|
||||
|
||||
componentDidUpdate() {
|
||||
this.processMessageQueue();
|
||||
// make sure deeplinks are propagated
|
||||
const {deepLinkPayload, target, activePlugin} = this.props;
|
||||
if (deepLinkPayload && target instanceof Client && activePlugin) {
|
||||
target.sandyPluginStates
|
||||
.get(activePlugin.id)
|
||||
?.triggerDeepLink(deepLinkPayload);
|
||||
}
|
||||
}
|
||||
|
||||
processMessageQueue() {
|
||||
@@ -373,7 +380,7 @@ class PluginContainer extends PureComponent<Props, State> {
|
||||
setPersistedState: (state) => setPluginState({pluginKey, state}),
|
||||
target,
|
||||
deepLinkPayload: this.props.deepLinkPayload,
|
||||
selectPlugin: (pluginID: string, deepLinkPayload: string | null) => {
|
||||
selectPlugin: (pluginID: string, deepLinkPayload: unknown) => {
|
||||
const {target} = this.props;
|
||||
// check if plugin will be available
|
||||
if (
|
||||
|
||||
@@ -16,8 +16,11 @@ import {
|
||||
FlipperClient,
|
||||
TestUtils,
|
||||
usePlugin,
|
||||
createState,
|
||||
useValue,
|
||||
} from 'flipper-plugin';
|
||||
import {selectPlugin, starPlugin} from '../reducers/connections';
|
||||
import {updateSettings} from '../reducers/settings';
|
||||
|
||||
interface PersistedState {
|
||||
count: 1;
|
||||
@@ -121,9 +124,6 @@ test('PluginContainer can render Sandy plugins', async () => {
|
||||
Component: MySandyPlugin,
|
||||
},
|
||||
);
|
||||
// any cast because this plugin is not enriched with the meta data that the plugin loader
|
||||
// normally adds. Our further sandy plugin test infra won't need this, but
|
||||
// for this test we do need to act a s a loaded plugin, to make sure PluginContainer itself can handle it
|
||||
const {
|
||||
renderer,
|
||||
act,
|
||||
@@ -230,3 +230,140 @@ test('PluginContainer can render Sandy plugins', async () => {
|
||||
).toBeCalledTimes(1);
|
||||
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
||||
});
|
||||
|
||||
test('PluginContainer + Sandy plugin supports deeplink', async () => {
|
||||
const linksSeen: any[] = [];
|
||||
|
||||
const plugin = (client: FlipperClient) => {
|
||||
const linkState = createState('');
|
||||
client.onDeepLink((link) => {
|
||||
linksSeen.push(link);
|
||||
linkState.set(String(link));
|
||||
});
|
||||
return {
|
||||
linkState,
|
||||
};
|
||||
};
|
||||
|
||||
const definition = new SandyPluginDefinition(
|
||||
TestUtils.createMockPluginDetails(),
|
||||
{
|
||||
plugin,
|
||||
Component() {
|
||||
const instance = usePlugin(plugin);
|
||||
const linkState = useValue(instance.linkState);
|
||||
return <h1>hello {linkState || 'world'}</h1>;
|
||||
},
|
||||
},
|
||||
);
|
||||
const {renderer, act, client, store} = await renderMockFlipperWithPlugin(
|
||||
definition,
|
||||
);
|
||||
|
||||
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
||||
|
||||
expect(linksSeen).toEqual([]);
|
||||
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="css-1orvm1g-View-FlexBox-FlexColumn"
|
||||
>
|
||||
<h1>
|
||||
hello
|
||||
world
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="css-bxcvv9-View-FlexBox-FlexRow"
|
||||
id="detailsSidebar"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`);
|
||||
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: definition.id,
|
||||
deepLinkPayload: 'universe!',
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(linksSeen).toEqual(['universe!']);
|
||||
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="css-1orvm1g-View-FlexBox-FlexColumn"
|
||||
>
|
||||
<h1>
|
||||
hello
|
||||
universe!
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="css-bxcvv9-View-FlexBox-FlexRow"
|
||||
id="detailsSidebar"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`);
|
||||
|
||||
// Sending same link doesn't trigger again
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: definition.id,
|
||||
deepLinkPayload: 'universe!',
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(linksSeen).toEqual(['universe!']);
|
||||
|
||||
// ...nor does a random other store update that does trigger a plugin container render
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
updateSettings({
|
||||
...store.getState().settingsState,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(linksSeen).toEqual(['universe!']);
|
||||
|
||||
// Different link does trigger again
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: definition.id,
|
||||
deepLinkPayload: 'london!',
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(linksSeen).toEqual(['universe!', 'london!']);
|
||||
|
||||
// and same link does trigger if something else was selected in the mean time
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: 'Logs',
|
||||
deepLinkPayload: 'london!',
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
store.dispatch(
|
||||
selectPlugin({
|
||||
selectedPlugin: definition.id,
|
||||
deepLinkPayload: 'london!',
|
||||
selectedApp: client.query.app,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(linksSeen).toEqual(['universe!', 'london!', 'london!']);
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {selectPlugin} from '../reducers/connections';
|
||||
import React from 'react';
|
||||
|
||||
type StateFromProps = {
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
blacklistedPlugins: Array<string>;
|
||||
blacklistedCategories: Array<string>;
|
||||
};
|
||||
@@ -28,7 +28,7 @@ type DispatchFromProps = {
|
||||
selectPlugin: (payload: {
|
||||
selectedPlugin: string | null;
|
||||
selectedApp: string | null | undefined;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
}) => any;
|
||||
};
|
||||
|
||||
@@ -62,7 +62,9 @@ class Notifications extends PureComponent<Props, State> {
|
||||
<Container>
|
||||
<ConnectedNotificationsTable
|
||||
onClear={clearAllNotifications}
|
||||
selectedID={deepLinkPayload}
|
||||
selectedID={
|
||||
typeof deepLinkPayload === 'string' ? deepLinkPayload : null
|
||||
}
|
||||
onSelectPlugin={selectPlugin}
|
||||
logger={logger}
|
||||
defaultFilters={[
|
||||
|
||||
@@ -190,7 +190,7 @@ type StateFromProps = {
|
||||
type SelectPlugin = (payload: {
|
||||
selectedPlugin: string | null;
|
||||
selectedApp?: string | null;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
selectedDevice: BaseDevice;
|
||||
}) => void;
|
||||
|
||||
|
||||
@@ -89,8 +89,8 @@ export type Props<T> = {
|
||||
persistedState: T;
|
||||
setPersistedState: (state: Partial<T>) => void;
|
||||
target: PluginTarget;
|
||||
deepLinkPayload: string | null;
|
||||
selectPlugin: (pluginID: string, deepLinkPayload: string | null) => boolean;
|
||||
deepLinkPayload: unknown;
|
||||
selectPlugin: (pluginID: string, deepLinkPayload: unknown) => boolean;
|
||||
isArchivedDevice: boolean;
|
||||
selectedApp: string | null;
|
||||
setStaticView: (payload: StaticView) => void;
|
||||
|
||||
@@ -23,10 +23,7 @@ const WelcomeScreen = isHeadless()
|
||||
import NotificationScreen from '../chrome/NotificationScreen';
|
||||
import SupportRequestFormV2 from '../fb-stubs/SupportRequestFormV2';
|
||||
import SupportRequestDetails from '../fb-stubs/SupportRequestDetails';
|
||||
import {
|
||||
getPluginKey,
|
||||
defaultEnabledBackgroundPlugins,
|
||||
} from '../utils/pluginUtils';
|
||||
import {getPluginKey} from '../utils/pluginUtils';
|
||||
import {deconstructClientId} from '../utils/clientUtils';
|
||||
import {FlipperDevicePlugin, PluginDefinition, isSandyPlugin} from '../plugin';
|
||||
import {RegisterPluginAction} from './plugins';
|
||||
@@ -63,7 +60,7 @@ export type State = {
|
||||
deviceId?: string;
|
||||
errorMessage?: string;
|
||||
}>;
|
||||
deepLinkPayload: string | null;
|
||||
deepLinkPayload: unknown;
|
||||
staticView: StaticView;
|
||||
};
|
||||
|
||||
@@ -89,7 +86,7 @@ export type Action =
|
||||
payload: {
|
||||
selectedPlugin: null | string;
|
||||
selectedApp?: null | string;
|
||||
deepLinkPayload: null | string;
|
||||
deepLinkPayload: unknown;
|
||||
selectedDevice?: null | BaseDevice;
|
||||
time: number;
|
||||
};
|
||||
@@ -245,8 +242,8 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
|
||||
const {payload} = action;
|
||||
const {selectedPlugin, selectedApp, deepLinkPayload} = payload;
|
||||
let selectedDevice = payload.selectedDevice;
|
||||
if (deepLinkPayload) {
|
||||
const deepLinkParams = new URLSearchParams(deepLinkPayload || '');
|
||||
if (typeof deepLinkPayload === 'string') {
|
||||
const deepLinkParams = new URLSearchParams(deepLinkPayload);
|
||||
const deviceParam = deepLinkParams.get('device');
|
||||
const deviceMatch = state.devices.find((v) => v.title === deviceParam);
|
||||
if (deviceMatch) {
|
||||
@@ -460,7 +457,7 @@ export const selectPlugin = (payload: {
|
||||
selectedPlugin: null | string;
|
||||
selectedApp?: null | string;
|
||||
selectedDevice?: BaseDevice | null;
|
||||
deepLinkPayload: null | string;
|
||||
deepLinkPayload: unknown;
|
||||
time?: number;
|
||||
}): Action => ({
|
||||
type: 'SELECT_PLUGIN',
|
||||
|
||||
Reference in New Issue
Block a user