diff --git a/CliClient/tests/services_CommandService.ts b/CliClient/tests/services_CommandService.ts index 6284bba94..1ddb3a68b 100644 --- a/CliClient/tests/services_CommandService.ts +++ b/CliClient/tests/services_CommandService.ts @@ -275,4 +275,59 @@ describe('services_CommandService', function() { expect(propValue).toBe('hello'); })); + it('should allow isEnabled expressions', asyncTest(async () => { + const service = newService(); + + registerCommand(service, createCommand('test1', { + isEnabled: 'selectedNoteCount == 1 && isMarkdownEditor', + execute: () => {}, + })); + + expect(service.isEnabled('test1', null, { + selectedNoteCount: 2, + isMarkdownEditor: true, + })).toBe(false); + + expect(service.isEnabled('test1', null, { + selectedNoteCount: 1, + isMarkdownEditor: true, + })).toBe(true); + + expect(service.isEnabled('test1', null, { + selectedNoteCount: 1, + isMarkdownEditor: false, + })).toBe(false); + })); + + it('should enable and disable toolbar buttons depending on boolean expression', asyncTest(async () => { + const service = newService(); + const toolbarButtonUtils = new ToolbarButtonUtils(service); + + const state:any = { + selectedNoteIds: ['note1', 'note2'], + }; + + registerCommand(service, createCommand('test1', { + execute: () => {}, + isEnabled: 'selectedNoteCount == 1 && selectedNoteId == note2', + })); + + { + const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons(state, ['test1']); + expect(toolbarInfos[0].enabled).toBe(false); + } + + { + state.selectedNoteIds = ['note1']; + const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons(state, ['test1']); + expect(toolbarInfos[0].enabled).toBe(false); + } + + { + state.selectedNoteIds = ['note2']; + const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons(state, ['test1']); + expect(toolbarInfos[0].enabled).toBe(true); + } + })); + }); diff --git a/ElectronClient/gui/MainScreen/commands/newTodo.ts b/ElectronClient/gui/MainScreen/commands/newTodo.ts index 6683b248c..03f24a38e 100644 --- a/ElectronClient/gui/MainScreen/commands/newTodo.ts +++ b/ElectronClient/gui/MainScreen/commands/newTodo.ts @@ -13,7 +13,7 @@ export const runtime = ():CommandRuntime => { return CommandService.instance().execute('newNote', { template: template, isTodo: true }); }, isEnabled: () => { - return CommandService.instance().isEnabled('newNote', {}); + return CommandService.instance().isEnabled('newNote', {}, null); }, title: () => { return _('New to-do'); diff --git a/ElectronClient/gui/MenuBar.tsx b/ElectronClient/gui/MenuBar.tsx index 1186bda82..4af6eb051 100644 --- a/ElectronClient/gui/MenuBar.tsx +++ b/ElectronClient/gui/MenuBar.tsx @@ -788,7 +788,7 @@ function useMenu(props:Props) { useEffect(() => { for (const commandName in props.menuItemProps) { if (!props.menuItemProps[commandName]) continue; - menuItemSetEnabled(commandName, CommandService.instance().isEnabled(commandName, props.menuItemProps[commandName])); + menuItemSetEnabled(commandName, CommandService.instance().isEnabled(commandName, props.menuItemProps[commandName], null)); } const layoutButtonSequenceOptions = Setting.enumOptions('layoutButtonSequence'); diff --git a/ReactNativeClient/lib/services/CommandService.ts b/ReactNativeClient/lib/services/CommandService.ts index e4c200b02..a94ebec6f 100644 --- a/ReactNativeClient/lib/services/CommandService.ts +++ b/ReactNativeClient/lib/services/CommandService.ts @@ -2,12 +2,15 @@ import eventManager from 'lib/eventManager'; import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from 'lib/markdownUtils'; import BaseService from 'lib/services/BaseService'; import shim from 'lib/shim'; +import BooleanExpression from './BooleanExpression'; type LabelFunction = () => string; +type IsEnabledFunction = (props:any) => boolean; +type IsEnabledExpression = string; export interface CommandRuntime { execute(props:any):Promise - isEnabled?(props:any):boolean + isEnabled?: IsEnabledFunction | IsEnabledExpression; // "state" type is "AppState" but in order not to introduce a // dependency to the desktop app (so that the service can @@ -197,11 +200,23 @@ export default class CommandService extends BaseService { }, 10); } - isEnabled(commandName:string, props:any):boolean { + public booleanExpressionContextFromState(state:any) { + return { + selectedNoteCount: state.selectedNoteIds.length, + selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null, + }; + } + + isEnabled(commandName:string, props:any, booleanExpressionContext:any):boolean { const command = this.commandByName(commandName); if (!command || !command.runtime) return false; - // if (!command.runtime.props) return false; - return command.runtime.isEnabled(props); + + if (typeof command.runtime.isEnabled === 'function') { + return command.runtime.isEnabled(props); + } else { + const exp = new BooleanExpression(command.runtime.isEnabled); + return exp.evaluate(booleanExpressionContext); + } } commandMapStateToProps(commandName:string, state:any):any { diff --git a/ReactNativeClient/lib/services/commands/ToolbarButtonUtils.ts b/ReactNativeClient/lib/services/commands/ToolbarButtonUtils.ts index b7d52ad9a..98335c1c3 100644 --- a/ReactNativeClient/lib/services/commands/ToolbarButtonUtils.ts +++ b/ReactNativeClient/lib/services/commands/ToolbarButtonUtils.ts @@ -35,8 +35,13 @@ export default class ToolbarButtonUtils { return this.service_; } - private commandToToolbarButton(commandName:string, props:any):ToolbarButtonInfo { - if (this.toolbarButtonCache_[commandName] && !propsHaveChanged(this.toolbarButtonCache_[commandName].props, props)) { + private commandToToolbarButton(commandName:string, props:any, booleanExpressionContext:any):ToolbarButtonInfo { + const newEnabled = this.service.isEnabled(commandName, props, booleanExpressionContext); + + if ( + this.toolbarButtonCache_[commandName] && + !propsHaveChanged(this.toolbarButtonCache_[commandName].props, props) && + this.toolbarButtonCache_[commandName].info.enabled === newEnabled) { return this.toolbarButtonCache_[commandName].info; } @@ -46,7 +51,7 @@ export default class ToolbarButtonUtils { name: commandName, tooltip: this.service.label(commandName), iconName: command.declaration.iconName, - enabled: this.service.isEnabled(commandName, props), + enabled: newEnabled, onClick: async () => { this.service.execute(commandName, props); }, @@ -67,6 +72,8 @@ export default class ToolbarButtonUtils { public commandsToToolbarButtons(state:any, commandNames:string[]):ToolbarButtonInfo[] { const output:ToolbarButtonInfo[] = []; + const booleanExpressionContext = this.service.booleanExpressionContextFromState(state); + for (const commandName of commandNames) { if (commandName === '-') { output.push(separatorItem as any); @@ -74,7 +81,7 @@ export default class ToolbarButtonUtils { } const props = this.service.commandMapStateToProps(commandName, state); - output.push(this.commandToToolbarButton(commandName, props)); + output.push(this.commandToToolbarButton(commandName, props, booleanExpressionContext)); } return stateUtils.selectArrayShallow({ array: output }, commandNames.join('_')); diff --git a/Tools/git-changelog.js b/Tools/git-changelog.js index d1bb3c154..50b5b97c9 100644 --- a/Tools/git-changelog.js +++ b/Tools/git-changelog.js @@ -87,7 +87,7 @@ function filterLogs(logs, platform) { if (platform === 'android' && prefix.indexOf('android') >= 0) addIt = true; if (platform === 'ios' && prefix.indexOf('ios') >= 0) addIt = true; if (platform === 'desktop' && prefix.indexOf('desktop') >= 0) addIt = true; - if (platform === 'desktop' && (prefix.indexOf('desktop') >= 0 || prefix.indexOf('api') >= 0)) addIt = true; + if (platform === 'desktop' && (prefix.indexOf('desktop') >= 0 || prefix.indexOf('api') >= 0 || prefix.indexOf('plugins') >= 0)) addIt = true; if (platform === 'cli' && prefix.indexOf('cli') >= 0) addIt = true; if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true; @@ -121,7 +121,7 @@ function formatCommitMessage(msg, author, options) { const isPlatformPrefix = prefix => { prefix = prefix.split(',').map(p => p.trim().toLowerCase()); for (const p of prefix) { - if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all', 'api'].indexOf(p) >= 0) return true; + if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all', 'api', 'plugins'].indexOf(p) >= 0) return true; } return false; }; @@ -129,6 +129,7 @@ function formatCommitMessage(msg, author, options) { if (splitted.length) { const platform = splitted[0].trim().toLowerCase(); if (platform === 'api') subModule = 'api'; + if (platform === 'plugins') subModule = 'plugins'; if (isPlatformPrefix(platform)) { splitted.splice(0, 1); } diff --git a/joplin.code-workspace b/joplin.code-workspace index 3158d69b7..7423a4eef 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -360,7 +360,8 @@ "CliClient/tests/support/plugins/settings/**/node_modules/": true, "CliClient/tests/support/plugins/selected_text/dist/*": true, "CliClient/tests/support/plugins/selected_text/**/node_modules/": true, - "**/CliClient/build/": true + "**/CliClient/build/": true, + "**/*.js": {"when": "$(basename).ts"}, }, "spellright.language": [ "en"