Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: Install default plugins on first-app-start #6585

Merged
merged 34 commits into from
Sep 1, 2022

Conversation

mak2002
Copy link
Contributor

@mak2002 mak2002 commented Jun 16, 2022

Hey,
This is a draft PR for installing default plugins. To implement this, I am installing plugins before the code that loads and runs the plugins, as to avoid restarting.

One issue I noticed is that, after this installation is complete, if we searched any plugin that we installed, we still see the "Install' button instead of "Installed". But after restarting the app, it displays it correctly as ''Installed". I think we need to update pluginSettings in order to fix this. But apart from that I didn't notice any issue.

Feel free to give any suggestions you have.

@CalebJohn CalebJohn marked this pull request as draft June 16, 2022 16:21
@CalebJohn
Copy link
Collaborator

@mak2002 For future reference, you can mark a PR as draft while creating it or after creating the PR

@@ -416,7 +416,7 @@ export default class PluginService extends BaseService {
return this.installPluginFromRepo(repoApi, pluginId);
}

public async installPlugin(jplPath: string): Promise<Plugin> {
public async installPlugin(jplPath: string, defaultPlugin: boolean = false): Promise<Plugin> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer we keep this method generic, with no mention of default plugins. Why is this option needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If defaultPlugin is true, then we won't be loading the plugins right away after installing them. As we are installing default plugins before loading regular plugins, we just need to load them once along with regular plugins.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok then please name the option as loadPlugin, defaults to true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
Setting.setValue('defaultPlugins', service.serializePluginSettings(pluginSettings));
return pluginSettings;
} else { return null; }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why return null here? Just return the unmodified PluginSettings? Then the called doesn't need to have logic to deal with either null or an object.

await service.installPlugin(defaultPluginPath, false);

pluginSettings = produce(pluginSettings, (draft: PluginSettings) => {
draft[plugin.path.replace('.jpl', '')] = defaultPluginSetting();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use lib/path to remove the file extension. It's more robust than a search and replace

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -416,7 +416,7 @@ export default class PluginService extends BaseService {
return this.installPluginFromRepo(repoApi, pluginId);
}

public async installPlugin(jplPath: string): Promise<Plugin> {
public async installPlugin(jplPath: string, loadPlugin: boolean = true): Promise<Plugin> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise<Plugin | null>

}
Setting.setValue('defaultPlugins', service.serializePluginSettings(pluginSettings));
return pluginSettings;
} else { return pluginSettings; }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's just me but generally I try to reduce indentation as much as possible, as it makes it easier to follow the logic. In this case, you could have an early exit at the top (if (!Setting.value('firstStart')) return pluginSettings;), and then unindent all the code below.

public: true,
appTypes: [AppType.Desktop],
storage: SettingStorage.Database,
},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this is a dynamic settings? Feels like it should just be a constant since it doesn't change for a given version?

Copy link
Contributor Author

@mak2002 mak2002 Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, It won't change for a given version. Are you suggesting I put it in some const variable? Because I think by putting defaultPlugins in Setting.ts it will be easier to retrieve it from anywhere.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just put a defaultPlugins.ts file in lib/services/plugins. That way it can be easily included anywhere. Putting it in Setting.ts could cause dependency loops since this file is already included in many places.

Copy link
Contributor Author

@mak2002 mak2002 Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I was thinking of making this defaultPlugins.ts as a class which will contain all the plugins' path, IDs etc. Do you think I should go ahead with this? Or should I just put it in PluginService.ts which is also in lib/services/plugins

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a constant, just make it constant? Anything else I don't know

Comment on lines 1507 to 1513
updatedDefaultPluginsInstallStates: {
value: false,
type: SettingItemType.Bool,
public: false,
appTypes: [AppType.Desktop],
storage: SettingStorage.File,
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this won't cause a dependency loop. Please correct me if I am wrong.

@laurent22
Copy link
Owner

Please try to make progress on this PR. In general we prefer not to keep draft PR for too long. If you're not sure what you need to do to finish please ask us.

@mak2002
Copy link
Contributor Author

mak2002 commented Jun 26, 2022

Sorry for making slow progress. I had some college stuff going on last week, so that's why less activity.
In the upcoming week, I will add tests and some initial settings for plugins. If there is anything other than this that I should do, please let me know.

@laurent22
Copy link
Owner

Ok no problem, take your time in that case. The thing with draft PRs is that I stumble upon them... and I can't do much because it doesn't make sense to review if it's not finished (it already takes me forever to review PRs that are supposed to be finished). Perhaps we will not allow draft PRs in the future, especially during GSoC.

@mak2002
Copy link
Contributor Author

mak2002 commented Jun 29, 2022

Hey, Now the only thing remaining is tests for defaultPluginsSettings.

@mak2002
Copy link
Contributor Author

mak2002 commented Jul 2, 2022

Hey, The PR is ready to be reviewed now!
I have included .jpl files of some plugins for now but will remove it before merging as there will be another script handling this part.

@mak2002 mak2002 marked this pull request as ready for review July 2, 2022 08:13
@mak2002
Copy link
Contributor Author

mak2002 commented Jul 11, 2022

Tests are passing now. Don't know why they were failing randomly.

Copy link
Owner

@laurent22 laurent22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mak2002, I'm struggling to provide guidance in your PRs because it's not small logic issues here and there - I feel some of it should be rearchitectured or rewritten.

I'm concerned about merging your code at this point as it's touching critical parts of the codebase.

@@ -273,6 +274,11 @@ class Application extends BaseApplication {
}

try {
const devEnv = Setting.constants_.env === Env.Dev;
let pluginsPath = '';
devEnv ? pluginsPath = path.join(__dirname, '..', 'app-desktop/build/defaultPlugins/') : pluginsPath = path.join(process.resourcesPath, 'build/defaultPlugins/');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use bridge().buildDir()

} else { return null; }
}

public async installDefaultPlugins(pathToPlugins: string, pluginSettings: PluginSettings, service: PluginService): Promise<PluginSettings> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why you pass the service to itself...

Copy link
Contributor Author

@mak2002 mak2002 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's not necessary. I will remove it.

draft[filename(plugin.path)] = defaultPluginSetting();
});
}
Setting.setValue('firstStart', 0);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why you are setting firstStart here??

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact this all firstStart is probably incorrect. It means that default plugins won't be enabled for existing installations. You need to have your own check to find out if a particular default plugin is already installed or not before installing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have now removed the first-app-start logic and implemented my own check.

}

public setSettingsForDefaultPlugins(initialSettings: InitialSettings) {
Object.keys(initialSettings).forEach(pluginId => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see checks in here whether the settings are already set or not. It sounds like you'd overwrite user settings on each start.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, I have added checks here.

settings[pluginId] = defaultPluginSetting();
});
return settings;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this function doesn't do anything useful. Also you are duplicating the list of default plugin IDs here and in the place where settings are defined. That list should only appear once in the codebase.

componentDidMount() {
if (this.props.defaultSection) {
this.setState({ selectedSectionName: this.props.defaultSection }, () => {
this.switchSection(this.props.defaultSection);
});
}
if (!Setting.value('updatedDefaultPluginsInstallStates')) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it mean the plugin settings will be set only once? How about when we add a new default plugin?

Copy link
Contributor Author

@mak2002 mak2002 Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now checking in installedDefaultPlugins array to see if new default plugin is available.

@mak2002
Copy link
Contributor Author

mak2002 commented Jul 22, 2022

Hey @laurent22,
Sorry for causing trouble for you. I think I have missed some cases like adding new default plugin, duplication of plugin IDs. I will fix issues which you mentioned above asap. I will make sure all of my code is ready to merge.

} catch (error) {
this.logger().error(`There was an error loading default plugins from ${Setting.value('pluginDir')}:`, error);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move new functionality into a default Plugins module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I was thinking of a way to only call function to install default plugins here, I will see what I can do.

packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx Outdated Show resolved Hide resolved
@@ -60,12 +61,28 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.setState({ settings: this.props.settings });
}

updatePluginsStates(value: any) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a descriptive parameter name value should be newPluginStates. And please add typing.

packages/app-desktop/app.ts Outdated Show resolved Hide resolved
}

// this is used for setting initial "installed" state for plugins
public setInstalledState(): any {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename to something descriptive (e.g. getDefaultPluginInstallState). And please add a return type.

Comment on lines 1564 to 1577
setInitialDefaultPluginsSettings: {
value: [],
type: SettingItemType.Array,
public: false,
storage: SettingStorage.File,
},

preInstalledDefaultPlugins: {
value: '',
type: SettingItemType.Object,
public: false,
storage: SettingStorage.File,
},

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these in favour of using the installedDefaultPlugins field alone.


export const initialSettings: InitialSettings = {
'io.github.jackgruber.backup': {
'path': `${Setting.value('profileDir')}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember you suggested putting this into .config or json files, but we may need to use variables here, so that's why I put it in ts file.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a discussion about this? Sorry if I missed it or if I'm repeating something but I think it's indeed ok to have the config directly in .ts file since it's not really dynamic (it's tied to the current build).

As previously discussed however we need all data related to the default plugins we support to be in the same place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a discussion about this?

I had discussion with my mentor CalebJohn about this. And I will make sure we put all the default plugins related data in the same place.

@@ -0,0 +1,210 @@
import { installDefaultPlugins, getDefaultPluginsInstallState, setSettingsForDefaultPlugins, checkPreInstalledDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import PluginRunner from '../../../app/services/plugins/PluginRunner';
import * as fs from 'fs-extra';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't import*. Only import the functions that you need

value: [],
type: SettingItemType.Array,
public: false,
storage: SettingStorage.File,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be database here

// this method checks if the 'value' passed is present in the Setting "Array"
// If yes, then it just returns 'true'. If its not present then, it will
// update it and return 'false'
static checkArrayAndUpdate(settingName: string, value: string): boolean {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe call it setArrayValue. Make the method public

'path': `${Setting.value('profileDir')}`,
},
};
return initialSettings;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to have a single object with all plugin related properties in there. For example:

export const defaultPlugins = {
	'io.github.jackgruber.backup': {
		version: '1.0.2',
		settings: {
			'path': `${Setting.value('profileDir')}`,
		},
	'plugin.calebjohn.rich-markdown': {
		version: '0.8.3',
	},	
};

That way you have everything in one place and don't need to repeat the key for each piece of info you want to add.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way it is a lot cleaner.

@@ -49,6 +49,7 @@ export default function manifestFromObject(o: any): PluginManifest {
permissions: permissions,

_recommended: getBoolean('_recommended', false, false),
_built_in: getBoolean('_built_in', false, false),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that used somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently. But I thought maybe we can use it in the future. For example, if we decided to have a special badge for default plugins. But if you want, I will remove it.

@laurent22
Copy link
Owner

Thanks for the update, the fact that most of your code is now in a utility file makes this feature easier to manage.

Does your code now handle updates correctly?

  • What if a new default plugin is added? Does it get correctly added to the user's plugins?
  • Does it handle correctly existing user (who already have settings) and new users (who don't)?
  • What happens when a default plugin setting changes?
    • Does it get correctly set for the user?
    • What happens when the user has already set that setting to a different value?

@mak2002
Copy link
Contributor Author

mak2002 commented Sep 1, 2022

  • What if a new default plugin is added? Does it get correctly added to the user's plugins?

Yes. The new plugin gets installed and added correctly to user's plugins.

  • Does it handle correctly existing user (who already have settings) and new users (who don't)?

Yes, It does. If the user already has some default plugins installed, then it will skip the installation and won't set default settings for them.

What happens when a default plugin setting changes?

  • Does it get correctly set for the user?
  • What happens when the user has already set that setting to a different value?

Do you mean if we change the default plugins settings from the code? If yes, then it won't affect users who already have default plugins installed. It will only affect the users who will be getting default plugins for first time.

@laurent22
Copy link
Owner

Thanks for clarifying @mak2002, let's merge!

@laurent22 laurent22 merged commit 01f4bb0 into laurent22:dev Sep 1, 2022
@mak2002
Copy link
Contributor Author

mak2002 commented Sep 2, 2022

Hey @laurent22 , Can we revert this commit so that I can remove some plugin.jpl files that I had kept for demo purposes? Sorry for the inconvenience.

@CalebJohn
Copy link
Collaborator

@mak2002 for now please just make a PR removing the files. That way Laurent can decide how he wants to proceed when he sees this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants