Desktop: Fixed issue which could cause plugin views to be orphaned

pull/4496/head
Laurent Cozic 2021-02-07 16:47:56 +00:00
parent 26bce33e98
commit ce8f156f51
3 changed files with 69 additions and 20 deletions

View File

@ -121,6 +121,7 @@ export interface AppState extends State {
visibleDialogs: any; // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
focusedField: string;
layoutMoveMode: boolean;
startupPluginsLoaded: boolean;
// Extra reducer keys go here
watchedResources: any;
@ -144,11 +145,14 @@ const appDefaultState: AppState = {
focusedField: null,
layoutMoveMode: false,
mainLayout: null,
startupPluginsLoaded: false,
...resourceEditWatcherDefaultState,
};
class Application extends BaseApplication {
private checkAllPluginStartedIID_: any = null;
constructor() {
super();
@ -200,6 +204,20 @@ class Application extends BaseApplication {
}
break;
case 'STARTUP_PLUGINS_LOADED':
// When all startup plugins have loaded, we also recreate the
// main layout to ensure that it is updated in the UI. There's
// probably a cleaner way to do this, but for now that will do.
if (state.startupPluginsLoaded !== action.value) {
newState = {
...newState,
startupPluginsLoaded: action.value,
mainLayout: JSON.parse(JSON.stringify(newState.mainLayout)),
};
}
break;
case 'WINDOW_CONTENT_SIZE_SET':
newState = Object.assign({}, state);
@ -552,6 +570,16 @@ class Application extends BaseApplication {
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
}
this.checkAllPluginStartedIID_ = setInterval(() => {
if (service.allPluginsStarted) {
clearInterval(this.checkAllPluginStartedIID_);
this.dispatch({
type: 'STARTUP_PLUGINS_LOADED',
value: true,
});
}
}, 500);
}
async start(argv: string[]): Promise<any> {

View File

@ -62,6 +62,7 @@ interface Props {
themeId: number;
settingEditorCodeView: boolean;
pluginsLegacy: any;
startupPluginsLoaded: boolean;
}
interface State {
@ -616,19 +617,21 @@ class MainScreenComponent extends React.Component<Props, State> {
if (components[key]) return components[key]();
const viewsToRemove: string[] = [];
if (key.indexOf('plugin-view') === 0) {
const viewInfo = pluginUtils.viewInfoByViewId(this.props.plugins, event.item.key);
if (!viewInfo) {
// Note that it will happen when the component is rendered
// before the plugins have loaded their views, so because of
// this we need to keep the view in the layout.
// Once all startup plugins have loaded, we know that all the
// views are ready so we can remove the orphans ones.
//
// But it can also be a problem if the view really is invalid
// due to a faulty plugin as currently there would be no way to
// remove it.
console.warn(`Could not find plugin associated with view: ${event.item.key}`);
return null;
// Before they are loaded, there might be views that don't match
// any plugins, but that's only because it hasn't loaded yet.
if (this.props.startupPluginsLoaded) {
console.warn(`Could not find plugin associated with view: ${event.item.key}`);
viewsToRemove.push(event.item.key);
}
} else {
const { view, plugin } = viewInfo;
@ -647,19 +650,19 @@ class MainScreenComponent extends React.Component<Props, State> {
throw new Error(`Invalid layout component: ${key}`);
}
// if (viewsToRemove.length) {
// window.requestAnimationFrame(() => {
// let newLayout = this.props.mainLayout;
// for (const itemKey of viewsToRemove) {
// newLayout = removeItem(newLayout, itemKey);
// }
if (viewsToRemove.length) {
window.requestAnimationFrame(() => {
let newLayout = this.props.mainLayout;
for (const itemKey of viewsToRemove) {
newLayout = removeItem(newLayout, itemKey);
}
// if (newLayout !== this.props.mainLayout) {
// console.warn('Removed invalid views:', viewsToRemove);
// this.updateMainLayout(newLayout);
// }
// });
// }
if (newLayout !== this.props.mainLayout) {
console.warn('Removed invalid views:', viewsToRemove);
this.updateMainLayout(newLayout);
}
});
}
}
renderPluginDialogs() {
@ -773,6 +776,7 @@ const mapStateToProps = (state: AppState) => {
focusedField: state.focusedField,
layoutMoveMode: state.layoutMoveMode,
mainLayout: state.mainLayout,
startupPluginsLoaded: state.startupPluginsLoaded,
};
};

View File

@ -73,6 +73,7 @@ export default class PluginService extends BaseService {
private platformImplementation_: any = null;
private plugins_: Plugins = {};
private runner_: BasePluginRunner = null;
private startedPlugins_: Record<string, boolean> = {};
public initialize(appVersion: string, platformImplementation: any, runner: BasePluginRunner, store: any) {
this.appVersion_ = appVersion;
@ -337,6 +338,13 @@ export default class PluginService extends BaseService {
return compareVersions(this.appVersion_, pluginVersion) >= 0;
}
public get allPluginsStarted(): boolean {
for (const pluginId of Object.keys(this.startedPlugins_)) {
if (!this.startedPlugins_[pluginId]) return false;
}
return true;
}
public async runPlugin(plugin: Plugin) {
if (!this.isCompatible(plugin.manifest.app_min_version)) {
throw new Error(`Plugin "${plugin.id}" was disabled because it requires Joplin version ${plugin.manifest.app_min_version} and current version is ${this.appVersion_}.`);
@ -351,6 +359,15 @@ export default class PluginService extends BaseService {
});
}
this.startedPlugins_[plugin.id] = false;
const onStarted = () => {
this.startedPlugins_[plugin.id] = true;
plugin.off('started', onStarted);
};
plugin.on('started', onStarted);
const pluginApi = new Global(this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi);
}