From e57444dc323b1e9b9aab698dfcb09105c9d3b7ed Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Thu, 19 Nov 2020 12:34:49 +0000 Subject: [PATCH] Desktop: Add config screen to add, remove or enable, disable plugins --- .eslintignore | 18 ++ .gitignore | 18 ++ joplin.code-workspace | 3 +- .../app-cli/tests/services_PluginService.ts | 26 +- packages/app-desktop/.gitignore | 1 - packages/app-desktop/app.ts | 31 ++- packages/app-desktop/bridge.ts | 9 +- .../gui/ConfigScreen/ConfigScreen.tsx | 130 +++++++-- .../ConfigScreen/controls/PluginsStates.tsx | 255 ++++++++++++++++++ .../gui/EncryptionConfigScreen.jsx | 2 +- .../NoteBody/CodeMirror/CodeMirror.tsx | 2 +- .../NoteEditor/NoteBody/CodeMirror/Editor.tsx | 26 +- .../NoteBody/TinyMCE/styles/index.ts | 1 + packages/app-desktop/gui/Root.tsx | 4 + .../gui/{dialogs.js => dialogs.ts} | 10 +- .../gui/lib/ToggleButton/ToggleButton.tsx | 37 +++ packages/app-desktop/package-lock.json | 54 +++- packages/app-desktop/package.json | 1 + .../services/plugins/PluginRunner.ts | 8 +- .../lib/components/shared/config-shared.js | 9 + packages/lib/models/Setting.ts | 19 +- packages/lib/services/plugins/Plugin.ts | 16 +- .../lib/services/plugins/PluginService.ts | 188 ++++++++++--- packages/renderer/MdToHtml.ts | 6 +- 24 files changed, 736 insertions(+), 138 deletions(-) create mode 100644 packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.tsx rename packages/app-desktop/gui/{dialogs.js => dialogs.ts} (59%) create mode 100644 packages/app-desktop/gui/lib/ToggleButton/ToggleButton.tsx diff --git a/.eslintignore b/.eslintignore index a4383bc4f7..a73773da94 100644 --- a/.eslintignore +++ b/.eslintignore @@ -241,6 +241,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map packages/app-cli/tests/support/plugins/events/src/index.d.ts packages/app-cli/tests/support/plugins/events/src/index.js packages/app-cli/tests/support/plugins/events/src/index.js.map +packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts +packages/app-cli/tests/support/plugins/jpl_test/api/index.js +packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map +packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts +packages/app-cli/tests/support/plugins/jpl_test/api/types.js +packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map +packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts +packages/app-cli/tests/support/plugins/jpl_test/src/index.js +packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map packages/app-cli/tests/support/plugins/json_export/api/index.d.ts packages/app-cli/tests/support/plugins/json_export/api/index.js packages/app-cli/tests/support/plugins/json_export/api/index.js.map @@ -367,6 +376,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map packages/app-desktop/gui/ConfigScreen/SideBar.d.ts packages/app-desktop/gui/ConfigScreen/SideBar.js packages/app-desktop/gui/ConfigScreen/SideBar.js.map +packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts +packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js +packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map packages/app-desktop/gui/DropboxLoginScreen.d.ts packages/app-desktop/gui/DropboxLoginScreen.js packages/app-desktop/gui/DropboxLoginScreen.js.map @@ -730,6 +742,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map packages/app-desktop/gui/ToolbarButton/styles/index.d.ts packages/app-desktop/gui/ToolbarButton/styles/index.js packages/app-desktop/gui/ToolbarButton/styles/index.js.map +packages/app-desktop/gui/dialogs.d.ts +packages/app-desktop/gui/dialogs.js +packages/app-desktop/gui/dialogs.js.map packages/app-desktop/gui/hooks/useEffectDebugger.d.ts packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useEffectDebugger.js.map @@ -742,6 +757,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map packages/app-desktop/gui/hooks/usePropsDebugger.d.ts packages/app-desktop/gui/hooks/usePropsDebugger.js packages/app-desktop/gui/hooks/usePropsDebugger.js.map +packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts +packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js +packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map packages/app-desktop/gui/menuCommandNames.d.ts packages/app-desktop/gui/menuCommandNames.js packages/app-desktop/gui/menuCommandNames.js.map diff --git a/.gitignore b/.gitignore index e7a685e1a0..7ca8695d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -233,6 +233,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map packages/app-cli/tests/support/plugins/events/src/index.d.ts packages/app-cli/tests/support/plugins/events/src/index.js packages/app-cli/tests/support/plugins/events/src/index.js.map +packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts +packages/app-cli/tests/support/plugins/jpl_test/api/index.js +packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map +packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts +packages/app-cli/tests/support/plugins/jpl_test/api/types.js +packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map +packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts +packages/app-cli/tests/support/plugins/jpl_test/src/index.js +packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map packages/app-cli/tests/support/plugins/json_export/api/index.d.ts packages/app-cli/tests/support/plugins/json_export/api/index.js packages/app-cli/tests/support/plugins/json_export/api/index.js.map @@ -359,6 +368,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map packages/app-desktop/gui/ConfigScreen/SideBar.d.ts packages/app-desktop/gui/ConfigScreen/SideBar.js packages/app-desktop/gui/ConfigScreen/SideBar.js.map +packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts +packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js +packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map packages/app-desktop/gui/DropboxLoginScreen.d.ts packages/app-desktop/gui/DropboxLoginScreen.js packages/app-desktop/gui/DropboxLoginScreen.js.map @@ -722,6 +734,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map packages/app-desktop/gui/ToolbarButton/styles/index.d.ts packages/app-desktop/gui/ToolbarButton/styles/index.js packages/app-desktop/gui/ToolbarButton/styles/index.js.map +packages/app-desktop/gui/dialogs.d.ts +packages/app-desktop/gui/dialogs.js +packages/app-desktop/gui/dialogs.js.map packages/app-desktop/gui/hooks/useEffectDebugger.d.ts packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useEffectDebugger.js.map @@ -734,6 +749,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map packages/app-desktop/gui/hooks/usePropsDebugger.d.ts packages/app-desktop/gui/hooks/usePropsDebugger.js packages/app-desktop/gui/hooks/usePropsDebugger.js.map +packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts +packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js +packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map packages/app-desktop/gui/menuCommandNames.d.ts packages/app-desktop/gui/menuCommandNames.js packages/app-desktop/gui/menuCommandNames.js.map diff --git a/joplin.code-workspace b/joplin.code-workspace index b564ae02dd..8be37e1c40 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -13,6 +13,7 @@ "_vieux/": true, ".gitignore": true, ".eslintignore": true, + "**/*.jpl": true, "./packages/app-cli/**/*.*~": true, "./packages/app-cli/**/*.mo": true, "./packages/app-cli/**/build/": true, @@ -55,7 +56,6 @@ "./packages/app-desktop/**/*.min.js": true, "./packages/app-desktop/**/dist/": true, "./packages/app-desktop/**/gui/note-viewer/pluginAssets/": true, - "./packages/app-desktop/**/lib/": true, "./packages/app-desktop/**/node_modules/": true, "./packages/app-desktop/**/packageInfo.js": true, "./packages/app-desktop/**/pluginAssets/": true, @@ -242,7 +242,6 @@ "packages/app-desktop/**/*.min.js": true, "packages/app-desktop/**/dist/": true, "packages/app-desktop/**/gui/note-viewer/pluginAssets/": true, - "packages/app-desktop/**/lib/": true, "packages/app-desktop/**/node_modules/": true, "packages/app-desktop/**/packageInfo.js": true, "packages/app-desktop/**/pluginAssets/": true, diff --git a/packages/app-cli/tests/services_PluginService.ts b/packages/app-cli/tests/services_PluginService.ts index 6930073a17..5b1fd68ddf 100644 --- a/packages/app-cli/tests/services_PluginService.ts +++ b/packages/app-cli/tests/services_PluginService.ts @@ -4,7 +4,7 @@ import { ContentScriptType } from '@joplin/lib/services/plugins/api/types'; import MdToHtml from '@joplin/renderer/MdToHtml'; import shim from '@joplin/lib/shim'; -const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js'); +const { asyncTest, expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js'); const Note = require('@joplin/lib/models/Note'); const Folder = require('@joplin/lib/models/Folder'); @@ -43,7 +43,7 @@ describe('services_PluginService', function() { it('should load and run a simple plugin', asyncTest(async () => { const service = newPluginService(); - await service.loadAndRunPlugins([`${testPluginDir}/simple`]); + await service.loadAndRunPlugins([`${testPluginDir}/simple`], {}); expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError(); @@ -59,13 +59,13 @@ describe('services_PluginService', function() { it('should load and run a simple plugin and handle trailing slash', asyncTest(async () => { const service = newPluginService(); - await service.loadAndRunPlugins([`${testPluginDir}/simple/`]); + await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {}); expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError(); })); it('should load and run a plugin that uses external packages', asyncTest(async () => { const service = newPluginService(); - await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`]); + await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {}); expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError(); const allFolders = await Folder.all(); @@ -78,7 +78,7 @@ describe('services_PluginService', function() { it('should load multiple plugins from a directory', asyncTest(async () => { const service = newPluginService(); - await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`); + await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {}); const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1'); const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2'); @@ -125,14 +125,14 @@ describe('services_PluginService', function() { it('should load plugins from JS bundle files', asyncTest(async () => { const service = newPluginService(); - await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`); + await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {}); expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true); expect((await Folder.all()).length).toBe(1); })); it('should load plugins from JPL archive', asyncTest(async () => { const service = newPluginService(); - await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`]); + await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {}); expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true); expect((await Folder.all()).length).toBe(1); })); @@ -248,9 +248,15 @@ describe('services_PluginService', function() { ]; for (const testCase of testCases) { - const [appVersion, expected] = testCase; - const plugin = await newPluginService(appVersion as string).loadPluginFromJsBundle('', pluginScript); - expect(plugin.enabled).toBe(expected as boolean); + const [appVersion, hasNoError] = testCase; + const service = newPluginService(appVersion as string); + const plugin = await service.loadPluginFromJsBundle('', pluginScript); + + if (hasNoError) { + await expectNotThrow(() => service.runPlugin(plugin)); + } else { + await expectThrow(() => service.runPlugin(plugin)); + } } })); diff --git a/packages/app-desktop/.gitignore b/packages/app-desktop/.gitignore index 3a1a002031..9d279fe2be 100644 --- a/packages/app-desktop/.gitignore +++ b/packages/app-desktop/.gitignore @@ -1,7 +1,6 @@ node_modules/ packageInfo.js dist/ -lib/ *.min.js .DS_Store gui/note-viewer/pluginAssets/ diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index baf246909e..254748d66b 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -495,12 +495,25 @@ class Application extends BaseApplication { pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' }); pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO); + const service = PluginService.instance(); + const pluginRunner = new PluginRunner(); - PluginService.instance().setLogger(pluginLogger); - PluginService.instance().initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store()); + service.setLogger(pluginLogger); + service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store()); + + const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states')); + + // Users can add and remove plugins from the config screen at any + // time, however we only effectively uninstall the plugin the next + // time the app is started. What plugin should be uninstalled is + // stored in the settings. + const newSettings = await service.uninstallPlugins(pluginSettings); + Setting.setValue('plugins.states', newSettings); try { - if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir')); + if (await shim.fsDriver().exists(Setting.value('pluginDir'))) { + await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings); + } } catch (error) { this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error); } @@ -508,12 +521,12 @@ class Application extends BaseApplication { try { if (Setting.value('plugins.devPluginPaths')) { const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim()); - await PluginService.instance().loadAndRunPlugins(paths); + await service.loadAndRunPlugins(paths, pluginSettings, true); } // Also load dev plugins that have passed via command line arguments if (Setting.value('startupDevPlugins')) { - await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins')); + await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true); } } catch (error) { this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error); @@ -723,6 +736,14 @@ class Application extends BaseApplication { // console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState())); // }, 2000); + // this.dispatch({ + // type: 'NAV_GO', + // routeName: 'Config', + // props: { + // defaultSection: 'plugins', + // }, + // }); + return null; } diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 2f143c4137..ffddcd98f1 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -87,7 +87,7 @@ export class Bridge { return filePath; } - showOpenDialog(options: any) { + showOpenDialog(options: any = null) { const { dialog } = require('electron'); if (!options) options = {}; let fileType = 'file'; @@ -117,13 +117,16 @@ export class Bridge { } showConfirmMessageBox(message: string, options: any = null) { - if (options === null) options = {}; + options = { + buttons: [_('OK'), _('Cancel')], + ...options, + }; const result = this.showMessageBox_(this.window(), Object.assign({}, { type: 'question', message: message, cancelId: 1, - buttons: [_('OK'), _('Cancel')], + buttons: options.buttons, }, options)); return result === 0; diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 173890264e..8c89079d6a 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -3,17 +3,23 @@ import SideBar from './SideBar'; import ButtonBar from './ButtonBar'; import Button, { ButtonLevel } from '../Button/Button'; import { _ } from '@joplin/lib/locale'; +import bridge from '../../services/bridge'; +import Setting from '@joplin/lib/models/Setting'; +import control_PluginsStates from './controls/PluginsStates'; + const { connect } = require('react-redux'); -const Setting = require('@joplin/lib/models/Setting').default; const { themeStyle } = require('@joplin/lib/theme'); const pathUtils = require('@joplin/lib/path-utils'); const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry'); const shared = require('@joplin/lib/components/shared/config-shared.js'); -const bridge = require('electron').remote.require('./bridge').default; const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min'); const { ClipperConfigScreen } = require('../ClipperConfigScreen.min'); const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); +const settingKeyToControl: any = { + 'plugins.states': control_PluginsStates, +}; + class ConfigScreenComponent extends React.Component { rowStyle_: any = null; @@ -27,6 +33,7 @@ class ConfigScreenComponent extends React.Component { selectedSectionName: 'general', screenName: '', changedSettingKeys: [], + needRestart: false, }; this.rowStyle_ = { @@ -41,6 +48,8 @@ class ConfigScreenComponent extends React.Component { this.onCancelClick = this.onCancelClick.bind(this); this.onSaveClick = this.onSaveClick.bind(this); this.onApplyClick = this.onApplyClick.bind(this); + this.renderLabel = this.renderLabel.bind(this); + this.renderDescription = this.renderDescription.bind(this); } async checkSyncConfig_() { @@ -261,6 +270,40 @@ class ConfigScreenComponent extends React.Component { ); } + private labelStyle(themeId: number) { + const theme = themeStyle(themeId); + return Object.assign({}, theme.textStyle, { + display: 'block', + color: theme.color, + fontSize: theme.fontSize * 1.083333, + fontWeight: 500, + marginBottom: theme.mainPadding / 4, + }); + } + + private descriptionStyle(themeId: number) { + const theme = themeStyle(themeId); + return Object.assign({}, theme.textStyle, { + color: theme.colorFaded, + fontStyle: 'italic', + maxWidth: '70em', + marginTop: 5, + }); + } + + private renderLabel(themeId: number, label: string) { + const labelStyle = this.labelStyle(themeId); + return ( +
+ +
+ ); + } + + private renderDescription(themeId: number, description: string) { + return description ?
{description}
: null; + } + settingToComponent(key: string, value: any) { const theme = themeStyle(this.props.themeId); @@ -270,13 +313,7 @@ class ConfigScreenComponent extends React.Component { marginBottom: theme.mainPadding, }; - const labelStyle = Object.assign({}, theme.textStyle, { - display: 'block', - color: theme.color, - fontSize: theme.fontSize * 1.083333, - fontWeight: 500, - marginBottom: theme.mainPadding / 4, - }); + const labelStyle = this.labelStyle(this.props.themeId); const subLabel = Object.assign({}, labelStyle, { display: 'block', @@ -297,13 +334,6 @@ class ConfigScreenComponent extends React.Component { backgroundColor: theme.backgroundColor, }; - const descriptionStyle = Object.assign({}, theme.textStyle, { - color: theme.colorFaded, - marginTop: 5, - fontStyle: 'italic', - maxWidth: '70em', - }); - const textInputBaseStyle = Object.assign({}, controlStyle, { fontFamily: theme.fontFamily, border: '1px solid', @@ -318,18 +348,39 @@ class ConfigScreenComponent extends React.Component { }); const updateSettingValue = (key: string, value: any) => { - // console.info(key + ' = ' + value); - return shared.updateSettingValue(this, key, value); - }; + const md = Setting.settingMetadata(key); + if (md.needRestart) { + this.setState({ needRestart: true }); + } + shared.updateSettingValue(this, key, value); - // Component key needs to be key+value otherwise it doesn't update when the settings change. + if (md.autoSave) { + shared.scheduleSaveSettings(this); + } + }; const md = Setting.settingMetadata(key); const descriptionText = Setting.keyDescription(key, 'desktop'); - const descriptionComp = descriptionText ?
{descriptionText}
: null; + const descriptionComp = this.renderDescription(this.props.themeId, descriptionText); - if (md.isEnum) { + if (settingKeyToControl[key]) { + const SettingComponent = settingKeyToControl[key]; + return ( +
+ {this.renderLabel(this.props.themeId, md.label())} + {this.renderDescription(this.props.themeId, md.description ? md.description() : null)} + { + updateSettingValue(key, event.value); + }} + /> +
+ ); + } else if (md.isEnum) { const items = []; const settingOptions = md.options(); const array = this.keyValueToArray(settingOptions); @@ -568,12 +619,33 @@ class ConfigScreenComponent extends React.Component { return output; } - onApplyClick() { - shared.saveSettings(this); + private restartMessage() { + return _('The application must be restarted for these changes to take effect.'); } - onSaveClick() { + private async restartApp() { + await Setting.saveAll(); + bridge().restart(); + } + + private async checkNeedRestart() { + if (this.state.needRestart) { + const doItNow = await bridge().showConfirmMessageBox(this.restartMessage(), { + buttons: [_('Do it now'), _('Later')], + }); + + if (doItNow) await this.restartApp(); + } + } + + async onApplyClick() { shared.saveSettings(this); + await this.checkNeedRestart(); + } + + async onSaveClick() { + shared.saveSettings(this); + await this.checkNeedRestart(); this.props.dispatch({ type: 'NAV_BACK' }); } @@ -621,6 +693,13 @@ class ConfigScreenComponent extends React.Component { const sections = shared.settingsSections({ device: 'desktop', settings }); + const needRestartComp: any = this.state.needRestart ? ( +
+ {this.restartMessage()} + { this.restartApp(); }}>{_('Restart now')} +
+ ) : null; + return (
{ />
{screenComp} + {needRestartComp}
{settingComps}
props.theme.backgroundColor}; + flex-direction: column; + align-items: flex-start; + padding: 15px; + border: 1px solid ${props => props.theme.dividerColor}; + border-radius: 6px; + width: 250px; + margin-right: 20px; + margin-bottom: 20px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.2); +`; + +const CellTop = styled.div` + display: flex; + flex-direction: row; + width: 100%; + margin-bottom: 10px; +`; + +const CellContent = styled.div` + display: flex; + margin-bottom: 10px; + flex: 1; +`; + +const CellFooter = styled.div` + display: flex; + flex-direction: row; +`; + +const DevModeLabel = styled.div` + border: 1px solid ${props => props.theme.color}; + border-radius: 4px; + padding: 4px 6px; + font-size: ${props => props.theme.fontSize * 0.75}px; + color: ${props => props.theme.color}; +`; + +const StyledName = styled.div` + font-family: ${props => props.theme.fontFamily}; + color: ${props => props.theme.color}; + font-size: ${props => props.theme.fontSize}px; + font-weight: bold; + flex: 1; +`; + +const StyledDescription = styled.div` + font-family: ${props => props.theme.fontFamily}; + color: ${props => props.theme.colorFaded}; + font-size: ${props => props.theme.fontSize}px; + line-height: 1.6em; +`; + +interface Props { + value: any; + themeId: number; + onChange: Function; +} + +interface CellProps { + item: PluginItem; + themeId: number; + onToggle: Function; + onDelete: Function; +} + +interface PluginItem { + id: string; + name: string; + description: string; + enabled: boolean; + deleted: boolean; + devMode: boolean; +} + +function Cell(props: CellProps) { + const { item } = props; + + // For plugins in dev mode things like enabling/disabling or + // uninstalling them doesn't make sense, as that should be done by + // adding/removing them from wherever they were loaded from. + + function renderToggleButton() { + if (item.devMode) { + return DEV; + } + + return props.onToggle({ item: props.item })} + />; + } + + function renderFooter() { + if (item.devMode) return null; + + return ( + +