persisted plugins state

Summary:
Two pros are passed into every plugin to persist state:
- `this.props.persistedState` which is the object of the persisted state
- `this.props.setPersistedState` which can be used to modify the persisted state

The state itself is stored in redux and therefore persisted when switching plugins.

The lifecycle hooks used a HOC are now implemented by the `ref`-function, which makes the code a little cleaner.

Reviewed By: jknoxville

Differential Revision: D8752097

fbshipit-source-id: d4f081f149cd840a29f1132bde91d72d3fba67ed
This commit is contained in:
Daniel Büchele
2018-07-10 02:22:16 -07:00
committed by Facebook Github Bot
parent d0ecb46d64
commit f5dcaf02a4
2 changed files with 63 additions and 74 deletions

View File

@@ -8,7 +8,6 @@ import type {SonarPlugin, SonarBasePlugin} from './plugin.js';
import type LogManager from './fb-stubs/Logger'; import type LogManager from './fb-stubs/Logger';
import type Client from './Client.js'; import type Client from './Client.js';
import type BaseDevice from './devices/BaseDevice.js'; import type BaseDevice from './devices/BaseDevice.js';
import type {Props as PluginProps} from './plugin.js';
import {SonarDevicePlugin} from './plugin.js'; import {SonarDevicePlugin} from './plugin.js';
import {ErrorBoundary, Component, FlexColumn, FlexRow, colors} from 'sonar'; import {ErrorBoundary, Component, FlexColumn, FlexRow, colors} from 'sonar';
@@ -51,78 +50,67 @@ type State = {
pluginKey: string, pluginKey: string,
}; };
function withPluginLifecycleHooks( function computeState(props: Props): State {
PluginComponent: Class<SonarBasePlugin<>>, // plugin changed
target: Client | BaseDevice, let activePlugin = devicePlugins.find(
) { (p: Class<SonarDevicePlugin<>>) => p.id === props.selectedPlugin,
return class extends React.Component<PluginProps<any>> { );
plugin: ?SonarBasePlugin<>; let target = props.selectedDevice;
let pluginKey = 'unknown';
static displayName = `${PluginComponent.title}Plugin`; if (activePlugin) {
pluginKey = `${props.selectedDevice.serial}#${activePlugin.id}`;
componentDidMount() { } else {
const {plugin} = this; target = props.clients.find(
if (plugin) { (client: Client) => client.id === props.selectedApp,
activateMenuItems(plugin); );
plugin._setup(target); activePlugin = plugins.find(
plugin._init(); (p: Class<SonarPlugin<>>) => p.id === props.selectedPlugin,
} );
} if (!activePlugin || !target) {
throw new Error(
componentWillUnmount() { `Plugin "${props.selectedPlugin || ''}" could not be found.`,
if (this.plugin) {
this.plugin._teardown();
}
}
render() {
return (
<PluginComponent
ref={(ref: ?SonarBasePlugin<>) => {
if (ref) {
this.plugin = ref;
}
}}
{...this.props}
/>
); );
} }
pluginKey = `${target.id}#${activePlugin.id}`;
}
return {
activePlugin,
target,
pluginKey,
}; };
} }
class PluginContainer extends Component<Props, State> { class PluginContainer extends Component<Props, State> {
static getDerivedStateFromProps(props: Props) { plugin: ?SonarBasePlugin<>;
let activePlugin = devicePlugins.find(
(p: Class<SonarDevicePlugin<>>) => p.id === props.selectedPlugin,
);
let target = props.selectedDevice;
let pluginKey = 'unknown';
if (activePlugin) {
pluginKey = `${props.selectedDevice.serial}#${activePlugin.id}`;
} else {
target = props.clients.find(
(client: Client) => client.id === props.selectedApp,
);
activePlugin = plugins.find(
(p: Class<SonarPlugin<>>) => p.id === props.selectedPlugin,
);
if (!activePlugin || !target) {
return null;
}
pluginKey = `${target.id}#${activePlugin.id}`;
}
return { constructor(props: Props) {
pluginKey, super();
activePlugin, this.state = computeState(props);
target,
};
} }
state = { componentWillReceiveProps(nextProps: Props) {
pluginKey: 'unknown', if (
activePlugin: null, nextProps.selectedDevice !== this.props.selectedDevice ||
target: null, nextProps.selectedApp !== this.props.selectedApp ||
nextProps.selectedPlugin !== this.props.selectedPlugin
) {
this.setState(computeState(nextProps));
}
}
refChanged = (ref: ?SonarBasePlugin<>) => {
if (this.plugin) {
this.plugin._teardown();
this.plugin = null;
}
const {target} = this.state;
if (ref && target) {
activateMenuItems(ref);
ref._setup(target);
ref._init();
this.plugin = ref;
}
}; };
render() { render() {
@@ -142,15 +130,13 @@ class PluginContainer extends Component<Props, State> {
activePlugin.title activePlugin.title
}" encountered an error during render`} }" encountered an error during render`}
logger={this.props.logger}> logger={this.props.logger}>
{React.createElement( {React.createElement(activePlugin, {
withPluginLifecycleHooks(activePlugin, target), key: pluginKey,
{ logger: this.props.logger,
key: pluginKey, persistedState: pluginStates[pluginKey] || {},
logger: this.props.logger, setPersistedState: state => setPluginState({pluginKey, state}),
persistedState: pluginStates[pluginKey], ref: this.refChanged,
setPersistedState: state => setPluginState({pluginKey, state}), })}
},
)}
</ErrorBoundary> </ErrorBoundary>
</Container> </Container>
<SidebarContainer id="sonarSidebar" /> <SidebarContainer id="sonarSidebar" />

View File

@@ -26,7 +26,10 @@ export default function reducer(
if (action.type === 'SET_PLUGIN_STATE') { if (action.type === 'SET_PLUGIN_STATE') {
return { return {
...state, ...state,
[action.payload.pluginKey]: action.payload.state, [action.payload.pluginKey]: {
...state[action.payload.pluginKey],
...action.payload.state,
},
}; };
} else { } else {
return state; return state;