1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Desktop: Simplified and improve command service, and added command palette

- Commands "enabled" state is now expressed using a "when-clause" like in VSCode
- A command palette has been added to the Tools menu
This commit is contained in:
Laurent Cozic 2020-10-18 21:52:10 +01:00
parent f529adac99
commit 3a57cfea02
78 changed files with 897 additions and 756 deletions

View File

@ -114,7 +114,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
ElectronClient/gui/MainScreen/commands/toggleEditors.js ElectronClient/gui/MainScreen/commands/toggleEditors.js
ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js
ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleSideBar.js
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MainScreen/MainScreen.js ElectronClient/gui/MainScreen/MainScreen.js
ElectronClient/gui/MenuBar.js ElectronClient/gui/MenuBar.js
@ -187,7 +187,9 @@ ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js ElectronClient/gui/ToolbarButton/ToolbarButton.js
ElectronClient/gui/utils/NoteListUtils.js ElectronClient/gui/utils/NoteListUtils.js
ElectronClient/InteropServiceHelper.js ElectronClient/InteropServiceHelper.js
ElectronClient/plugins/GotoAnything.js
ElectronClient/services/bridge.js ElectronClient/services/bridge.js
ElectronClient/services/commands/types.js
ElectronClient/services/plugins/hooks/useThemeCss.js ElectronClient/services/plugins/hooks/useThemeCss.js
ElectronClient/services/plugins/hooks/useViewIsReady.js ElectronClient/services/plugins/hooks/useViewIsReady.js
ElectronClient/services/plugins/PlatformImplementation.js ElectronClient/services/plugins/PlatformImplementation.js
@ -235,9 +237,10 @@ ReactNativeClient/lib/services/AlarmServiceDriver.android.js
ReactNativeClient/lib/services/AlarmServiceDriver.ios.js ReactNativeClient/lib/services/AlarmServiceDriver.ios.js
ReactNativeClient/lib/services/AlarmServiceDriverNode.js ReactNativeClient/lib/services/AlarmServiceDriverNode.js
ReactNativeClient/lib/services/BaseService.js ReactNativeClient/lib/services/BaseService.js
ReactNativeClient/lib/services/BooleanExpression.js ReactNativeClient/lib/services/commands/commandsToMarkdownTable.js
ReactNativeClient/lib/services/commands/MenuUtils.js ReactNativeClient/lib/services/commands/MenuUtils.js
ReactNativeClient/lib/services/commands/propsHaveChanged.js ReactNativeClient/lib/services/commands/propsHaveChanged.js
ReactNativeClient/lib/services/commands/stateToWhenClauseContext.js
ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js
ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/contextkey/contextkey.js ReactNativeClient/lib/services/contextkey/contextkey.js
@ -309,6 +312,7 @@ ReactNativeClient/lib/services/synchronizer/migrations/1.js
ReactNativeClient/lib/services/synchronizer/migrations/2.js ReactNativeClient/lib/services/synchronizer/migrations/2.js
ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/synchronizer/utils/types.js
ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/services/UndoRedoService.js
ReactNativeClient/lib/services/WhenClause.js
ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/ShareExtension.js
ReactNativeClient/lib/shareHandler.js ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/shim.js ReactNativeClient/lib/shim.js

8
.gitignore vendored
View File

@ -108,7 +108,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
ElectronClient/gui/MainScreen/commands/toggleEditors.js ElectronClient/gui/MainScreen/commands/toggleEditors.js
ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js
ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleSideBar.js
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MainScreen/MainScreen.js ElectronClient/gui/MainScreen/MainScreen.js
ElectronClient/gui/MenuBar.js ElectronClient/gui/MenuBar.js
@ -181,7 +181,9 @@ ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js ElectronClient/gui/ToolbarButton/ToolbarButton.js
ElectronClient/gui/utils/NoteListUtils.js ElectronClient/gui/utils/NoteListUtils.js
ElectronClient/InteropServiceHelper.js ElectronClient/InteropServiceHelper.js
ElectronClient/plugins/GotoAnything.js
ElectronClient/services/bridge.js ElectronClient/services/bridge.js
ElectronClient/services/commands/types.js
ElectronClient/services/plugins/hooks/useThemeCss.js ElectronClient/services/plugins/hooks/useThemeCss.js
ElectronClient/services/plugins/hooks/useViewIsReady.js ElectronClient/services/plugins/hooks/useViewIsReady.js
ElectronClient/services/plugins/PlatformImplementation.js ElectronClient/services/plugins/PlatformImplementation.js
@ -229,9 +231,10 @@ ReactNativeClient/lib/services/AlarmServiceDriver.android.js
ReactNativeClient/lib/services/AlarmServiceDriver.ios.js ReactNativeClient/lib/services/AlarmServiceDriver.ios.js
ReactNativeClient/lib/services/AlarmServiceDriverNode.js ReactNativeClient/lib/services/AlarmServiceDriverNode.js
ReactNativeClient/lib/services/BaseService.js ReactNativeClient/lib/services/BaseService.js
ReactNativeClient/lib/services/BooleanExpression.js ReactNativeClient/lib/services/commands/commandsToMarkdownTable.js
ReactNativeClient/lib/services/commands/MenuUtils.js ReactNativeClient/lib/services/commands/MenuUtils.js
ReactNativeClient/lib/services/commands/propsHaveChanged.js ReactNativeClient/lib/services/commands/propsHaveChanged.js
ReactNativeClient/lib/services/commands/stateToWhenClauseContext.js
ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js
ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/contextkey/contextkey.js ReactNativeClient/lib/services/contextkey/contextkey.js
@ -303,6 +306,7 @@ ReactNativeClient/lib/services/synchronizer/migrations/1.js
ReactNativeClient/lib/services/synchronizer/migrations/2.js ReactNativeClient/lib/services/synchronizer/migrations/2.js
ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/synchronizer/utils/types.js
ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/services/UndoRedoService.js
ReactNativeClient/lib/services/WhenClause.js
ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/ShareExtension.js
ReactNativeClient/lib/shareHandler.js ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/shim.js ReactNativeClient/lib/shim.js

View File

@ -57,7 +57,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
ElectronClient/gui/MainScreen/commands/toggleEditors.js ElectronClient/gui/MainScreen/commands/toggleEditors.js
ElectronClient/gui/MainScreen/commands/toggleNoteList.js ElectronClient/gui/MainScreen/commands/toggleNoteList.js
ElectronClient/gui/MainScreen/commands/toggleSidebar.js ElectronClient/gui/MainScreen/commands/toggleSideBar.js
ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MainScreen/MainScreen.js ElectronClient/gui/MainScreen/MainScreen.js
ElectronClient/gui/MenuBar.js ElectronClient/gui/MenuBar.js
@ -130,7 +130,9 @@ ElectronClient/gui/ToolbarButton/styles/index.js
ElectronClient/gui/ToolbarButton/ToolbarButton.js ElectronClient/gui/ToolbarButton/ToolbarButton.js
ElectronClient/gui/utils/NoteListUtils.js ElectronClient/gui/utils/NoteListUtils.js
ElectronClient/InteropServiceHelper.js ElectronClient/InteropServiceHelper.js
ElectronClient/plugins/GotoAnything.js
ElectronClient/services/bridge.js ElectronClient/services/bridge.js
ElectronClient/services/commands/types.js
ElectronClient/services/plugins/hooks/useThemeCss.js ElectronClient/services/plugins/hooks/useThemeCss.js
ElectronClient/services/plugins/hooks/useViewIsReady.js ElectronClient/services/plugins/hooks/useViewIsReady.js
ElectronClient/services/plugins/PlatformImplementation.js ElectronClient/services/plugins/PlatformImplementation.js
@ -178,9 +180,10 @@ ReactNativeClient/lib/services/AlarmServiceDriver.android.js
ReactNativeClient/lib/services/AlarmServiceDriver.ios.js ReactNativeClient/lib/services/AlarmServiceDriver.ios.js
ReactNativeClient/lib/services/AlarmServiceDriverNode.js ReactNativeClient/lib/services/AlarmServiceDriverNode.js
ReactNativeClient/lib/services/BaseService.js ReactNativeClient/lib/services/BaseService.js
ReactNativeClient/lib/services/BooleanExpression.js ReactNativeClient/lib/services/commands/commandsToMarkdownTable.js
ReactNativeClient/lib/services/commands/MenuUtils.js ReactNativeClient/lib/services/commands/MenuUtils.js
ReactNativeClient/lib/services/commands/propsHaveChanged.js ReactNativeClient/lib/services/commands/propsHaveChanged.js
ReactNativeClient/lib/services/commands/stateToWhenClauseContext.js
ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js
ReactNativeClient/lib/services/CommandService.js ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/contextkey/contextkey.js ReactNativeClient/lib/services/contextkey/contextkey.js
@ -252,6 +255,7 @@ ReactNativeClient/lib/services/synchronizer/migrations/1.js
ReactNativeClient/lib/services/synchronizer/migrations/2.js ReactNativeClient/lib/services/synchronizer/migrations/2.js
ReactNativeClient/lib/services/synchronizer/utils/types.js ReactNativeClient/lib/services/synchronizer/utils/types.js
ReactNativeClient/lib/services/UndoRedoService.js ReactNativeClient/lib/services/UndoRedoService.js
ReactNativeClient/lib/services/WhenClause.js
ReactNativeClient/lib/ShareExtension.js ReactNativeClient/lib/ShareExtension.js
ReactNativeClient/lib/shareHandler.js ReactNativeClient/lib/shareHandler.js
ReactNativeClient/lib/shim.js ReactNativeClient/lib/shim.js

View File

@ -2,7 +2,7 @@ import MenuUtils from 'lib/services/commands/MenuUtils';
import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils';
import CommandService, { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService'; import CommandService, { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient } = require('test-utils.js'); const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow } = require('test-utils.js');
interface TestCommand { interface TestCommand {
declaration: CommandDeclaration, declaration: CommandDeclaration,
@ -11,7 +11,12 @@ interface TestCommand {
function newService():CommandService { function newService():CommandService {
const service = new CommandService(); const service = new CommandService();
service.initialize({}); const mockStore = {
getState: () => {
return {};
},
};
service.initialize(mockStore, true);
return service; return service;
} }
@ -24,8 +29,7 @@ function createCommand(name:string, options:any):TestCommand {
execute: options.execute, execute: options.execute,
}; };
if (options.mapStateToProps) runtime.mapStateToProps = options.mapStateToProps; if (options.enabledCondition) runtime.enabledCondition = options.enabledCondition;
if (options.isEnabled) runtime.isEnabled = options.isEnabled;
return { declaration, runtime }; return { declaration, runtime };
} }
@ -61,7 +65,7 @@ describe('services_CommandService', function() {
}, },
})); }));
const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons({}, ['test1', 'test2']); const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {});
await toolbarInfos[0].onClick(); await toolbarInfos[0].onClick();
await toolbarInfos[1].onClick(); await toolbarInfos[1].onClick();
@ -77,98 +81,78 @@ describe('services_CommandService', function() {
registerCommand(service, createCommand('test1', { registerCommand(service, createCommand('test1', {
execute: () => {}, execute: () => {},
mapStateToProps: (state:any) => { enabledCondition: 'oneNoteSelected',
return {
selectedNoteId: state.selectedNoteId,
selectedFolderId: state.selectedFolderId,
};
},
isEnabled: (props:any) => {
return props.selectedNoteId === 'abc';
},
})); }));
registerCommand(service, createCommand('test2', { registerCommand(service, createCommand('test2', {
execute: () => {}, execute: () => {},
mapStateToProps: (state:any) => { enabledCondition: 'multipleNotesSelected',
return {
selectedNoteId: state.selectedNoteId,
selectedFolderId: state.selectedFolderId,
};
},
isEnabled: (props:any) => {
return props.selectedNoteId === '123';
},
})); }));
const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons({ const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
selectedNoteId: '123', oneNoteSelected: false,
selectedFolderId: 'aaa', multipleNotesSelected: true,
}, ['test1', 'test2']); });
expect(toolbarInfos[0].enabled).toBe(false); expect(toolbarInfos[0].enabled).toBe(false);
expect(toolbarInfos[1].enabled).toBe(true); expect(toolbarInfos[1].enabled).toBe(true);
})); }));
it('should enable commands by default', asyncTest(async () => {
const service = newService();
registerCommand(service, createCommand('test1', {
execute: () => {},
}));
expect(service.isEnabled('test1', {})).toBe(true);
}));
it('should return the same toolbarButtons array if nothing has changed', asyncTest(async () => { it('should return the same toolbarButtons array if nothing has changed', asyncTest(async () => {
const service = newService(); const service = newService();
const toolbarButtonUtils = new ToolbarButtonUtils(service); const toolbarButtonUtils = new ToolbarButtonUtils(service);
registerCommand(service, createCommand('test1', { registerCommand(service, createCommand('test1', {
execute: () => {}, execute: () => {},
mapStateToProps: (state:any) => { enabledCondition: 'cond1',
return {
selectedNoteId: state.selectedNoteId,
};
},
isEnabled: (props:any) => {
return props.selectedNoteId === 'ok';
},
})); }));
registerCommand(service, createCommand('test2', { registerCommand(service, createCommand('test2', {
execute: () => {}, execute: () => {},
mapStateToProps: (state:any) => { enabledCondition: 'cond2',
return {
selectedFolderId: state.selectedFolderId,
};
},
isEnabled: (props:any) => {
return props.selectedFolderId === 'ok';
},
})); }));
const toolbarInfos1 = toolbarButtonUtils.commandsToToolbarButtons({ const toolbarInfos1 = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
selectedNoteId: 'ok', cond1: true,
selectedFolderId: 'notok', cond2: false,
}, ['test1', 'test2']); });
const toolbarInfos2 = toolbarButtonUtils.commandsToToolbarButtons({ const toolbarInfos2 = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
selectedNoteId: 'ok', cond1: true,
selectedFolderId: 'notok', cond2: false,
}, ['test1', 'test2']); });
expect(toolbarInfos1).toBe(toolbarInfos2); expect(toolbarInfos1).toBe(toolbarInfos2);
expect(toolbarInfos1[0] === toolbarInfos2[0]).toBe(true); expect(toolbarInfos1[0] === toolbarInfos2[0]).toBe(true);
expect(toolbarInfos1[1] === toolbarInfos2[1]).toBe(true); expect(toolbarInfos1[1] === toolbarInfos2[1]).toBe(true);
const toolbarInfos3 = toolbarButtonUtils.commandsToToolbarButtons({ const toolbarInfos3 = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
selectedNoteId: 'ok', cond1: true,
selectedFolderId: 'ok', cond2: true,
}, ['test1', 'test2']); });
expect(toolbarInfos2 === toolbarInfos3).toBe(false); expect(toolbarInfos2 === toolbarInfos3).toBe(false);
expect(toolbarInfos2[0] === toolbarInfos3[0]).toBe(true); expect(toolbarInfos2[0] === toolbarInfos3[0]).toBe(true);
expect(toolbarInfos2[1] === toolbarInfos3[1]).toBe(false); expect(toolbarInfos2[1] === toolbarInfos3[1]).toBe(false);
{ {
expect(toolbarButtonUtils.commandsToToolbarButtons({ expect(toolbarButtonUtils.commandsToToolbarButtons(['test1', '-', 'test2'], {
selectedNoteId: 'ok', cond1: true,
selectedFolderId: 'notok', cond2: false,
}, ['test1', '-', 'test2'])).toBe(toolbarButtonUtils.commandsToToolbarButtons({ })).toBe(toolbarButtonUtils.commandsToToolbarButtons(['test1', '-', 'test2'], {
selectedNoteId: 'ok', cond1: true,
selectedFolderId: 'notok', cond2: false,
}, ['test1', '-', 'test2'])); }));
} }
})); }));
@ -206,50 +190,37 @@ describe('services_CommandService', function() {
const utils = new MenuUtils(service); const utils = new MenuUtils(service);
registerCommand(service, createCommand('test1', { registerCommand(service, createCommand('test1', {
mapStateToProps: (state:any) => {
return {
isOk: state.test1 === 'ok',
};
},
execute: () => {}, execute: () => {},
enabledCondition: 'cond1',
})); }));
registerCommand(service, createCommand('test2', { registerCommand(service, createCommand('test2', {
mapStateToProps: (state:any) => {
return {
isOk: state.test2 === 'ok',
};
},
execute: () => {}, execute: () => {},
enabledCondition: 'cond2',
})); }));
{ {
const menuItemProps = utils.commandsToMenuItemProps({ const menuItemProps = utils.commandsToMenuItemProps(['test1', 'test2'], {
test1: 'ok', cond1: true,
test2: 'notok', cond2: false,
}, ['test1', 'test2']); });
expect(menuItemProps.test1.isOk).toBe(true); expect(menuItemProps.test1.enabled).toBe(true);
expect(menuItemProps.test2.isOk).toBe(false); expect(menuItemProps.test2.enabled).toBe(false);
} }
{ {
const menuItemProps = utils.commandsToMenuItemProps({ const menuItemProps = utils.commandsToMenuItemProps(['test1', 'test2'], {
test1: 'ok', cond1: true,
test2: 'ok', cond2: true,
}, ['test1', 'test2']); });
expect(menuItemProps.test1.isOk).toBe(true); expect(menuItemProps.test1.enabled).toBe(true);
expect(menuItemProps.test2.isOk).toBe(true); expect(menuItemProps.test2.enabled).toBe(true);
} }
expect(utils.commandsToMenuItemProps({ expect(utils.commandsToMenuItemProps(['test1', 'test2'], { cond1: true, cond2: true }))
test1: 'ok', .toBe(utils.commandsToMenuItemProps(['test1', 'test2'], { cond1: true, cond2: true }));
test2: 'ok',
}, ['test1', 'test2'])).toBe(utils.commandsToMenuItemProps({
test1: 'ok',
test2: 'ok',
}, ['test1', 'test2']));
})); }));
it('should create stateful menu items', asyncTest(async () => { it('should create stateful menu items', asyncTest(async () => {
@ -259,20 +230,30 @@ describe('services_CommandService', function() {
let propValue = null; let propValue = null;
registerCommand(service, createCommand('test1', { registerCommand(service, createCommand('test1', {
mapStateToProps: (state:any) => { execute: (_context:any, greeting:string) => {
return { propValue = greeting;
isOk: state.test1 === 'ok',
};
},
execute: (props:any) => {
propValue = props.isOk;
}, },
})); }));
const menuItem = utils.commandToStatefulMenuItem('test1', { isOk: 'hello' }); const menuItem = utils.commandToStatefulMenuItem('test1', 'hello');
menuItem.click(); menuItem.click();
expect(propValue).toBe('hello'); expect(propValue).toBe('hello');
})); }));
it('should throw an error for invalid when clause keys in dev mode', asyncTest(async () => {
const service = newService();
registerCommand(service, createCommand('test1', {
execute: () => {},
enabledCondition: 'cond1 && cond2',
}));
await expectThrow(async () => service.isEnabled('test1', {}));
await expectThrow(async () => service.isEnabled('test1', { cond1: true }));
await expectNotThrow(async () => service.isEnabled('test1', { cond1: true, cond2: true }));
await expectNotThrow(async () => service.isEnabled('test1', { cond1: true, cond2: false }));
}));
}); });

View File

@ -45,7 +45,7 @@ joplin.plugins.register({
newLines.push(newCells.join(' | ')); newLines.push(newCells.join(' | '));
} }
await joplin.commands.execute('replaceSelection', { value: newLines.join('\n') }); await joplin.commands.execute('replaceSelection', newLines.join('\n'));
}, },
}); });

View File

@ -2420,6 +2420,11 @@
"path-exists": "^3.0.0" "path-exists": "^3.0.0"
} }
}, },
"lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
"integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE="
},
"lru-cache": { "lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2741,6 +2746,14 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true "dev": true
}, },
"node-emoji": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
"integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==",
"requires": {
"lodash.toarray": "^4.4.0"
}
},
"node-libs-browser": { "node-libs-browser": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@ -3429,11 +3442,6 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true "dev": true
}, },
"slug": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/slug/-/slug-3.3.4.tgz",
"integrity": "sha512-VpHbtRCEWmgaZsrZcTsVl/Dhw98lcrOYDO17DNmJCNpppI6s3qJvnNu2Q3D4L84/2bi6vkW40mjNQI9oGQsflg=="
},
"snapdragon": { "snapdragon": {
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -3981,6 +3989,11 @@
"imurmurhash": "^0.1.4" "imurmurhash": "^0.1.4"
} }
}, },
"unorm": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
"integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
},
"unset-value": { "unset-value": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz",
@ -4067,6 +4080,14 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true "dev": true
}, },
"uslug": {
"version": "git+https://github.com/laurent22/uslug.git#ba2834d79beb0435318709958b2f5e817d96674d",
"from": "git+https://github.com/laurent22/uslug.git#emoji-support",
"requires": {
"node-emoji": "^1.10.0",
"unorm": ">= 1.0.0"
}
},
"util": { "util": {
"version": "0.11.1", "version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",

View File

@ -20,6 +20,6 @@
"webpack-cli": "^3.3.11" "webpack-cli": "^3.3.11"
}, },
"dependencies": { "dependencies": {
"slug": "^3.3.4" "uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
} }
} }

View File

@ -49,9 +49,7 @@ joplin.plugins.register({
panels.onMessage(view, (message:any) => { panels.onMessage(view, (message:any) => {
if (message.name === 'scrollToHash') { if (message.name === 'scrollToHash') {
joplin.commands.execute('scrollToHash', { joplin.commands.execute('scrollToHash', message.hash)
hash: message.hash,
})
} }
}); });

View File

@ -155,7 +155,7 @@ export default class InteropServiceHelper {
if (Array.isArray(path)) path = path[0]; if (Array.isArray(path)) path = path[0];
CommandService.instance().execute('showModalMessage', { message: _('Exporting to "%s" as "%s" format. Please wait...', path, module.format) }); CommandService.instance().execute('showModalMessage', _('Exporting to "%s" as "%s" format. Please wait...', path, module.format));
const exportOptions:ExportOptions = {}; const exportOptions:ExportOptions = {};
exportOptions.path = path; exportOptions.path = path;

View File

@ -58,7 +58,7 @@ const commands = [
require('./gui/MainScreen/commands/showNoteProperties'), require('./gui/MainScreen/commands/showNoteProperties'),
require('./gui/MainScreen/commands/showShareNoteDialog'), require('./gui/MainScreen/commands/showShareNoteDialog'),
require('./gui/MainScreen/commands/toggleNoteList'), require('./gui/MainScreen/commands/toggleNoteList'),
require('./gui/MainScreen/commands/toggleSidebar'), require('./gui/MainScreen/commands/toggleSideBar'),
require('./gui/MainScreen/commands/toggleVisiblePanes'), require('./gui/MainScreen/commands/toggleVisiblePanes'),
require('./gui/MainScreen/commands/toggleEditors'), require('./gui/MainScreen/commands/toggleEditors'),
require('./gui/NoteEditor/commands/focusElementNoteBody'), require('./gui/NoteEditor/commands/focusElementNoteBody'),
@ -85,7 +85,7 @@ const globalCommands = [
const editorCommandDeclarations = require('./gui/NoteEditor/commands/editorCommandDeclarations').default; const editorCommandDeclarations = require('./gui/NoteEditor/commands/editorCommandDeclarations').default;
const pluginClasses = [ const pluginClasses = [
require('./plugins/GotoAnything.min'), require('./plugins/GotoAnything').default,
]; ];
interface AppStateRoute { interface AppStateRoute {
@ -513,7 +513,7 @@ class Application extends BaseApplication {
this.initRedux(); this.initRedux();
CommandService.instance().initialize(this.store()); CommandService.instance().initialize(this.store(), Setting.value('env') == 'dev');
for (const command of commands) { for (const command of commands) {
CommandService.instance().registerDeclaration(command.declaration); CommandService.instance().registerDeclaration(command.declaration);

View File

@ -6,7 +6,7 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async ({ target }:any) => { execute: async (_context:any, target:string) => {
if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody'); if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody');
if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList'); if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList');
if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar'); if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar');

View File

@ -1,13 +1,10 @@
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { stateUtils } from 'lib/reducer';
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const bridge = require('electron').remote.require('./bridge').default; const bridge = require('electron').remote.require('./bridge').default;
interface Props {
noteId: string
}
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'startExternalEditing', name: 'startExternalEditing',
label: () => _('Edit in external editor'), label: () => _('Edit in external editor'),
@ -16,21 +13,16 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (props:Props) => { execute: async (context:CommandContext, noteId:string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
try { try {
const note = await Note.load(props.noteId); const note = await Note.load(noteId);
ExternalEditWatcher.instance().openAndWatch(note); ExternalEditWatcher.instance().openAndWatch(note);
} catch (error) { } catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message)); bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
} }
}, },
isEnabled: (props:any) => { enabledCondition: 'oneNoteSelected',
return !!props.noteId;
},
mapStateToProps: (state:any) => {
return {
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
};
},
}; };
}; };

View File

@ -1,11 +1,8 @@
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { stateUtils } from 'lib/reducer';
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
interface Props {
noteId: string
}
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'stopExternalEditing', name: 'stopExternalEditing',
label: () => _('Stop external editing'), label: () => _('Stop external editing'),
@ -14,14 +11,10 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (props:Props) => { execute: async (context:CommandContext, noteId:string = null) => {
ExternalEditWatcher.instance().stopWatching(props.noteId); noteId = noteId || stateUtils.selectedNoteId(context.state);
}, ExternalEditWatcher.instance().stopWatching(noteId);
isEnabled: (props:any) => {
return !!props.noteId;
},
mapStateToProps: (state:any) => {
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
}, },
enabledCondition: 'oneNoteSelected',
}; };
}; };

View File

@ -1,12 +1,7 @@
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService'; import CommandService, { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { AppState } from '../app'; import { stateUtils } from 'lib/reducer';
import CommandService from 'lib/services/CommandService'; import { DesktopCommandContext } from '../services/commands/types';
interface Props {
noteId: string
noteIsBeingWatched: boolean
}
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'toggleExternalEditing', name: 'toggleExternalEditing',
@ -16,27 +11,21 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (props:Props) => { execute: async (context:DesktopCommandContext, noteId:string = null) => {
if (!props.noteId) return; noteId = noteId || stateUtils.selectedNoteId(context.state);
if (props.noteIsBeingWatched) { if (!noteId) return;
CommandService.instance().execute('stopExternalEditing', { noteId: props.noteId });
if (context.state.watchedNoteFiles.includes(noteId)) {
CommandService.instance().execute('stopExternalEditing', noteId);
} else { } else {
CommandService.instance().execute('startExternalEditing', { noteId: props.noteId }); CommandService.instance().execute('startExternalEditing', noteId);
} }
}, },
isEnabled: (props:Props) => { enabledCondition: 'oneNoteSelected',
return !!props.noteId; mapStateToTitle: (state:any) => {
}, const noteId = stateUtils.selectedNoteId(state);
mapStateToProps: (state:AppState):Props => { return state.watchedNoteFiles.includes(noteId) ? _('Stop') : '';
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
return {
noteId: noteId,
noteIsBeingWatched: noteId ? state.watchedNoteFiles.includes(noteId) : false,
};
},
title: (props:Props) => {
return props.noteIsBeingWatched ? _('Stop') : '';
}, },
}; };
}; };

View File

@ -26,6 +26,8 @@ const getLabel = (commandName: string) => {
return _('Hide Joplin'); return _('Hide Joplin');
case 'closeWindow': case 'closeWindow':
return _('Close Window'); return _('Close Window');
case 'commandPalette':
return _('Command palette');
case 'config': case 'config':
return shim.isMac() ? _('Preferences') : _('Options'); return shim.isMac() ? _('Preferences') : _('Options');
default: default:

View File

@ -61,7 +61,7 @@ const commands = [
require('./commands/showShareNoteDialog'), require('./commands/showShareNoteDialog'),
require('./commands/toggleEditors'), require('./commands/toggleEditors'),
require('./commands/toggleNoteList'), require('./commands/toggleNoteList'),
require('./commands/toggleSidebar'), require('./commands/toggleSideBar'),
require('./commands/toggleVisiblePanes'), require('./commands/toggleVisiblePanes'),
]; ];
@ -320,7 +320,7 @@ class MainScreenComponent extends React.Component<any, any> {
window.removeEventListener('resize', this.window_resize); window.removeEventListener('resize', this.window_resize);
} }
toggleSidebar() { toggleSideBar() {
this.props.dispatch({ this.props.dispatch({
type: 'SIDEBAR_VISIBILITY_TOGGLE', type: 'SIDEBAR_VISIBILITY_TOGGLE',
}); });

View File

@ -1,8 +1,8 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import eventManager from 'lib/eventManager'; import eventManager from 'lib/eventManager';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { stateUtils } from 'lib/reducer';
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
const BaseModel = require('lib/BaseModel');
const { time } = require('lib/time-utils'); const { time } = require('lib/time-utils');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -13,7 +13,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteId }:any) => { execute: async (context:CommandContext, noteId:string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
const note = await Note.load(noteId); const note = await Note.load(noteId);
const defaultDate = new Date(Date.now() + 2 * 3600 * 1000); const defaultDate = new Date(Date.now() + 2 * 3600 * 1000);
@ -51,25 +53,12 @@ export const runtime = (comp:any):CommandRuntime => {
}, },
}); });
}, },
title: (props:any):string => {
if (!props.noteId) return null;
if (!props.noteTodoDue) return null;
return time.formatMsToLocal(props.noteTodoDue);
},
isEnabled: (props:any):boolean => {
if (!props.noteId) return false;
return !!props.noteIsTodo && !props.noteTodoCompleted;
},
mapStateToProps: (state:any):any => {
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
const note = noteId ? BaseModel.byId(state.notes, noteId) : null;
return { enabledCondition: 'oneNoteSelected && noteIsTodo && !noteTodoCompleted',
noteId: note ? noteId : null,
noteIsTodo: note ? note.is_todo : false, mapStateToTitle: (state:any) => {
noteTodoCompleted: note ? note.todo_completed : false, const note = stateUtils.selectedNote(state);
noteTodoDue: note ? note.todo_due : null, return note && note.todo_due ? time.formatMsToLocal(note.todo_due) : null;
};
}, },
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import shim from 'lib/shim'; import shim from 'lib/shim';
import InteropServiceHelper from '../../../InteropServiceHelper'; import InteropServiceHelper from '../../../InteropServiceHelper';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
@ -12,8 +12,10 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteIds }:any) => { execute: async (context:CommandContext, noteIds:string[] = null) => {
try { try {
noteIds = noteIds || context.state.selectedNoteIds;
if (!noteIds.length) throw new Error('No notes selected for pdf export'); if (!noteIds.length) throw new Error('No notes selected for pdf export');
let path = null; let path = null;
@ -22,7 +24,6 @@ export const runtime = (comp:any):CommandRuntime => {
filters: [{ name: _('PDF File'), extensions: ['pdf'] }], filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
defaultPath: await InteropServiceHelper.defaultFilename(noteIds[0], 'pdf'), defaultPath: await InteropServiceHelper.defaultFilename(noteIds[0], 'pdf'),
}); });
} else { } else {
path = bridge().showOpenDialog({ path = bridge().showOpenDialog({
properties: ['openDirectory', 'createDirectory'], properties: ['openDirectory', 'createDirectory'],
@ -50,13 +51,7 @@ export const runtime = (comp:any):CommandRuntime => {
bridge().showErrorMessageBox(error.message); bridge().showErrorMessageBox(error.message);
} }
}, },
isEnabled: (props:any):boolean => {
return !!props.noteIds.length; enabledCondition: 'someNotesSelected',
},
mapStateToProps: (state:any):any => {
return {
noteIds: state.selectedNoteIds,
};
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'hideModalMessage', name: 'hideModalMessage',

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const Folder = require('lib/models/Folder'); const Folder = require('lib/models/Folder');
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
@ -10,7 +10,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteIds }:any) => { execute: async (context:CommandContext, noteIds:string[] = null) => {
noteIds = noteIds || context.state.selectedNoteIds;
const folders:any[] = await Folder.sortFolderTree(); const folders:any[] = await Folder.sortFolderTree();
const startFolders:any[] = []; const startFolders:any[] = [];
const maxDepth = 15; const maxDepth = 15;
@ -42,13 +44,6 @@ export const runtime = (comp:any):CommandRuntime => {
}, },
}); });
}, },
isEnabled: (props:any):boolean => { enabledCondition: 'someNotesSelected',
return !!props.noteIds.length;
},
mapStateToProps: (state:any):any => {
return {
noteIds: state.selectedNoteIds,
};
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandContext, CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const Folder = require('lib/models/Folder'); const Folder = require('lib/models/Folder');
const bridge = require('electron').remote.require('./bridge').default; const bridge = require('electron').remote.require('./bridge').default;
@ -11,7 +11,7 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ parentId }:any) => { execute: async (_context:CommandContext, parentId:string = null) => {
comp.setState({ comp.setState({
promptOptions: { promptOptions: {
label: _('Notebook title:'), label: _('Notebook title:'),
@ -39,8 +39,5 @@ export const runtime = (comp:any):CommandRuntime => {
}, },
}); });
}, },
title: () => {
return _('New notebook');
},
}; };
}; };

View File

@ -1,8 +1,7 @@
import { utils, CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { utils, CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const Setting = require('lib/models/Setting').default; const Setting = require('lib/models/Setting').default;
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
const Folder = require('lib/models/Folder');
const TemplateUtils = require('lib/TemplateUtils'); const TemplateUtils = require('lib/TemplateUtils');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -13,7 +12,7 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async ({ template, isTodo }:any) => { execute: async (_context:CommandContext, template:string = null, isTodo:boolean = false) => {
const folderId = Setting.value('activeFolderId'); const folderId = Setting.value('activeFolderId');
if (!folderId) return; if (!folderId) return;
@ -34,12 +33,6 @@ export const runtime = ():CommandRuntime => {
id: newNote.id, id: newNote.id,
}); });
}, },
isEnabled: () => { enabledCondition: 'oneFolderSelected && !inConflictFolder',
const { folders, selectedFolderId } = utils.store.getState();
return !!folders.length && selectedFolderId !== Folder.conflictFolderId();
},
title: () => {
return _('New note');
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import CommandService, { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -9,14 +9,9 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async ({ template }:any) => { execute: async (_context:CommandContext, template:string = null) => {
return CommandService.instance().execute('newNote', { template: template, isTodo: true }); return CommandService.instance().execute('newNote', template, true);
},
isEnabled: () => {
return CommandService.instance().isEnabled('newNote', {});
},
title: () => {
return _('New to-do');
}, },
enabledCondition: 'oneFolderSelected && !inConflictFolder',
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const bridge = require('electron').remote.require('./bridge').default; const bridge = require('electron').remote.require('./bridge').default;
@ -10,8 +10,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteIds }:any) => { execute: async (context:CommandContext, noteIds:string[] = null) => {
// TODO: test noteIds = noteIds || context.state.selectedNoteIds;
try { try {
if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.')); if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.'));
await comp.printTo_('printer', { noteId: noteIds[0] }); await comp.printTo_('printer', { noteId: noteIds[0] });
@ -19,13 +20,6 @@ export const runtime = (comp:any):CommandRuntime => {
bridge().showErrorMessageBox(error.message); bridge().showErrorMessageBox(error.message);
} }
}, },
isEnabled: (props:any):boolean => { enabledCondition: 'someNotesSelected',
return !!props.noteIds.length;
},
mapStateToProps: (state:any):any => {
return {
noteIds: state.selectedNoteIds,
};
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const Folder = require('lib/models/Folder'); const Folder = require('lib/models/Folder');
const bridge = require('electron').remote.require('./bridge').default; const bridge = require('electron').remote.require('./bridge').default;
@ -10,7 +10,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ folderId }:any) => { execute: async (context:CommandContext, folderId:string = null) => {
folderId = folderId || context.state.selectedFolderId;
const folder = await Folder.load(folderId); const folder = await Folder.load(folderId);
if (folder) { if (folder) {

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const Tag = require('lib/models/Tag'); const Tag = require('lib/models/Tag');
const bridge = require('electron').remote.require('./bridge').default; const bridge = require('electron').remote.require('./bridge').default;
@ -10,7 +10,10 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ tagId }:any) => { execute: async (context:CommandContext, tagId:string = null) => {
tagId = tagId || context.state.selectedTagId;
if (!tagId) return;
const tag = await Tag.load(tagId); const tag = await Tag.load(tagId);
if (tag) { if (tag) {
comp.setState({ comp.setState({

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
const BaseModel = require('lib/BaseModel'); const BaseModel = require('lib/BaseModel');
const uuid = require('lib/uuid').default; const uuid = require('lib/uuid').default;
@ -9,7 +9,7 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ query }:any) => { execute: async (_context:CommandContext, query:string) => {
if (!comp.searchId_) comp.searchId_ = uuid.create(); if (!comp.searchId_) comp.searchId_ = uuid.create();
comp.props.dispatch({ comp.props.dispatch({

View File

@ -1,4 +1,4 @@
import CommandService, { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const TemplateUtils = require('lib/TemplateUtils'); const TemplateUtils = require('lib/TemplateUtils');
@ -8,7 +8,7 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteType }:any) => { execute: async (_context:CommandContext, noteType:string) => {
comp.setState({ comp.setState({
promptOptions: { promptOptions: {
label: _('Template file:'), label: _('Template file:'),
@ -18,9 +18,9 @@ export const runtime = (comp:any):CommandRuntime => {
onClose: async (answer:any) => { onClose: async (answer:any) => {
if (answer) { if (answer) {
if (noteType === 'note' || noteType === 'todo') { if (noteType === 'note' || noteType === 'todo') {
CommandService.instance().execute('newNote', { template: answer.value, isTodo: noteType === 'todo' }); CommandService.instance().execute('newNote', answer.value, noteType === 'todo');
} else { } else {
CommandService.instance().execute('insertText', { value: TemplateUtils.render(answer.value) }); CommandService.instance().execute('insertText', TemplateUtils.render(answer.value));
} }
} }

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const Tag = require('lib/models/Tag'); const Tag = require('lib/models/Tag');
@ -10,7 +10,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteIds }:any) => { execute: async (context:CommandContext, noteIds:string[] = null) => {
noteIds = noteIds || context.state.selectedNoteIds;
const tags = await Tag.commonTagsByNoteIds(noteIds); const tags = await Tag.commonTagsByNoteIds(noteIds);
const startTags = tags const startTags = tags
.map((a:any) => { .map((a:any) => {
@ -64,11 +66,6 @@ export const runtime = (comp:any):CommandRuntime => {
}, },
}); });
}, },
isEnabled: (props:any) => { enabledCondition: 'someNotesSelected',
return !!props.noteIds.length;
},
mapStateToProps: (state:any) => {
return { noteIds: state.selectedNoteIds };
},
}; };
}; };

View File

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandDeclaration, CommandRuntime, CommandContext } from 'lib/services/CommandService';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'showModalMessage', name: 'showModalMessage',
@ -7,7 +7,7 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ message }:any) => { execute: async (_context:CommandContext, message:string) => {
comp.setState({ comp.setState({
modalLayer: { modalLayer: {
visible: true, visible: true,

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { stateUtils } from 'lib/reducer';
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -9,7 +10,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteId }:any) => { execute: async (context:CommandContext, noteId:string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
const note = await Note.load(noteId); const note = await Note.load(noteId);
if (note) { if (note) {
comp.setState({ comp.setState({
@ -21,11 +24,7 @@ export const runtime = (comp:any):CommandRuntime => {
}); });
} }
}, },
isEnabled: (props:any) => {
return !!props.noteId; enabledCondition: 'oneNoteSelected',
},
mapStateToProps: (state:any) => {
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
},
}; };
}; };

View File

@ -1,5 +1,6 @@
import CommandService, { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { stateUtils } from 'lib/reducer';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'showNoteProperties', name: 'showNoteProperties',
@ -9,7 +10,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteId }:any) => { execute: async (context:CommandContext, noteId:string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
comp.setState({ comp.setState({
notePropertiesDialogOptions: { notePropertiesDialogOptions: {
noteId: noteId, noteId: noteId,
@ -20,11 +23,6 @@ export const runtime = (comp:any):CommandRuntime => {
}, },
}); });
}, },
isEnabled: (props:any) => { enabledCondition: 'oneNoteSelected',
return !!props.noteId;
},
mapStateToProps: (state:any) => {
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -8,7 +8,9 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteIds }:any) => { execute: async (context:CommandContext, noteIds:string[] = null) => {
noteIds = noteIds || context.state.selectedNoteIds;
comp.setState({ comp.setState({
shareNoteDialogOptions: { shareNoteDialogOptions: {
noteIds: noteIds, noteIds: noteIds,

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandDeclaration, CommandRuntime, CommandContext } from 'lib/services/CommandService';
import Setting from 'lib/models/Setting'; import Setting from 'lib/models/Setting';
import { stateUtils } from 'lib/reducer'; import { stateUtils } from 'lib/reducer';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
@ -11,22 +11,14 @@ export const declaration:CommandDeclaration = {
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (props:any) => { execute: async (context:CommandContext) => {
// A bit of a hack, but for now don't allow changing code view // A bit of a hack, but for now don't allow changing code view
// while a note is being saved as it will cause a problem with // while a note is being saved as it will cause a problem with
// TinyMCE because it won't have time to send its content before // TinyMCE because it won't have time to send its content before
// being switch to Ace Editor. // being switch to Ace Editor.
if (props.hasNotesBeingSaved) return; if (stateUtils.hasNotesBeingSaved(context.state)) return;
Setting.toggle('editor.codeView'); Setting.toggle('editor.codeView');
}, },
isEnabled: (props:any):boolean => { enabledCondition: '!notesAreBeingSaved && oneNoteSelected',
return !props.hasNotesBeingSaved && props.hasOneSelectedNote;
},
mapStateToProps: (state:any):any => {
return {
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
hasOneSelectedNote: state.selectedNoteIds.length === 1,
};
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {

View File

@ -1,8 +1,8 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'toggleSidebar', name: 'toggleSideBar',
label: () => _('Toggle sidebar'), label: () => _('Toggle sidebar'),
iconName: 'fas fa-bars', iconName: 'fas fa-bars',
}; };

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -14,14 +14,7 @@ export const runtime = (comp:any):CommandRuntime => {
type: 'NOTE_VISIBLE_PANES_TOGGLE', type: 'NOTE_VISIBLE_PANES_TOGGLE',
}); });
}, },
isEnabled: (props:any):boolean => {
return props.settingEditorCodeView && props.selectedNoteIds.length === 1; enabledCondition: 'markdownEditorVisible && oneNoteSelected',
},
mapStateToProps: (state:any):any => {
return {
selectedNoteIds: state.selectedNoteIds,
settingEditorCodeView: state.settings['editor.codeView'],
};
},
}; };
}; };

View File

@ -13,6 +13,7 @@ import { Module } from 'lib/services/interop/types';
import InteropServiceHelper from '../InteropServiceHelper'; import InteropServiceHelper from '../InteropServiceHelper';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { MenuItem, MenuItemLocation } from 'lib/services/plugins/api/types'; import { MenuItem, MenuItemLocation } from 'lib/services/plugins/api/types';
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
@ -108,7 +109,7 @@ const commandNames:string[] = [
'attachFile', 'attachFile',
'focusSearch', 'focusSearch',
'showLocalSearch', 'showLocalSearch',
'toggleSidebar', 'toggleSideBar',
'toggleNoteList', 'toggleNoteList',
'toggleVisiblePanes', 'toggleVisiblePanes',
'toggleExternalEditing', 'toggleExternalEditing',
@ -157,7 +158,7 @@ function useMenu(props:Props) {
if (Array.isArray(path)) path = path[0]; if (Array.isArray(path)) path = path[0];
CommandService.instance().execute('showModalMessage', { message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format) }); CommandService.instance().execute('showModalMessage', _('Importing from "%s" as "%s" format. Please wait...', path, module.format));
const importOptions = { const importOptions = {
path, path,
@ -299,12 +300,12 @@ function useMenu(props:Props) {
templateItems.push({ templateItems.push({
label: _('Create note from template'), label: _('Create note from template'),
click: () => { click: () => {
CommandService.instance().execute('selectTemplate', { noteType: 'note' }); CommandService.instance().execute('selectTemplate', 'note');
}, },
}, { }, {
label: _('Create to-do from template'), label: _('Create to-do from template'),
click: () => { click: () => {
CommandService.instance().execute('selectTemplate', { noteType: 'todo' }); CommandService.instance().execute('selectTemplate', 'todo');
}, },
}, { }, {
label: _('Insert template'), label: _('Insert template'),
@ -532,7 +533,7 @@ function useMenu(props:Props) {
view: { view: {
label: _('&View'), label: _('&View'),
submenu: [ submenu: [
menuItemDic.toggleSidebar, menuItemDic.toggleSideBar,
menuItemDic.toggleNoteList, menuItemDic.toggleNoteList,
menuItemDic.toggleVisiblePanes, menuItemDic.toggleVisiblePanes,
{ {
@ -550,7 +551,6 @@ function useMenu(props:Props) {
id: 'showNoteCounts', id: 'showNoteCounts',
label: Setting.settingMetadata('showNoteCounts').label(), label: Setting.settingMetadata('showNoteCounts').label(),
type: 'checkbox', type: 'checkbox',
// checked: Setting.value('showNoteCounts'),
click: () => { click: () => {
Setting.setValue('showNoteCounts', !Setting.value('showNoteCounts')); Setting.setValue('showNoteCounts', !Setting.value('showNoteCounts'));
}, },
@ -558,7 +558,6 @@ function useMenu(props:Props) {
id: 'uncompletedTodosOnTop', id: 'uncompletedTodosOnTop',
label: Setting.settingMetadata('uncompletedTodosOnTop').label(), label: Setting.settingMetadata('uncompletedTodosOnTop').label(),
type: 'checkbox', type: 'checkbox',
// checked: Setting.value('uncompletedTodosOnTop'),
click: () => { click: () => {
Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop')); Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
}, },
@ -566,7 +565,6 @@ function useMenu(props:Props) {
id: 'showCompletedTodos', id: 'showCompletedTodos',
label: Setting.settingMetadata('showCompletedTodos').label(), label: Setting.settingMetadata('showCompletedTodos').label(),
type: 'checkbox', type: 'checkbox',
// checked: Setting.value('showCompletedTodos'),
click: () => { click: () => {
Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos')); Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
}, },
@ -786,9 +784,13 @@ function useMenu(props:Props) {
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime]); }, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime]);
useEffect(() => { useEffect(() => {
const whenClauseContext = CommandService.instance().currentWhenClauseContext();
for (const commandName in props.menuItemProps) { for (const commandName in props.menuItemProps) {
if (!props.menuItemProps[commandName]) continue; const p = props.menuItemProps[commandName];
menuItemSetEnabled(commandName, CommandService.instance().isEnabled(commandName, props.menuItemProps[commandName])); if (!p) continue;
const enabled = 'enabled' in p ? p.enabled : CommandService.instance().isEnabled(commandName, whenClauseContext);
menuItemSetEnabled(commandName, enabled);
} }
const layoutButtonSequenceOptions = Setting.enumOptions('layoutButtonSequence'); const layoutButtonSequenceOptions = Setting.enumOptions('layoutButtonSequence');
@ -858,8 +860,10 @@ function MenuBar(props:Props):JSX.Element {
} }
const mapStateToProps = (state:AppState) => { const mapStateToProps = (state:AppState) => {
const whenClauseContext = stateToWhenClauseContext(state);
return { return {
menuItemProps: menuUtils.commandsToMenuItemProps(state, commandNames.concat(pluginCommandNames(state.pluginService.plugins))), menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(pluginCommandNames(state.pluginService.plugins)), whenClauseContext),
routeName: state.route.routeName, routeName: state.route.routeName,
selectedFolderId: state.selectedFolderId, selectedFolderId: state.selectedFolderId,
layoutButtonSequence: state.settings.layoutButtonSequence, layoutButtonSequence: state.settings.layoutButtonSequence,

View File

@ -46,8 +46,6 @@ function formatReadTime(readTimeMinutes: number) {
} }
export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) { export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) {
console.info('MMMMMMMMMMMM', props.markupLanguage);
const theme = themeStyle(props.themeId); const theme = themeStyle(props.themeId);
const tableBodyComps: JSX.Element[] = []; const tableBodyComps: JSX.Element[] = [];
// For the source Markdown // For the source Markdown

View File

@ -5,6 +5,7 @@ import { utils as pluginUtils } from 'lib/services/plugins/reducer';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AppState } from '../../../../app'; import { AppState } from '../../../../app';
import ToolbarButtonUtils, { ToolbarButtonInfo } from 'lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils, { ToolbarButtonInfo } from 'lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
const { buildStyle } = require('lib/theme'); const { buildStyle } = require('lib/theme');
interface ToolbarProps { interface ToolbarProps {
@ -31,6 +32,8 @@ function Toolbar(props:ToolbarProps) {
} }
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
const whenClauseContext = stateToWhenClauseContext(state);
const commandNames = [ const commandNames = [
'historyBackward', 'historyBackward',
'historyForward', 'historyForward',
@ -53,7 +56,7 @@ const mapStateToProps = (state: AppState) => {
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'editorToolbar')); ].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'editorToolbar'));
return { return {
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(state, commandNames), toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(commandNames, whenClauseContext),
}; };
}; };

View File

@ -25,6 +25,7 @@ import eventManager from 'lib/eventManager';
import { AppState } from '../../app'; import { AppState } from '../../app';
import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const { substrWithEllipsis } = require('lib/string-utils'); const { substrWithEllipsis } = require('lib/string-utils');
@ -247,9 +248,9 @@ function NoteEditor(props: NoteEditorProps) {
event.preventDefault(); event.preventDefault();
if (event.shiftKey) { if (event.shiftKey) {
CommandService.instance().execute('focusElement', { target: 'noteList' }); CommandService.instance().execute('focusElement', 'noteList');
} else { } else {
CommandService.instance().execute('focusElement', { target: 'noteBody' }); CommandService.instance().execute('focusElement', 'noteBody');
} }
} }
}, [props.dispatch]); }, [props.dispatch]);
@ -364,7 +365,7 @@ function NoteEditor(props: NoteEditorProps) {
function renderTagBar() { function renderTagBar() {
const theme = themeStyle(props.themeId); const theme = themeStyle(props.themeId);
const noteIds = [formNote.id]; const noteIds = [formNote.id];
const instructions = <span onClick={() => { CommandService.instance().execute('setTags', { noteIds }); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>Click to add tags...</span>; const instructions = <span onClick={() => { CommandService.instance().execute('setTags', noteIds); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>Click to add tags...</span>;
const tagList = props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null; const tagList = props.selectedNoteTags.length ? <TagList items={props.selectedNoteTags} /> : null;
return ( return (
@ -565,6 +566,7 @@ export {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null; const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
const whenClauseContext = stateToWhenClauseContext(state);
return { return {
noteId: noteId, noteId: noteId,
@ -587,15 +589,15 @@ const mapStateToProps = (state: AppState) => {
watchedResources: state.watchedResources, watchedResources: state.watchedResources,
highlightedWords: state.highlightedWords, highlightedWords: state.highlightedWords,
plugins: state.pluginService.plugins, plugins: state.pluginService.plugins,
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(state, [ toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
'historyBackward', 'historyBackward',
'historyForward', 'historyForward',
'toggleEditors', 'toggleEditors',
'toggleExternalEditing', 'toggleExternalEditing',
]), ], whenClauseContext),
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons(state, [ setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([
'setTags', 'setTags',
])[0], ], whenClauseContext)[0],
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -12,13 +12,6 @@ export const runtime = (comp:any):CommandRuntime => {
execute: async () => { execute: async () => {
comp.editorRef.current.execCommand({ name: 'focus' }); comp.editorRef.current.execCommand({ name: 'focus' });
}, },
isEnabled: (props:any):boolean => { enabledCondition: 'oneNoteSelected',
return props.hasOneNoteSelected;
},
mapStateToProps: (state:any):any => {
return {
hasOneNoteSelected: state.selectedNoteIds.length === 1,
};
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -13,14 +13,6 @@ export const runtime = (comp:any):CommandRuntime => {
if (!comp.titleInputRef.current) return; if (!comp.titleInputRef.current) return;
comp.titleInputRef.current.focus(); comp.titleInputRef.current.focus();
}, },
isEnabled: (props:any):boolean => { enabledCondition: 'oneNoteSelected',
return props.hasOneNoteSelected;
},
mapStateToProps: (state:any):any => {
return {
hasOneNoteSelected: state.selectedNoteIds.length === 1,
};
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
@ -16,11 +16,6 @@ export const runtime = (comp:any):CommandRuntime => {
if (comp.noteSearchBarRef.current) comp.noteSearchBarRef.current.wrappedInstance.focus(); if (comp.noteSearchBarRef.current) comp.noteSearchBarRef.current.wrappedInstance.focus();
} }
}, },
isEnabled: (props:any) => { enabledCondition: 'oneNoteSelected',
return !!props.noteId;
},
mapStateToProps: (state:any) => {
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'showRevisions', name: 'showRevisions',

View File

@ -1,11 +1,9 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FormNote, ScrollOptionTypes } from './types'; import { FormNote, ScrollOptionTypes } from './types';
import editorCommandDeclarations from '../commands/editorCommandDeclarations'; import editorCommandDeclarations from '../commands/editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService'; import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from 'lib/services/CommandService';
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const BaseModel = require('lib/BaseModel');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { MarkupToHtml } = require('lib/joplin-renderer');
const commandsWithDependencies = [ const commandsWithDependencies = [
require('../commands/showLocalSearch'), require('../commands/showLocalSearch'),
@ -25,49 +23,30 @@ interface HookDependencies {
function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):CommandRuntime { function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):CommandRuntime {
return { return {
execute: async (props:any) => { execute: async (_context:CommandContext, ...args:any[]) => {
// console.info('Running editor command:', declaration.name, props);
if (!editorRef.current.execCommand) { if (!editorRef.current.execCommand) {
reg.logger().warn('Received command, but editor cannot execute commands', declaration.name); reg.logger().warn('Received command, but editor cannot execute commands', declaration.name);
return;
}
if (declaration.name === 'insertDateTime') {
return editorRef.current.execCommand({
name: 'insertText',
value: time.formatMsToLocal(new Date().getTime()),
});
} else if (declaration.name === 'scrollToHash') {
return editorRef.current.scrollTo({
type: ScrollOptionTypes.Hash,
value: args[0],
});
} else { } else {
if (declaration.name === 'insertDateTime') { return editorRef.current.execCommand({
return editorRef.current.execCommand({ name: declaration.name,
name: 'insertText', value: args[0],
value: time.formatMsToLocal(new Date().getTime()), });
});
} else if (declaration.name === 'scrollToHash') {
return editorRef.current.scrollTo({
type: ScrollOptionTypes.Hash,
value: props.hash,
});
} else {
return editorRef.current.execCommand({
name: declaration.name,
value: props.value,
});
}
} }
}, },
isEnabled: (props:any) => { enabledCondition: '!modalDialogVisible && markdownEditorPaneVisible && oneNoteSelected && noteIsMarkdown',
if (props.isDialogVisible) return false;
if (props.markdownEditorViewerOnly) return false;
if (!props.hasSelectedNote) return false;
return props.isMarkdownNote;
},
mapStateToProps: (state:any) => {
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
const note = noteId ? BaseModel.byId(state.notes, noteId) : null;
const isMarkdownNote = note ? note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN : false;
return {
// True when the Markdown editor is active, and only the viewer pane is visible
// In this case, all editor-related shortcuts are disabled.
markdownEditorViewerOnly: state.settings['editor.codeView'] && state.noteVisiblePanes.length === 1 && state.noteVisiblePanes[0] === 'viewer',
hasSelectedNote: !!note,
isDialogVisible: !!Object.keys(state.visibleDialogs).length,
isMarkdownNote: isMarkdownNote,
};
},
}; };
} }

View File

@ -24,7 +24,7 @@ const StyledRoot = styled.div`
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: ${(props:any) => props.theme.backgroundColor3}; background-color: ${(props:any) => props.theme.backgroundColor3};
border-right: 1px solid ${(props:any) => props.theme.dividerColor}, border-right: 1px solid ${(props:any) => props.theme.dividerColor};
`; `;
class NoteListComponent extends React.Component { class NoteListComponent extends React.Component {
@ -386,9 +386,9 @@ class NoteListComponent extends React.Component {
event.preventDefault(); event.preventDefault();
if (event.shiftKey) { if (event.shiftKey) {
CommandService.instance().execute('focusElement', { target: 'sideBar' }); CommandService.instance().execute('focusElement', 'sideBar');
} else { } else {
CommandService.instance().execute('focusElement', { target: 'noteTitle' }); CommandService.instance().execute('focusElement', 'noteTitle');
} }
} }

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService'; import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { stateUtils } from 'lib/reducer';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'focusElementNoteList', name: 'focusElementNoteList',
@ -9,19 +10,14 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ noteId }:any) => { execute: async (context:CommandContext, noteId:string = null) => {
noteId = noteId || stateUtils.selectedNoteId(context.state);
if (noteId) { if (noteId) {
const ref = comp.itemAnchorRef(noteId); const ref = comp.itemAnchorRef(noteId);
if (ref) ref.focus(); if (ref) ref.focus();
} }
}, },
isEnabled: (props:any):boolean => { enabledCondition: 'noteListHasNotes',
return !!props.noteId;
},
mapStateToProps: (state:any):any => {
return {
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
};
},
}; };
}; };

View File

@ -52,13 +52,13 @@ export default function NoteListControls(props:Props) {
return ( return (
<ButtonContainer> <ButtonContainer>
<StyledButton <StyledButton
tooltip={CommandService.instance().title('newTodo', {})} tooltip={CommandService.instance().label('newTodo')}
iconName="far fa-check-square" iconName="far fa-check-square"
level={ButtonLevel.Primary} level={ButtonLevel.Primary}
onClick={onNewTodoButtonClick} onClick={onNewTodoButtonClick}
/> />
<StyledButton <StyledButton
tooltip={CommandService.instance().title('newNote', {})} tooltip={CommandService.instance().label('newNote')}
iconName="icon-note" iconName="icon-note"
level={ButtonLevel.Primary} level={ButtonLevel.Primary}
onClick={onNewNoteButtonClick} onClick={onNewNoteButtonClick}

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {

View File

@ -3,6 +3,7 @@ import CommandService from 'lib/services/CommandService';
import ToolbarBase from '../ToolbarBase'; import ToolbarBase from '../ToolbarBase';
import { utils as pluginUtils } from 'lib/services/plugins/reducer'; import { utils as pluginUtils } from 'lib/services/plugins/reducer';
import ToolbarButtonUtils, { ToolbarButtonInfo } from 'lib/services/commands/ToolbarButtonUtils'; import ToolbarButtonUtils, { ToolbarButtonInfo } from 'lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { buildStyle } = require('lib/theme'); const { buildStyle } = require('lib/theme');
@ -32,12 +33,14 @@ function NoteToolbar(props:NoteToolbarProps) {
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance()); const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
const mapStateToProps = (state:any) => { const mapStateToProps = (state:any) => {
const whenClauseContext = stateToWhenClauseContext(state);
return { return {
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(state, [ toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
'editAlarm', 'editAlarm',
'toggleVisiblePanes', 'toggleVisiblePanes',
'showNoteProperties', 'showNoteProperties',
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar'))), ].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar')), whenClauseContext),
}; };
}; };

View File

@ -5,7 +5,7 @@ const debounce = require('debounce');
export default function useSearch(query:string) { export default function useSearch(query:string) {
useEffect(() => { useEffect(() => {
const search = debounce((query:string) => { const search = debounce((query:string) => {
CommandService.instance().execute('search', { query }); CommandService.instance().execute('search', query);
}, 500); }, 500);
search(query); search(query);

View File

@ -230,7 +230,7 @@ class SideBarComponent extends React.Component<Props, State> {
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append( menu.append(
new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder', { parentId: itemId })) new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder', itemId))
); );
} }
@ -259,7 +259,7 @@ class SideBarComponent extends React.Component<Props, State> {
); );
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('renameFolder', { folderId: itemId }))); menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('renameFolder', itemId)));
menu.append(new MenuItem({ type: 'separator' })); menu.append(new MenuItem({ type: 'separator' }));
@ -290,7 +290,7 @@ class SideBarComponent extends React.Component<Props, State> {
if (itemType === BaseModel.TYPE_TAG) { if (itemType === BaseModel.TYPE_TAG) {
menu.append(new MenuItem( menu.append(new MenuItem(
menuUtils.commandToStatefulMenuItem('renameTag', { tagId: itemId }) menuUtils.commandToStatefulMenuItem('renameTag', itemId)
)); ));
} }
@ -510,9 +510,9 @@ class SideBarComponent extends React.Component<Props, State> {
event.preventDefault(); event.preventDefault();
if (event.shiftKey) { if (event.shiftKey) {
CommandService.instance().execute('focusElement', { target: 'noteBody' }); CommandService.instance().execute('focusElement', 'noteBody');
} else { } else {
CommandService.instance().execute('focusElement', { target: 'noteList' }); CommandService.instance().execute('focusElement', 'noteList');
} }
} }
@ -559,7 +559,7 @@ class SideBarComponent extends React.Component<Props, State> {
iconAnimation={iconAnimation} iconAnimation={iconAnimation}
title={label} title={label}
onClick={() => { onClick={() => {
CommandService.instance().execute('synchronize', { syncStarted: type !== 'sync' }); CommandService.instance().execute('synchronize', type !== 'sync');
}} }}
/> />
); );

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService'; import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
import { DesktopCommandContext } from 'ElectronClient/services/commands/types';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'focusElementSideBar', name: 'focusElementSideBar',
@ -9,8 +10,10 @@ export const declaration:CommandDeclaration = {
export const runtime = (comp:any):CommandRuntime => { export const runtime = (comp:any):CommandRuntime => {
return { return {
execute: async ({ sidebarVisibility }:any) => { execute: async (context:DesktopCommandContext) => {
if (sidebarVisibility) { const sideBarVisible = !!context.state.sidebarVisibility;
if (sideBarVisible) {
const item = comp.selectedItem(); const item = comp.selectedItem();
if (item) { if (item) {
const anchorRef = comp.anchorItemRefs[item.type][item.id]; const anchorRef = comp.anchorItemRefs[item.type][item.id];
@ -21,13 +24,7 @@ export const runtime = (comp:any):CommandRuntime => {
} }
} }
}, },
isEnabled: (props:any):boolean => {
return props.sidebarVisibility; enabledCondition: 'sideBarVisible',
},
mapStateToProps: (state:any):any => {
return {
sidebarVisibility: state.sidebarVisibility,
};
},
}; };
}; };

View File

@ -40,11 +40,11 @@ export default class NoteListUtils {
if (!hasEncrypted) { if (!hasEncrypted) {
menu.append( menu.append(
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', { noteIds })) new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', noteIds))
); );
menu.append( menu.append(
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', { noteIds })) new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', noteIds))
); );
menu.append( menu.append(
@ -63,7 +63,7 @@ export default class NoteListUtils {
if (singleNoteId) { if (singleNoteId) {
const cmd = props.watchedNoteFiles.includes(singleNoteId) ? 'stopExternalEditing' : 'startExternalEditing'; const cmd = props.watchedNoteFiles.includes(singleNoteId) ? 'stopExternalEditing' : 'startExternalEditing';
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem(cmd, { noteId: singleNoteId }))); menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem(cmd, singleNoteId)));
} }
if (noteIds.length <= 1) { if (noteIds.length <= 1) {
@ -132,7 +132,7 @@ export default class NoteListUtils {
menu.append( menu.append(
new MenuItem( new MenuItem(
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', { noteIds: noteIds.slice() }) menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())
) )
); );
@ -157,7 +157,7 @@ export default class NoteListUtils {
exportMenu.append( exportMenu.append(
new MenuItem( new MenuItem(
menuUtils.commandToStatefulMenuItem('exportPdf', { noteIds: noteIds }) menuUtils.commandToStatefulMenuItem('exportPdf', noteIds)
) )
); );

View File

@ -1,8 +1,12 @@
const React = require('react'); import * as React from 'react';
import { AppState } from '../app';
import CommandService, { SearchResult as CommandSearchResult } from 'lib/services/CommandService';
import KeymapService from 'lib/services/KeymapService';
import shim from 'lib/shim';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { _ } = require('lib/locale'); const { _ } = require('lib/locale');
const { themeStyle } = require('lib/theme'); const { themeStyle } = require('lib/theme');
const CommandService = require('lib/services/CommandService').default;
const SearchEngine = require('lib/services/searchengine/SearchEngine'); const SearchEngine = require('lib/services/searchengine/SearchEngine');
const BaseModel = require('lib/BaseModel'); const BaseModel = require('lib/BaseModel');
const Tag = require('lib/models/Tag'); const Tag = require('lib/models/Tag');
@ -12,32 +16,73 @@ const { ItemList } = require('../gui/ItemList.min');
const HelpButton = require('../gui/HelpButton.min'); const HelpButton = require('../gui/HelpButton.min');
const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('lib/string-utils.js'); const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('lib/string-utils.js');
const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js'); const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js');
const PLUGIN_NAME = 'gotoAnything';
const markupLanguageUtils = require('lib/markupLanguageUtils'); const markupLanguageUtils = require('lib/markupLanguageUtils');
const KeymapService = require('lib/services/KeymapService.js').default;
const shim = require('lib/shim').default; const PLUGIN_NAME = 'gotoAnything';
interface SearchResult {
id: string,
title: string,
parent_id: string,
fields: string[],
fragments?: string,
path?: string,
type?: number,
}
interface Props {
themeId: number,
dispatch: Function,
folders: any[],
showCompletedTodos: boolean,
userData: any,
}
interface State {
query: string,
results: SearchResult[],
selectedItemId: string,
keywords: string[],
listType: number,
showHelp: boolean,
resultsInBody: boolean,
}
class GotoAnything { class GotoAnything {
onTrigger() { public dispatch:Function;
public static Dialog:any;
public static manifest:any;
onTrigger(event:any) {
this.dispatch({ this.dispatch({
type: 'PLUGINLEGACY_DIALOG_SET', type: 'PLUGINLEGACY_DIALOG_SET',
open: true, open: true,
pluginName: PLUGIN_NAME, pluginName: PLUGIN_NAME,
userData: event.userData,
}); });
} }
} }
class Dialog extends React.PureComponent { class Dialog extends React.PureComponent<Props, State> {
constructor() { private fuzzy_:boolean;
super(); private styles_:any;
private inputRef:any;
private itemListRef:any;
private listUpdateIID_:any;
private markupToHtml_:any;
constructor(props:Props) {
super(props);
this.fuzzy_ = false; this.fuzzy_ = false;
const startString = props?.userData?.startString ? props?.userData?.startString : '';
this.state = { this.state = {
query: '', query: startString,
results: [], results: [],
selectedItemId: null, selectedItemId: null,
keywords: [], keywords: [],
@ -55,19 +100,25 @@ class Dialog extends React.PureComponent {
this.input_onChange = this.input_onChange.bind(this); this.input_onChange = this.input_onChange.bind(this);
this.input_onKeyDown = this.input_onKeyDown.bind(this); this.input_onKeyDown = this.input_onKeyDown.bind(this);
this.modalLayer_onClick = this.modalLayer_onClick.bind(this); this.modalLayer_onClick = this.modalLayer_onClick.bind(this);
this.listItemRenderer = this.listItemRenderer.bind(this); this.renderItem = this.renderItem.bind(this);
this.listItem_onClick = this.listItem_onClick.bind(this); this.listItem_onClick = this.listItem_onClick.bind(this);
this.helpButton_onClick = this.helpButton_onClick.bind(this); this.helpButton_onClick = this.helpButton_onClick.bind(this);
if (startString) this.scheduleListUpdate();
} }
style() { style() {
const styleKey = [this.props.themeId, this.state.resultsInBody ? '1' : '0'].join('-'); const styleKey = [this.props.themeId, this.state.listType, this.state.resultsInBody ? '1' : '0'].join('-');
if (this.styles_[styleKey]) return this.styles_[styleKey]; if (this.styles_[styleKey]) return this.styles_[styleKey];
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const itemHeight = this.state.resultsInBody ? 84 : 64; let itemHeight = this.state.resultsInBody ? 84 : 64;
if (this.state.listType === BaseModel.TYPE_COMMAND) {
itemHeight = 40;
}
this.styles_[styleKey] = { this.styles_[styleKey] = {
dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }), dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }),
@ -138,7 +189,7 @@ class Dialog extends React.PureComponent {
}); });
} }
onKeyDown(event) { onKeyDown(event:any) {
if (event.keyCode === 27) { // ESCAPE if (event.keyCode === 27) { // ESCAPE
this.props.dispatch({ this.props.dispatch({
pluginName: PLUGIN_NAME, pluginName: PLUGIN_NAME,
@ -148,7 +199,7 @@ class Dialog extends React.PureComponent {
} }
} }
modalLayer_onClick(event) { modalLayer_onClick(event:any) {
if (event.currentTarget == event.target) { if (event.currentTarget == event.target) {
this.props.dispatch({ this.props.dispatch({
pluginName: PLUGIN_NAME, pluginName: PLUGIN_NAME,
@ -162,7 +213,7 @@ class Dialog extends React.PureComponent {
this.setState({ showHelp: !this.state.showHelp }); this.setState({ showHelp: !this.state.showHelp });
} }
input_onChange(event) { input_onChange(event:any) {
this.setState({ query: event.target.value }); this.setState({ query: event.target.value });
this.scheduleListUpdate(); this.scheduleListUpdate();
@ -177,7 +228,7 @@ class Dialog extends React.PureComponent {
}, 100); }, 100);
} }
makeSearchQuery(query) { makeSearchQuery(query:string) {
const output = []; const output = [];
const splitted = query.split(' '); const splitted = query.split(' ');
@ -190,7 +241,7 @@ class Dialog extends React.PureComponent {
return output.join(' '); return output.join(' ');
} }
async keywords(searchQuery) { async keywords(searchQuery:string) {
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, this.fuzzy_); const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, this.fuzzy_);
return SearchEngine.instance().allParsedQueryTerms(parsedQuery); return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
} }
@ -207,11 +258,28 @@ class Dialog extends React.PureComponent {
if (!this.state.query) { if (!this.state.query) {
this.setState({ results: [], keywords: [] }); this.setState({ results: [], keywords: [] });
} else { } else {
let results = []; let results:SearchResult[] = [];
let listType = null; let listType = null;
let searchQuery = ''; let searchQuery = '';
let keywords = null;
if (this.state.query.indexOf('#') === 0) { // TAGS if (this.state.query.indexOf(':') === 0) { // COMMANDS
const query = this.state.query.substr(1);
listType = BaseModel.TYPE_COMMAND;
keywords = [query];
const commandResults = CommandService.instance().searchCommands(query, true);
results = commandResults.map((result:CommandSearchResult) => {
return {
id: result.commandName,
title: result.title,
parent_id: null,
fields: [],
type: BaseModel.TYPE_COMMAND,
};
});
} else if (this.state.query.indexOf('#') === 0) { // TAGS
listType = BaseModel.TYPE_TAG; listType = BaseModel.TYPE_TAG;
searchQuery = `*${this.state.query.split(' ')[0].substr(1).trim()}*`; searchQuery = `*${this.state.query.split(' ')[0].substr(1).trim()}*`;
results = await Tag.searchAllWithNotes({ titlePattern: searchQuery }); results = await Tag.searchAllWithNotes({ titlePattern: searchQuery });
@ -230,7 +298,7 @@ class Dialog extends React.PureComponent {
searchQuery = this.makeSearchQuery(this.state.query); searchQuery = this.makeSearchQuery(this.state.query);
results = await SearchEngine.instance().search(searchQuery, { fuzzy: this.fuzzy_ }); results = await SearchEngine.instance().search(searchQuery, { fuzzy: this.fuzzy_ });
resultsInBody = !!results.find(row => row.fields.includes('body')); resultsInBody = !!results.find((row:any) => row.fields.includes('body'));
if (!resultsInBody || this.state.query.length <= 1) { if (!resultsInBody || this.state.query.length <= 1) {
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
@ -241,7 +309,9 @@ class Dialog extends React.PureComponent {
} else { } else {
const limit = 20; const limit = 20;
const searchKeywords = await this.keywords(searchQuery); const searchKeywords = await this.keywords(searchQuery);
const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed'] }); const notes = await Note.byIds(results.map((result:any) => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed'] });
// Can't make any sense of this code so...
// @ts-ignore
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {}); const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
@ -272,7 +342,7 @@ class Dialog extends React.PureComponent {
// e.g. 'Joplin is a free, open source' and 'open source note taking application' // e.g. 'Joplin is a free, open source' and 'open source note taking application'
// will result in 'Joplin is a free, open source note taking application' // will result in 'Joplin is a free, open source note taking application'
const mergedIndices = mergeOverlappingIntervals(indices, 3); const mergedIndices = mergeOverlappingIntervals(indices, 3);
fragments = mergedIndices.map(f => body.slice(f[0], f[1])).join(' ... '); fragments = mergedIndices.map((f:any) => body.slice(f[0], f[1])).join(' ... ');
// Add trailing ellipsis if the final fragment doesn't end where the note is ending // Add trailing ellipsis if the final fragment doesn't end where the note is ending
if (mergedIndices.length && mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...'; if (mergedIndices.length && mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
@ -285,7 +355,7 @@ class Dialog extends React.PureComponent {
} }
if (!this.props.showCompletedTodos) { if (!this.props.showCompletedTodos) {
results = results.filter((row) => !row.is_todo || !row.todo_completed); results = results.filter((row:any) => !row.is_todo || !row.todo_completed);
} }
} }
} }
@ -296,20 +366,25 @@ class Dialog extends React.PureComponent {
this.setState({ this.setState({
listType: listType, listType: listType,
results: results, results: results,
keywords: await this.keywords(searchQuery), keywords: keywords ? keywords : await this.keywords(searchQuery),
selectedItemId: results.length === 0 ? null : results[0].id, selectedItemId: results.length === 0 ? null : results[0].id,
resultsInBody: resultsInBody, resultsInBody: resultsInBody,
}); });
} }
} }
async gotoItem(item) { async gotoItem(item:any) {
this.props.dispatch({ this.props.dispatch({
pluginName: PLUGIN_NAME, pluginName: PLUGIN_NAME,
type: 'PLUGINLEGACY_DIALOG_SET', type: 'PLUGINLEGACY_DIALOG_SET',
open: false, open: false,
}); });
if (item.type === BaseModel.TYPE_COMMAND) {
CommandService.instance().execute(item.id);
return;
}
if (this.state.listType === BaseModel.TYPE_NOTE || this.state.listType === BaseModel.TYPE_FOLDER) { if (this.state.listType === BaseModel.TYPE_NOTE || this.state.listType === BaseModel.TYPE_FOLDER) {
const folderPath = await Folder.folderPath(this.props.folders, item.parent_id); const folderPath = await Folder.folderPath(this.props.folders, item.parent_id);
@ -329,7 +404,7 @@ class Dialog extends React.PureComponent {
noteId: item.id, noteId: item.id,
}); });
CommandService.instance().scheduleExecute('focusElement', { target: 'noteBody' }); CommandService.instance().scheduleExecute('focusElement', 'noteBody');
} else if (this.state.listType === BaseModel.TYPE_TAG) { } else if (this.state.listType === BaseModel.TYPE_TAG) {
this.props.dispatch({ this.props.dispatch({
type: 'TAG_SELECT', type: 'TAG_SELECT',
@ -343,17 +418,19 @@ class Dialog extends React.PureComponent {
} }
} }
listItem_onClick(event) { listItem_onClick(event:any) {
const itemId = event.currentTarget.getAttribute('data-id'); const itemId = event.currentTarget.getAttribute('data-id');
const parentId = event.currentTarget.getAttribute('data-parent-id'); const parentId = event.currentTarget.getAttribute('data-parent-id');
const itemType = event.currentTarget.getAttribute('data-type');
this.gotoItem({ this.gotoItem({
id: itemId, id: itemId,
parent_id: parentId, parent_id: parentId,
type: itemType,
}); });
} }
listItemRenderer(item) { renderItem(item:SearchResult) {
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const style = this.style(); const style = this.style();
const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row; const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row;
@ -368,7 +445,7 @@ class Dialog extends React.PureComponent {
const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: (fragmentsHtml) }}></div>; const fragmentComp = !fragmentsHtml ? null : <div style={style.rowFragments} dangerouslySetInnerHTML={{ __html: (fragmentsHtml) }}></div>;
return ( return (
<div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}> <div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id} data-type={item.type}>
<div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div> <div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
{fragmentComp} {fragmentComp}
{pathComp} {pathComp}
@ -376,7 +453,7 @@ class Dialog extends React.PureComponent {
); );
} }
selectedItemIndex(results, itemId) { selectedItemIndex(results:any[] = undefined, itemId:string = undefined) {
if (typeof results === 'undefined') results = this.state.results; if (typeof results === 'undefined') results = this.state.results;
if (typeof itemId === 'undefined') itemId = this.state.selectedItemId; if (typeof itemId === 'undefined') itemId = this.state.selectedItemId;
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
@ -392,7 +469,7 @@ class Dialog extends React.PureComponent {
return this.state.results[index]; return this.state.results[index];
} }
input_onKeyDown(event) { input_onKeyDown(event:any) {
const keyCode = event.keyCode; const keyCode = event.keyCode;
if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) { // DOWN / UP if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) { // DOWN / UP
@ -428,7 +505,7 @@ class Dialog extends React.PureComponent {
const itemListStyle = { const itemListStyle = {
marginTop: 5, marginTop: 5,
height: Math.min(style.itemHeight * this.state.results.length, 7 * style.itemHeight), height: Math.min(style.itemHeight * this.state.results.length, 10 * style.itemHeight),
}; };
return ( return (
@ -437,7 +514,7 @@ class Dialog extends React.PureComponent {
itemHeight={style.itemHeight} itemHeight={style.itemHeight}
items={this.state.results} items={this.state.results}
style={itemListStyle} style={itemListStyle}
itemRenderer={this.listItemRenderer} itemRenderer={this.renderItem}
/> />
); );
} }
@ -445,7 +522,7 @@ class Dialog extends React.PureComponent {
render() { render() {
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
const style = this.style(); const style = this.style();
const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')}</div>; const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.')}</div>;
return ( return (
<div onClick={this.modalLayer_onClick} style={theme.dialogModalLayer}> <div onClick={this.modalLayer_onClick} style={theme.dialogModalLayer}>
@ -463,7 +540,7 @@ class Dialog extends React.PureComponent {
} }
const mapStateToProps = (state) => { const mapStateToProps = (state:AppState) => {
return { return {
folders: state.folders, folders: state.folders,
themeId: state.settings.theme, themeId: state.settings.theme,
@ -485,8 +562,18 @@ GotoAnything.manifest = {
accelerator: () => KeymapService.instance().getAccelerator('gotoAnything'), accelerator: () => KeymapService.instance().getAccelerator('gotoAnything'),
screens: ['Main'], screens: ['Main'],
}, },
{
name: 'main',
parent: 'tools',
label: _('Command palette'),
accelerator: () => KeymapService.instance().getAccelerator('commandPalette'),
screens: ['Main'],
userData: {
startString: ':',
},
},
], ],
}; };
module.exports = GotoAnything; export default GotoAnything;

View File

@ -0,0 +1,5 @@
import { AppState } from '../../app';
export interface DesktopCommandContext {
state: AppState,
}

View File

@ -590,7 +590,24 @@ class BaseModel {
} }
} }
BaseModel.typeEnum_ = [['TYPE_NOTE', 1], ['TYPE_FOLDER', 2], ['TYPE_SETTING', 3], ['TYPE_RESOURCE', 4], ['TYPE_TAG', 5], ['TYPE_NOTE_TAG', 6], ['TYPE_SEARCH', 7], ['TYPE_ALARM', 8], ['TYPE_MASTER_KEY', 9], ['TYPE_ITEM_CHANGE', 10], ['TYPE_NOTE_RESOURCE', 11], ['TYPE_RESOURCE_LOCAL_STATE', 12], ['TYPE_REVISION', 13], ['TYPE_MIGRATION', 14], ['TYPE_SMART_FILTER', 15]]; BaseModel.typeEnum_ = [
['TYPE_NOTE', 1],
['TYPE_FOLDER', 2],
['TYPE_SETTING', 3],
['TYPE_RESOURCE', 4],
['TYPE_TAG', 5],
['TYPE_NOTE_TAG', 6],
['TYPE_SEARCH', 7],
['TYPE_ALARM', 8],
['TYPE_MASTER_KEY', 9],
['TYPE_ITEM_CHANGE', 10],
['TYPE_NOTE_RESOURCE', 11],
['TYPE_RESOURCE_LOCAL_STATE', 12],
['TYPE_REVISION', 13],
['TYPE_MIGRATION', 14],
['TYPE_SMART_FILTER', 15],
['TYPE_COMMAND', 16],
];
for (let i = 0; i < BaseModel.typeEnum_.length; i++) { for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i]; const e = BaseModel.typeEnum_[i];

View File

@ -4,27 +4,16 @@ import { _ } from 'lib/locale';
export const declaration:CommandDeclaration = { export const declaration:CommandDeclaration = {
name: 'historyBackward', name: 'historyBackward',
label: () => _('Back'), label: () => _('Back'),
// iconName: 'fa-arrow-left',
iconName: 'icon-back', iconName: 'icon-back',
}; };
interface Props {
hasBackwardNotes: boolean,
}
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (props:Props) => { execute: async () => {
if (!props.hasBackwardNotes) return;
utils.store.dispatch({ utils.store.dispatch({
type: 'HISTORY_BACKWARD', type: 'HISTORY_BACKWARD',
}); });
}, },
isEnabled: (props:Props) => { enabledCondition: 'historyhasBackwardNotes',
return props.hasBackwardNotes;
},
mapStateToProps: (state:any) => {
return { hasBackwardNotes: state.backwardHistoryNotes.length > 0 };
},
}; };
}; };

View File

@ -7,23 +7,13 @@ export const declaration:CommandDeclaration = {
iconName: 'icon-forward', iconName: 'icon-forward',
}; };
interface Props {
hasForwardNotes: boolean,
}
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async (props:Props) => { execute: async () => {
if (!props.hasForwardNotes) return;
utils.store.dispatch({ utils.store.dispatch({
type: 'HISTORY_FORWARD', type: 'HISTORY_FORWARD',
}); });
}, },
isEnabled: (props:Props) => { enabledCondition: 'historyhasForwardNotes',
return props.hasForwardNotes;
},
mapStateToProps: (state:any) => {
return { hasForwardNotes: state.forwardHistoryNotes.length > 0 };
},
}; };
}; };

View File

@ -1,4 +1,4 @@
import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService'; import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
import { _ } from 'lib/locale'; import { _ } from 'lib/locale';
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
@ -8,9 +8,11 @@ export const declaration:CommandDeclaration = {
iconName: 'fa-sync-alt', iconName: 'fa-sync-alt',
}; };
// Note that this command actually acts as a toggle - it starts or cancels
// synchronisation depending on the "syncStarted" parameter
export const runtime = ():CommandRuntime => { export const runtime = ():CommandRuntime => {
return { return {
execute: async ({ syncStarted }:any) => { execute: async (_context:CommandContext, syncStarted:boolean = false) => {
const action = syncStarted ? 'cancel' : 'start'; const action = syncStarted ? 'cancel' : 'start';
if (!(await reg.syncTarget().isAuthenticated())) { if (!(await reg.syncTarget().isAuthenticated())) {
@ -43,13 +45,5 @@ export const runtime = ():CommandRuntime => {
return 'sync'; return 'sync';
} }
}, },
isEnabled: (props:any) => {
return !props.syncStarted;
},
mapStateToProps: (state:any):any => {
return {
syncStarted: state.syncStarted,
};
},
}; };
}; };

View File

@ -2,6 +2,7 @@ import produce, { Draft } from 'immer';
import pluginServiceReducer, { stateRootKey as pluginServiceStateRootKey, defaultState as pluginServiceDefaultState, State as PluginServiceState } from 'lib/services/plugins/reducer'; import pluginServiceReducer, { stateRootKey as pluginServiceStateRootKey, defaultState as pluginServiceDefaultState, State as PluginServiceState } from 'lib/services/plugins/reducer';
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const BaseModel = require('lib/BaseModel');
const ArrayUtils = require('lib/ArrayUtils.js'); const ArrayUtils = require('lib/ArrayUtils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids'); const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
const { createSelectorCreator, defaultMemoize } = require('reselect'); const { createSelectorCreator, defaultMemoize } = require('reselect');
@ -160,8 +161,6 @@ for (const additionalReducer of additionalReducers) {
export const MAX_HISTORY = 200; export const MAX_HISTORY = 200;
export const stateUtils:any = {};
const derivedStateCache_:any = {}; const derivedStateCache_:any = {};
// Allows, for a given state, to return the same derived // Allows, for a given state, to return the same derived
@ -185,9 +184,7 @@ const createShallowArrayEqualSelector = createSelectorCreator(
} }
); );
// Given an input array, this selector ensures that the same array is returned const selectArrayShallow = createCachedSelector(
// if its content hasn't changed.
stateUtils.selectArrayShallow = createCachedSelector(
(state:any) => state.array, (state:any) => state.array,
(array:any[]) => array (array:any[]) => array
)({ )({
@ -197,85 +194,85 @@ stateUtils.selectArrayShallow = createCachedSelector(
selectorCreator: createShallowArrayEqualSelector, selectorCreator: createShallowArrayEqualSelector,
}); });
stateUtils.hasOneSelectedNote = function(state:State):boolean { class StateUtils {
return state.selectedNoteIds.length === 1;
};
stateUtils.notesOrder = function(stateSettings:any) { // Given an input array, this selector ensures that the same array is returned
if (stateSettings['notes.sortOrder.field'] === 'order') { // if its content hasn't changed.
return cacheEnabledOutput('notesOrder', [ public selectArrayShallow(props:any, cacheKey:any) {
{ return selectArrayShallow(props, cacheKey);
by: 'order',
dir: 'DESC',
},
{
by: 'user_created_time',
dir: 'DESC',
},
]);
} else {
return cacheEnabledOutput('notesOrder', [
{
by: stateSettings['notes.sortOrder.field'],
dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
},
]);
} }
};
stateUtils.foldersOrder = function(stateSettings:any) { public oneNoteSelected(state:State):boolean {
return cacheEnabledOutput('foldersOrder', [ return state.selectedNoteIds.length === 1;
{
by: stateSettings['folders.sortOrder.field'],
dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
},
]);
};
stateUtils.hasNotesBeingSaved = function(state:State):boolean {
for (const id in state.editorNoteStatuses) {
if (state.editorNoteStatuses[id] === 'saving') return true;
} }
return false;
};
stateUtils.parentItem = function(state:State) { public notesOrder(stateSettings:any) {
const t = state.notesParentType; if (stateSettings['notes.sortOrder.field'] === 'order') {
let id = null; return cacheEnabledOutput('notesOrder', [
if (t === 'Folder') id = state.selectedFolderId; {
if (t === 'Tag') id = state.selectedTagId; by: 'order',
if (t === 'Search') id = state.selectedSearchId; dir: 'DESC',
if (!t || !id) return null; },
return { type: t, id: id }; {
}; by: 'user_created_time',
dir: 'DESC',
stateUtils.lastSelectedNoteIds = function(state:State):string[] { },
const parent = stateUtils.parentItem(state); ]);
if (!parent) return []; } else {
const output = (state.lastSelectedNotesIds as any)[parent.type][parent.id]; return cacheEnabledOutput('notesOrder', [
return output ? output : []; {
}; by: stateSettings['notes.sortOrder.field'],
dir: stateSettings['notes.sortOrder.reverse'] ? 'DESC' : 'ASC',
stateUtils.getCurrentNote = function(state:State) { },
const selectedNoteIds = state.selectedNoteIds; ]);
const notes = state.notes;
if (selectedNoteIds != null && selectedNoteIds.length > 0) {
const currNote = notes.find(note => note.id === selectedNoteIds[0]);
if (currNote != null) {
return {
id: currNote.id,
parent_id: currNote.parent_id,
notesParentType: state.notesParentType,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
searches: state.searches,
selectedSmartFilterId: state.selectedSmartFilterId,
};
} }
} }
return null;
}; public foldersOrder(stateSettings:any) {
return cacheEnabledOutput('foldersOrder', [
{
by: stateSettings['folders.sortOrder.field'],
dir: stateSettings['folders.sortOrder.reverse'] ? 'DESC' : 'ASC',
},
]);
}
public hasNotesBeingSaved(state:State):boolean {
for (const id in state.editorNoteStatuses) {
if (state.editorNoteStatuses[id] === 'saving') return true;
}
return false;
}
public parentItem(state:State) {
const t = state.notesParentType;
let id = null;
if (t === 'Folder') id = state.selectedFolderId;
if (t === 'Tag') id = state.selectedTagId;
if (t === 'Search') id = state.selectedSearchId;
if (!t || !id) return null;
return { type: t, id: id };
}
public lastSelectedNoteIds(state:State):string[] {
const parent = this.parentItem(state);
if (!parent) return [];
const output = (state.lastSelectedNotesIds as any)[parent.type][parent.id];
return output ? output : [];
}
public selectedNote(state:State):any {
const noteId = this.selectedNoteId(state);
return noteId ? BaseModel.byId(state.notes, noteId) : null;
}
public selectedNoteId(state:State):any {
return state.selectedNoteIds.length ? state.selectedNoteIds[0] : null;
}
}
export const stateUtils:StateUtils = new StateUtils();
function arrayHasEncryptedItems(array:any[]) { function arrayHasEncryptedItems(array:any[]) {
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
@ -526,8 +523,29 @@ const getContextFromHistory = (ctx:any) => {
return result; return result;
}; };
function getNoteHistoryInfo(state:State) {
const selectedNoteIds = state.selectedNoteIds;
const notes = state.notes;
if (selectedNoteIds != null && selectedNoteIds.length > 0) {
const currNote = notes.find(note => note.id === selectedNoteIds[0]);
if (currNote != null) {
return {
id: currNote.id,
parent_id: currNote.parent_id,
notesParentType: state.notesParentType,
selectedFolderId: state.selectedFolderId,
selectedTagId: state.selectedTagId,
selectedSearchId: state.selectedSearchId,
searches: state.searches,
selectedSmartFilterId: state.selectedSmartFilterId,
};
}
}
return null;
}
function handleHistory(draft:Draft<State>, action:any) { function handleHistory(draft:Draft<State>, action:any) {
const currentNote = stateUtils.getCurrentNote(draft); const currentNote = getNoteHistoryInfo(draft);
switch (action.type) { switch (action.type) {
case 'HISTORY_BACKWARD': { case 'HISTORY_BACKWARD': {
const note = draft.backwardHistoryNotes[draft.backwardHistoryNotes.length - 1]; const note = draft.backwardHistoryNotes[draft.backwardHistoryNotes.length - 1];
@ -1086,6 +1104,7 @@ const reducer = produce((draft: Draft<State> = defaultState, action:any) => {
const newPluginsLegacy = Object.assign({}, draft.pluginsLegacy); const newPluginsLegacy = Object.assign({}, draft.pluginsLegacy);
const newPlugin = draft.pluginsLegacy[action.pluginName] ? Object.assign({}, draft.pluginsLegacy[action.pluginName]) : {}; const newPlugin = draft.pluginsLegacy[action.pluginName] ? Object.assign({}, draft.pluginsLegacy[action.pluginName]) : {};
if ('open' in action) newPlugin.dialogOpen = action.open; if ('open' in action) newPlugin.dialogOpen = action.open;
if ('userData' in action) newPlugin.userData = action.userData;
newPluginsLegacy[action.pluginName] = newPlugin; newPluginsLegacy[action.pluginName] = newPlugin;
draft.pluginsLegacy = newPluginsLegacy; draft.pluginsLegacy = newPluginsLegacy;
} }

View File

@ -1,30 +1,23 @@
import { State } from 'lib/reducer';
import eventManager from 'lib/eventManager'; import eventManager from 'lib/eventManager';
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from 'lib/markdownUtils';
import BaseService from 'lib/services/BaseService'; import BaseService from 'lib/services/BaseService';
import shim from 'lib/shim'; import shim from 'lib/shim';
import WhenClause from './WhenClause';
import stateToWhenClauseContext from './commands/stateToWhenClauseContext';
type LabelFunction = () => string; type LabelFunction = () => string;
type EnabledCondition = string;
export interface CommandContext {
// The state may also be of type "AppState" (used by the desktop app), which inherits from "State" (used by all apps)
state: State,
}
export interface CommandRuntime { export interface CommandRuntime {
execute(props:any):Promise<any> execute(context:CommandContext, ...args:any[]):Promise<any>
isEnabled?(props:any):boolean enabledCondition?: EnabledCondition;
// "state" type is "AppState" but in order not to introduce a
// dependency to the desktop app (so that the service can
// potentially be used by the mobile app too), we keep it as "any".
// Individual commands can define it as state:AppState when relevant.
//
// In general this method should reduce the provided state to only
// what's absolutely necessary. For example, if the property of a
// note is needed, return only that particular property and not the
// whole note object. This will ensure that components that depends
// on this command are not uncessarily re-rendered. A note object for
// example might change frequently but its markdown_language property
// will almost never change.
mapStateToProps?(state:any):any
// Used for the (optional) toolbar button title // Used for the (optional) toolbar button title
title?(props:any):string, mapStateToTitle?(state:any):string,
} }
export interface CommandDeclaration { export interface CommandDeclaration {
@ -33,6 +26,9 @@ export interface CommandDeclaration {
// Used for the menu item label, and toolbar button tooltip // Used for the menu item label, and toolbar button tooltip
label?: LabelFunction | string, label?: LabelFunction | string,
// Command description - if none is provided, the label will be used as description
description?: string,
// This is a bit of a hack because some labels don't make much sense in isolation. For example, // This is a bit of a hack because some labels don't make much sense in isolation. For example,
// the commmand to focus the note list is called just "Note list". This makes sense within the menu // the commmand to focus the note list is called just "Note list". This makes sense within the menu
// but not so much within the keymap config screen, where the parent item is not displayed. Because // but not so much within the keymap config screen, where the parent item is not displayed. Because
@ -88,30 +84,29 @@ interface CommandByNameOptions {
runtimeMustBeRegistered?:boolean, runtimeMustBeRegistered?:boolean,
} }
interface CommandState { export interface SearchResult {
commandName: string,
title: string, title: string,
enabled: boolean,
}
interface CommandStates {
[key:string]: CommandState
} }
export default class CommandService extends BaseService { export default class CommandService extends BaseService {
private static instance_:CommandService; private static instance_:CommandService;
static instance():CommandService { public static instance():CommandService {
if (this.instance_) return this.instance_; if (this.instance_) return this.instance_;
this.instance_ = new CommandService(); this.instance_ = new CommandService();
return this.instance_; return this.instance_;
} }
private commands_:Commands = {}; private commands_:Commands = {};
private commandPreviousStates_:CommandStates = {}; private store_:any;
private devMode_:boolean;
initialize(store:any) { public initialize(store:any, devMode:boolean) {
utils.store = store; utils.store = store;
this.store_ = store;
this.devMode_ = devMode;
} }
public on(eventName:string, callback:Function) { public on(eventName:string, callback:Function) {
@ -122,6 +117,36 @@ export default class CommandService extends BaseService {
eventManager.off(eventName, callback); eventManager.off(eventName, callback);
} }
public searchCommands(query:string, returnAllWhenEmpty:boolean, excludeWithoutLabel:boolean = true):SearchResult[] {
query = query.toLowerCase();
const output = [];
for (const commandName of this.commandNames()) {
const label = this.label(commandName, true);
if (!label && excludeWithoutLabel) continue;
const title = label ? `${label} (${commandName})` : commandName;
if ((returnAllWhenEmpty && !query) || title.toLowerCase().includes(query)) {
output.push({
commandName: commandName,
title: title,
});
}
}
output.sort((a:SearchResult, b:SearchResult) => {
return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : +1;
});
return output;
}
public commandNames() {
return Object.keys(this.commands_);
}
public commandByName(name:string, options:CommandByNameOptions = null):Command { public commandByName(name:string, options:CommandByNameOptions = null):Command {
options = { options = {
mustExist: true, mustExist: true,
@ -140,7 +165,7 @@ export default class CommandService extends BaseService {
return command; return command;
} }
registerDeclaration(declaration:CommandDeclaration) { public registerDeclaration(declaration:CommandDeclaration) {
declaration = { ...declaration }; declaration = { ...declaration };
if (!declaration.label) declaration.label = ''; if (!declaration.label) declaration.label = '';
if (!declaration.iconName) declaration.iconName = ''; if (!declaration.iconName) declaration.iconName = '';
@ -148,83 +173,89 @@ export default class CommandService extends BaseService {
this.commands_[declaration.name] = { this.commands_[declaration.name] = {
declaration: declaration, declaration: declaration,
}; };
delete this.commandPreviousStates_[declaration.name];
} }
registerRuntime(commandName:string, runtime:CommandRuntime) { public registerRuntime(commandName:string, runtime:CommandRuntime) {
if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`); if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`);
const command = this.commandByName(commandName); const command = this.commandByName(commandName);
runtime = Object.assign({}, runtime); runtime = Object.assign({}, runtime);
if (!runtime.isEnabled) runtime.isEnabled = () => true; if (!runtime.enabledCondition) runtime.enabledCondition = 'true';
if (!runtime.title) runtime.title = () => null;
command.runtime = runtime; command.runtime = runtime;
delete this.commandPreviousStates_[commandName];
} }
componentRegisterCommands(component:any, commands:any[]) { public componentRegisterCommands(component:any, commands:any[]) {
for (const command of commands) { for (const command of commands) {
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component)); CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component));
} }
} }
componentUnregisterCommands(commands:any[]) { public componentUnregisterCommands(commands:any[]) {
for (const command of commands) { for (const command of commands) {
CommandService.instance().unregisterRuntime(command.declaration.name); CommandService.instance().unregisterRuntime(command.declaration.name);
} }
} }
unregisterRuntime(commandName:string) { public unregisterRuntime(commandName:string) {
const command = this.commandByName(commandName, { mustExist: false }); const command = this.commandByName(commandName, { mustExist: false });
if (!command || !command.runtime) return; if (!command || !command.runtime) return;
delete command.runtime; delete command.runtime;
delete this.commandPreviousStates_[commandName];
} }
async execute(commandName:string, props:any = null):Promise<any> { public async execute(commandName:string, ...args:any[]):Promise<any> {
const command = this.commandByName(commandName); const command = this.commandByName(commandName);
this.logger().info('CommandService::execute:', commandName, props); this.logger().info('CommandService::execute:', commandName, args);
return command.runtime.execute(props ? props : {}); return command.runtime.execute({ state: this.store_.getState() }, ...args);
} }
scheduleExecute(commandName:string, args:any) { public scheduleExecute(commandName:string, args:any) {
shim.setTimeout(() => { shim.setTimeout(() => {
this.execute(commandName, args); this.execute(commandName, args);
}, 10); }, 10);
} }
isEnabled(commandName:string, props:any):boolean { public currentWhenClauseContext() {
return stateToWhenClauseContext(this.store_.getState());
}
// When looping on commands and checking their enabled state, the whenClauseContext
// should be specified (created using currentWhenClauseContext) to avoid having
// to re-create it on each call.
public isEnabled(commandName:string, whenClauseContext:any = null):boolean {
const command = this.commandByName(commandName); const command = this.commandByName(commandName);
if (!command || !command.runtime) return false; if (!command || !command.runtime) return false;
// if (!command.runtime.props) return false;
return command.runtime.isEnabled(props); if (!whenClauseContext) whenClauseContext = this.currentWhenClauseContext();
const exp = new WhenClause(command.runtime.enabledCondition, this.devMode_);
return exp.evaluate(whenClauseContext);
} }
commandMapStateToProps(commandName:string, state:any):any { // The title is dynamic and derived from the state, which is why the state is passed
const command = this.commandByName(commandName); // as an argument. Title can be used for example to display the alarm date on the
if (!command.runtime) return null; // "set alarm" toolbar button.
if (!command.runtime.mapStateToProps) return {}; public title(commandName:string, state:any = null):string {
return command.runtime.mapStateToProps(state);
}
title(commandName:string, props:any):string {
const command = this.commandByName(commandName); const command = this.commandByName(commandName);
if (!command || !command.runtime) return null; if (!command || !command.runtime) return null;
return command.runtime.title(props);
state = state || this.store_.getState();
if (command.runtime.mapStateToTitle) {
return command.runtime.mapStateToTitle(state);
} else {
return '';
}
} }
iconName(commandName:string, variant:string = null):string { public iconName(commandName:string, variant:string = null):string {
const command = this.commandByName(commandName); const command = this.commandByName(commandName);
if (!command) throw new Error(`No such command: ${commandName}`); if (!command) throw new Error(`No such command: ${commandName}`);
if (variant === 'tinymce') return command.declaration.tinymceIconName ? command.declaration.tinymceIconName : 'preferences'; if (variant === 'tinymce') return command.declaration.tinymceIconName ? command.declaration.tinymceIconName : 'preferences';
return command.declaration.iconName; return command.declaration.iconName;
} }
label(commandName:string, fullLabel:boolean = false):string { public label(commandName:string, fullLabel:boolean = false):string {
const command = this.commandByName(commandName); const command = this.commandByName(commandName);
if (!command) throw new Error(`Command: ${commandName} is not declared`); if (!command) throw new Error(`Command: ${commandName} is not declared`);
const output = []; const output = [];
@ -240,42 +271,15 @@ export default class CommandService extends BaseService {
return output.join(': '); return output.join(': ');
} }
exists(commandName:string):boolean { public description(commandName:string):string {
const command = this.commandByName(commandName);
if (command.declaration.description) return command.declaration.description;
return this.label(commandName, true);
}
public exists(commandName:string):boolean {
const command = this.commandByName(commandName, { mustExist: false }); const command = this.commandByName(commandName, { mustExist: false });
return !!command; return !!command;
} }
public commandsToMarkdownTable(state:any):string {
const headers:MarkdownTableHeader[] = [
{
name: 'commandName',
label: 'Name',
},
{
name: 'description',
label: 'Description',
},
{
name: 'props',
label: 'Props',
},
];
const rows:MarkdownTableRow[] = [];
for (const commandName in this.commands_) {
const props = this.commandMapStateToProps(commandName, state);
const row:MarkdownTableRow = {
commandName: commandName,
description: this.label(commandName),
props: JSON.stringify(props),
};
rows.push(row);
}
return markdownUtils.createMarkdownTable(headers, rows);
}
} }

View File

@ -16,7 +16,7 @@ const defaultKeymapItems = {
{ accelerator: 'Cmd+N', command: 'newNote' }, { accelerator: 'Cmd+N', command: 'newNote' },
{ accelerator: 'Cmd+T', command: 'newTodo' }, { accelerator: 'Cmd+T', command: 'newTodo' },
{ accelerator: 'Cmd+S', command: 'synchronize' }, { accelerator: 'Cmd+S', command: 'synchronize' },
{ accelerator: 'Cmd+P', command: 'print' }, { accelerator: '', command: 'print' },
{ accelerator: 'Cmd+H', command: 'hideApp' }, { accelerator: 'Cmd+H', command: 'hideApp' },
{ accelerator: 'Cmd+Q', command: 'quit' }, { accelerator: 'Cmd+Q', command: 'quit' },
{ accelerator: 'Cmd+,', command: 'config' }, { accelerator: 'Cmd+,', command: 'config' },
@ -37,20 +37,21 @@ const defaultKeymapItems = {
{ accelerator: 'Shift+Cmd+L', command: 'focusElementNoteList' }, { accelerator: 'Shift+Cmd+L', command: 'focusElementNoteList' },
{ accelerator: 'Shift+Cmd+N', command: 'focusElementNoteTitle' }, { accelerator: 'Shift+Cmd+N', command: 'focusElementNoteTitle' },
{ accelerator: 'Shift+Cmd+B', command: 'focusElementNoteBody' }, { accelerator: 'Shift+Cmd+B', command: 'focusElementNoteBody' },
{ accelerator: 'Option+Cmd+S', command: 'toggleSidebar' }, { accelerator: 'Option+Cmd+S', command: 'toggleSideBar' },
{ accelerator: 'Option+Cmd+L', command: 'toggleNoteList' }, { accelerator: 'Option+Cmd+L', command: 'toggleNoteList' },
{ accelerator: 'Cmd+L', command: 'toggleVisiblePanes' }, { accelerator: 'Cmd+L', command: 'toggleVisiblePanes' },
{ accelerator: 'Cmd+0', command: 'zoomActualSize' }, { accelerator: 'Cmd+0', command: 'zoomActualSize' },
{ accelerator: 'Cmd+E', command: 'toggleExternalEditing' }, { accelerator: 'Cmd+E', command: 'toggleExternalEditing' },
{ accelerator: 'Option+Cmd+T', command: 'setTags' }, { accelerator: 'Option+Cmd+T', command: 'setTags' },
{ accelerator: 'Cmd+G', command: 'gotoAnything' }, { accelerator: 'Cmd+P', command: 'gotoAnything' },
{ accelerator: 'Shift+Cmd+P', command: 'commandPalette' },
{ accelerator: 'F1', command: 'help' }, { accelerator: 'F1', command: 'help' },
], ],
default: [ default: [
{ accelerator: 'Ctrl+N', command: 'newNote' }, { accelerator: 'Ctrl+N', command: 'newNote' },
{ accelerator: 'Ctrl+T', command: 'newTodo' }, { accelerator: 'Ctrl+T', command: 'newTodo' },
{ accelerator: 'Ctrl+S', command: 'synchronize' }, { accelerator: 'Ctrl+S', command: 'synchronize' },
{ accelerator: 'Ctrl+P', command: 'print' }, { accelerator: '', command: 'print' },
{ accelerator: 'Ctrl+Q', command: 'quit' }, { accelerator: 'Ctrl+Q', command: 'quit' },
{ accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' }, { accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' },
{ accelerator: 'Ctrl+C', command: 'textCopy' }, { accelerator: 'Ctrl+C', command: 'textCopy' },
@ -68,14 +69,15 @@ const defaultKeymapItems = {
{ accelerator: 'Ctrl+Shift+L', command: 'focusElementNoteList' }, { accelerator: 'Ctrl+Shift+L', command: 'focusElementNoteList' },
{ accelerator: 'Ctrl+Shift+N', command: 'focusElementNoteTitle' }, { accelerator: 'Ctrl+Shift+N', command: 'focusElementNoteTitle' },
{ accelerator: 'Ctrl+Shift+B', command: 'focusElementNoteBody' }, { accelerator: 'Ctrl+Shift+B', command: 'focusElementNoteBody' },
{ accelerator: 'F10', command: 'toggleSidebar' }, { accelerator: 'F10', command: 'toggleSideBar' },
{ accelerator: 'F11', command: 'toggleNoteList' }, { accelerator: 'F11', command: 'toggleNoteList' },
{ accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' }, { accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' },
{ accelerator: 'Ctrl+0', command: 'zoomActualSize' }, { accelerator: 'Ctrl+0', command: 'zoomActualSize' },
{ accelerator: 'Ctrl+E', command: 'toggleExternalEditing' }, { accelerator: 'Ctrl+E', command: 'toggleExternalEditing' },
{ accelerator: 'Ctrl+Alt+T', command: 'setTags' }, { accelerator: 'Ctrl+Alt+T', command: 'setTags' },
{ accelerator: 'Ctrl+,', command: 'config' }, { accelerator: 'Ctrl+,', command: 'config' },
{ accelerator: 'Ctrl+G', command: 'gotoAnything' }, { accelerator: 'Ctrl+P', command: 'gotoAnything' },
{ accelerator: 'Ctrl+Shift+P', command: 'commandPalette' },
{ accelerator: 'F1', command: 'help' }, { accelerator: 'F1', command: 'help' },
], ],
}; };
@ -90,13 +92,14 @@ interface Keymap {
} }
export default class KeymapService extends BaseService { export default class KeymapService extends BaseService {
private keymap: Keymap; private keymap: Keymap;
private platform: string; private platform: string;
private customKeymapPath: string; private customKeymapPath: string;
private defaultKeymapItems: KeymapItem[]; private defaultKeymapItems: KeymapItem[];
private lastSaveTime_:number; private lastSaveTime_:number;
constructor() { public constructor() {
super(); super();
this.lastSaveTime_ = Date.now(); this.lastSaveTime_ = Date.now();
@ -106,11 +109,11 @@ export default class KeymapService extends BaseService {
this.initialize(); this.initialize();
} }
get lastSaveTime():number { public get lastSaveTime():number {
return this.lastSaveTime_; return this.lastSaveTime_;
} }
initialize(platform: string = shim.platformName()) { public initialize(platform: string = shim.platformName()) {
this.platform = platform; this.platform = platform;
switch (platform) { switch (platform) {
@ -131,7 +134,7 @@ export default class KeymapService extends BaseService {
} }
} }
async loadCustomKeymap(customKeymapPath: string) { public async loadCustomKeymap(customKeymapPath: string) {
this.customKeymapPath = customKeymapPath; // Useful for saving the changes later this.customKeymapPath = customKeymapPath; // Useful for saving the changes later
if (await shim.fsDriver().exists(customKeymapPath)) { if (await shim.fsDriver().exists(customKeymapPath)) {
@ -143,7 +146,7 @@ export default class KeymapService extends BaseService {
} }
} }
async saveCustomKeymap(customKeymapPath: string = this.customKeymapPath) { public async saveCustomKeymap(customKeymapPath: string = this.customKeymapPath) {
this.logger().info(`KeymapService: Saving keymap to file: ${customKeymapPath}`); this.logger().info(`KeymapService: Saving keymap to file: ${customKeymapPath}`);
try { try {
@ -161,7 +164,7 @@ export default class KeymapService extends BaseService {
} }
} }
acceleratorExists(command: string) { public acceleratorExists(command: string) {
return !!this.keymap[command]; return !!this.keymap[command];
} }
@ -189,33 +192,33 @@ export default class KeymapService extends BaseService {
}; };
} }
setAccelerator(command: string, accelerator: string) { public setAccelerator(command: string, accelerator: string) {
this.keymap[command].accelerator = accelerator; this.keymap[command].accelerator = accelerator;
} }
getAccelerator(command: string) { public getAccelerator(command: string) {
const item = this.keymap[command]; const item = this.keymap[command];
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`); if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
return item.accelerator; return item.accelerator;
} }
getDefaultAccelerator(command: string) { public getDefaultAccelerator(command: string) {
const defaultItem = this.defaultKeymapItems.find((item => item.command === command)); const defaultItem = this.defaultKeymapItems.find((item => item.command === command));
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`); if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
return defaultItem.accelerator; return defaultItem.accelerator;
} }
getCommandNames() { public getCommandNames() {
return Object.keys(this.keymap); return Object.keys(this.keymap);
} }
getKeymapItems() { public getKeymapItems() {
return Object.values(this.keymap); return Object.values(this.keymap);
} }
getCustomKeymapItems() { public getCustomKeymapItems() {
const customkeymapItems: KeymapItem[] = []; const customkeymapItems: KeymapItem[] = [];
this.defaultKeymapItems.forEach(({ command, accelerator }) => { this.defaultKeymapItems.forEach(({ command, accelerator }) => {
const currentAccelerator = this.getAccelerator(command); const currentAccelerator = this.getAccelerator(command);
@ -236,11 +239,11 @@ export default class KeymapService extends BaseService {
return customkeymapItems; return customkeymapItems;
} }
getDefaultKeymapItems() { public getDefaultKeymapItems() {
return [...this.defaultKeymapItems]; return [...this.defaultKeymapItems];
} }
overrideKeymap(customKeymapItems: KeymapItem[]) { public overrideKeymap(customKeymapItems: KeymapItem[]) {
try { try {
for (let i = 0; i < customKeymapItems.length; i++) { for (let i = 0; i < customKeymapItems.length; i++) {
const item = customKeymapItems[i]; const item = customKeymapItems[i];
@ -284,7 +287,7 @@ export default class KeymapService extends BaseService {
} }
} }
validateKeymap(proposedKeymapItem: KeymapItem = null) { public validateKeymap(proposedKeymapItem: KeymapItem = null) {
const usedAccelerators = new Set(); const usedAccelerators = new Set();
// Validate as if the proposed change is already present in the current keymap // Validate as if the proposed change is already present in the current keymap
@ -312,7 +315,7 @@ export default class KeymapService extends BaseService {
} }
} }
validateAccelerator(accelerator: string) { public validateAccelerator(accelerator: string) {
let keyFound = false; let keyFound = false;
const parts = accelerator.split('+'); const parts = accelerator.split('+');
@ -334,7 +337,7 @@ export default class KeymapService extends BaseService {
if (!isValid) throw new Error(_('Accelerator "%s" is not valid.', accelerator)); if (!isValid) throw new Error(_('Accelerator "%s" is not valid.', accelerator));
} }
domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) { public domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) {
const parts = []; const parts = [];
const { key, ctrlKey, metaKey, altKey, shiftKey } = event; const { key, ctrlKey, metaKey, altKey, shiftKey } = event;
@ -358,7 +361,7 @@ export default class KeymapService extends BaseService {
return parts.join('+'); return parts.join('+');
} }
static domToElectronKey(domKey: string) { private static domToElectronKey(domKey: string) {
let electronKey; let electronKey;
if (/^([a-z])$/.test(domKey)) { if (/^([a-z])$/.test(domKey)) {
@ -398,7 +401,7 @@ export default class KeymapService extends BaseService {
private static instance_:KeymapService = null; private static instance_:KeymapService = null;
static instance():KeymapService { public static instance():KeymapService {
if (this.instance_) return this.instance_; if (this.instance_) return this.instance_;
this.instance_ = new KeymapService(); this.instance_ = new KeymapService();

View File

@ -1,5 +1,4 @@
const Logger = require('lib/Logger').default; const Logger = require('lib/Logger').default;
const KeymapService = require('lib/services/KeymapService').default;
class PluginManager { class PluginManager {
constructor() { constructor() {
@ -52,6 +51,7 @@ class PluginManager {
const p = this.pluginInstance_(event.pluginName); const p = this.pluginInstance_(event.pluginName);
p.onTrigger({ p.onTrigger({
itemName: event.itemName, itemName: event.itemName,
userData: event.userData,
}); });
} }
@ -65,7 +65,7 @@ class PluginManager {
return { return {
Dialog: Class.Dialog, Dialog: Class.Dialog,
props: this.dialogProps_(name), props: Object.assign({}, this.dialogProps_(name), { userData: p.userData }),
}; };
} }
@ -81,20 +81,24 @@ class PluginManager {
menuItems() { menuItems() {
let output = []; let output = [];
const keymapService = KeymapService.instance();
for (const name in this.plugins_) { for (const name in this.plugins_) {
const menuItems = this.plugins_[name].Class.manifest.menuItems; const menuItems = this.plugins_[name].Class.manifest.menuItems.slice();
if (!menuItems) continue; if (!menuItems) continue;
for (const item of menuItems) { for (let i = 0; i < menuItems.length; i++) {
const item = Object.assign({}, menuItems[i]);
item.click = () => { item.click = () => {
this.onPluginMenuItemTrigger_({ this.onPluginMenuItemTrigger_({
pluginName: name, pluginName: name,
itemName: item.name, itemName: item.name,
userData: item.userData,
}); });
}; };
item.accelerator = keymapService.getAccelerator(name);
item.accelerator = menuItems[i].accelerator();
menuItems[i] = item;
} }
output = output.concat(menuItems); output = output.concat(menuItems);

View File

@ -1,12 +1,14 @@
import { ContextKeyExpr, ContextKeyExpression } from './contextkey/contextkey'; import { ContextKeyExpr, ContextKeyExpression } from './contextkey/contextkey';
export default class BooleanExpression { export default class WhenClause {
private expression_:string; private expression_:string;
private validate_:boolean;
private rules_:ContextKeyExpression = null; private rules_:ContextKeyExpression = null;
constructor(expression:string) { constructor(expression:string, validate:boolean) {
this.expression_ = expression; this.expression_ = expression;
this.validate_ = validate;
} }
private createContext(ctx: any) { private createContext(ctx: any) {
@ -21,11 +23,20 @@ export default class BooleanExpression {
if (!this.rules_) { if (!this.rules_) {
this.rules_ = ContextKeyExpr.deserialize(this.expression_); this.rules_ = ContextKeyExpr.deserialize(this.expression_);
} }
return this.rules_; return this.rules_;
} }
public evaluate(context:any):boolean { public evaluate(context:any):boolean {
if (this.validate_) this.validate(context);
return this.rules.evaluate(this.createContext(context)); return this.rules.evaluate(this.createContext(context));
} }
public validate(context:any) {
const keys = this.rules.keys();
for (const key of keys) {
if (!(key in context)) throw new Error(`No such key: ${key}`);
}
}
} }

View File

@ -87,9 +87,9 @@ export default class MenuUtils {
return item; return item;
} }
public commandToStatefulMenuItem(commandName:string, props:any = null):MenuItem { public commandToStatefulMenuItem(commandName:string, ...args:any[]):MenuItem {
return this.commandToMenuItem(commandName, () => { return this.commandToMenuItem(commandName, () => {
return this.service.execute(commandName, props ? props : {}); return this.service.execute(commandName, ...args);
}); });
} }
@ -108,11 +108,14 @@ export default class MenuUtils {
return output; return output;
} }
public commandsToMenuItemProps(state:any, commandNames:string[]):MenuItemProps { public commandsToMenuItemProps(commandNames:string[], whenClauseContext:any):MenuItemProps {
const output:MenuItemProps = {}; const output:MenuItemProps = {};
for (const commandName of commandNames) { for (const commandName of commandNames) {
const newProps = this.service.commandMapStateToProps(commandName, state); const newProps = {
enabled: this.service.isEnabled(commandName, whenClauseContext),
};
if (newProps === null || propsHaveChanged(this.menuItemPropsCache_[commandName], newProps)) { if (newProps === null || propsHaveChanged(this.menuItemPropsCache_[commandName], newProps)) {
output[commandName] = newProps; output[commandName] = newProps;
this.menuItemPropsCache_[commandName] = newProps; this.menuItemPropsCache_[commandName] = newProps;

View File

@ -1,5 +1,4 @@
import CommandService from '../CommandService'; import CommandService from 'lib/services/CommandService';
import propsHaveChanged from './propsHaveChanged';
import { stateUtils } from 'lib/reducer'; import { stateUtils } from 'lib/reducer';
const separatorItem = { type: 'separator' }; const separatorItem = { type: 'separator' };
@ -14,7 +13,6 @@ export interface ToolbarButtonInfo {
} }
interface ToolbarButtonCacheItem { interface ToolbarButtonCacheItem {
props: any,
info: ToolbarButtonInfo, info: ToolbarButtonInfo,
} }
@ -35,8 +33,15 @@ export default class ToolbarButtonUtils {
return this.service_; return this.service_;
} }
private commandToToolbarButton(commandName:string, props:any):ToolbarButtonInfo { private commandToToolbarButton(commandName:string, whenClauseContext:any):ToolbarButtonInfo {
if (this.toolbarButtonCache_[commandName] && !propsHaveChanged(this.toolbarButtonCache_[commandName].props, props)) { const newEnabled = this.service.isEnabled(commandName, whenClauseContext);
const newTitle = this.service.title(commandName);
if (
this.toolbarButtonCache_[commandName] &&
this.toolbarButtonCache_[commandName].info.enabled === newEnabled &&
this.toolbarButtonCache_[commandName].info.title === newTitle
) {
return this.toolbarButtonCache_[commandName].info; return this.toolbarButtonCache_[commandName].info;
} }
@ -46,15 +51,14 @@ export default class ToolbarButtonUtils {
name: commandName, name: commandName,
tooltip: this.service.label(commandName), tooltip: this.service.label(commandName),
iconName: command.declaration.iconName, iconName: command.declaration.iconName,
enabled: this.service.isEnabled(commandName, props), enabled: newEnabled,
onClick: async () => { onClick: async () => {
this.service.execute(commandName, props); this.service.execute(commandName);
}, },
title: this.service.title(commandName, props), title: newTitle,
}; };
this.toolbarButtonCache_[commandName] = { this.toolbarButtonCache_[commandName] = {
props: props,
info: output, info: output,
}; };
@ -64,7 +68,7 @@ export default class ToolbarButtonUtils {
// This method ensures that if the provided commandNames and state hasn't changed // This method ensures that if the provided commandNames and state hasn't changed
// the output also won't change. Invididual toolbarButtonInfo also won't changed // the output also won't change. Invididual toolbarButtonInfo also won't changed
// if the state they use hasn't changed. This is to avoid useless renders of the toolbars. // if the state they use hasn't changed. This is to avoid useless renders of the toolbars.
public commandsToToolbarButtons(state:any, commandNames:string[]):ToolbarButtonInfo[] { public commandsToToolbarButtons(commandNames:string[], whenClauseContext:any):ToolbarButtonInfo[] {
const output:ToolbarButtonInfo[] = []; const output:ToolbarButtonInfo[] = [];
for (const commandName of commandNames) { for (const commandName of commandNames) {
@ -73,8 +77,7 @@ export default class ToolbarButtonUtils {
continue; continue;
} }
const props = this.service.commandMapStateToProps(commandName, state); output.push(this.commandToToolbarButton(commandName, whenClauseContext));
output.push(this.commandToToolbarButton(commandName, props));
} }
return stateUtils.selectArrayShallow({ array: output }, commandNames.join('_')); return stateUtils.selectArrayShallow({ array: output }, commandNames.join('_'));

View File

@ -0,0 +1,32 @@
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from 'lib/markdownUtils';
export default function commandsToMarkdownTable():string {
const headers:MarkdownTableHeader[] = [
{
name: 'commandName',
label: 'Name',
},
{
name: 'description',
label: 'Description',
},
{
name: 'props',
label: 'Props',
},
];
const rows:MarkdownTableRow[] = [];
for (const commandName in this.commands_) {
const row:MarkdownTableRow = {
commandName: commandName,
description: this.label(commandName),
};
rows.push(row);
}
return markdownUtils.createMarkdownTable(headers, rows);
}

View File

@ -1,5 +1,7 @@
export default function propsHaveChanged(previous:any, next:any):boolean { export default function propsHaveChanged(previous:any, next:any):boolean {
if (!previous && next) return true; if (!previous && next) return true;
if (previous && !next) return true;
if (!previous && !next) return false;
if (Object.keys(previous).length !== Object.keys(next).length) return true; if (Object.keys(previous).length !== Object.keys(next).length) return true;

View File

@ -0,0 +1,47 @@
import { stateUtils } from 'lib/reducer';
const BaseModel = require('lib/BaseModel');
const Folder = require('lib/models/Folder');
const MarkupToHtml = require('lib/joplin-renderer/MarkupToHtml');
export default function stateToWhenClauseContext(state:any) {
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
const note = noteId ? BaseModel.byId(state.notes, noteId) : null;
return {
// UI elements
markdownEditorVisible: !!state.settings['editor.codeView'],
richTextEditorVisible: !state.settings['editor.codeView'],
markdownEditorPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('editor'),
markdownViewerPaneVisible: state.settings['editor.codeView'] && state.noteVisiblePanes.includes('viewer'),
modalDialogVisible: !!Object.keys(state.visibleDialogs).length,
sideBarVisible: !!state.sidebarVisibility,
noteListHasNotes: !!state.notes.length,
// Application state
notesAreBeingSaved: stateUtils.hasNotesBeingSaved(state),
syncStarted: state.syncStarted,
// Current location
inConflictFolder: state.selectedFolderId === Folder.conflictFolderId(),
// Note selection
oneNoteSelected: !!note,
someNotesSelected: state.selectedNoteIds.length > 0,
multipleNotesSelected: state.selectedNoteIds.length > 1,
noNotesSelected: !state.selectedNoteIds.length,
// Note history
historyhasBackwardNotes: state.backwardHistoryNotes.length > 0,
historyhasForwardNotes: state.forwardHistoryNotes.length > 0,
// Folder selection
oneFolderSelected: !!state.selectedFolderId,
// Current note properties
noteIsTodo: note ? !!note.is_todo : false,
noteTodoCompleted: note ? !!note.todo_completed : false,
noteIsMarkdown: note ? note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN : false,
noteIsHtml: note ? note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML : false,
};
}

View File

@ -30,11 +30,11 @@ export default class JoplinCommands {
* *
* // Create a new sub-notebook under the provided notebook * // Create a new sub-notebook under the provided notebook
* // Note: internally, notebooks are called "folders". * // Note: internally, notebooks are called "folders".
* await joplin.commands.execute('newFolder', { parent_id: "SOME_FOLDER_ID" }); * await joplin.commands.execute('newFolder', "SOME_FOLDER_ID");
* ``` * ```
*/ */
async execute(commandName: string, props: any = null):Promise<any> { async execute(commandName: string, ...args:any[]):Promise<any> {
return CommandService.instance().execute(commandName, props); return CommandService.instance().execute(commandName, ...args);
} }
/** /**
@ -65,8 +65,7 @@ export default class JoplinCommands {
execute: command.execute, execute: command.execute,
}; };
if ('isEnabled' in command) runtime.isEnabled = command.isEnabled; if ('enabledCondition' in command) runtime.enabledCondition = command.enabledCondition;
if ('mapStateToProps' in command) runtime.mapStateToProps = command.mapStateToProps;
CommandService.instance().registerDeclaration(declaration); CommandService.instance().registerDeclaration(declaration);
CommandService.instance().registerRuntime(declaration.name, runtime); CommandService.instance().registerRuntime(declaration.name, runtime);

View File

@ -3,12 +3,47 @@
// ================================================================= // =================================================================
export interface Command { export interface Command {
/**
* Name of command - must be globally unique
*/
name: string name: string
/**
* Label to be displayed on menu items or keyboard shortcut editor for example
*/
label: string label: string
/**
* Icon to be used on toolbar buttons for example
*/
iconName?: string, iconName?: string,
/**
* Code to be ran when the command is executed. It maybe return a result.
*/
execute(props:any):Promise<any> execute(props:any):Promise<any>
isEnabled?(props:any):boolean
mapStateToProps?(state:any):any /**
* Defines whether the command should be enabled or disabled, which in turns affects
* the enabled state of any associated button or menu item.
*
* The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to
* `true` or `false`. It supports the following operators:
*
* Operator | Symbol | Example
* -- | -- | --
* Equality | == | "editorType == markdown"
* Inequality | != | "currentScreen != config"
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder"
*
* Currently the supported context variables aren't documented, but you can find the list there:
*
* https://github.com/laurent22/joplin/blob/dev/ReactNativeClient/lib/services/commands/stateToWhenClauseContext.ts
*
* Note: Commands are enabled by default unless you use this property.
*/
enabledCondition?: string
} }
// ================================================================= // =================================================================

View File

@ -87,7 +87,7 @@ function filterLogs(logs, platform) {
if (platform === 'android' && prefix.indexOf('android') >= 0) addIt = true; if (platform === 'android' && prefix.indexOf('android') >= 0) addIt = true;
if (platform === 'ios' && prefix.indexOf('ios') >= 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) 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 === 'cli' && prefix.indexOf('cli') >= 0) addIt = true;
if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true; if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true;
@ -121,7 +121,7 @@ function formatCommitMessage(msg, author, options) {
const isPlatformPrefix = prefix => { const isPlatformPrefix = prefix => {
prefix = prefix.split(',').map(p => p.trim().toLowerCase()); prefix = prefix.split(',').map(p => p.trim().toLowerCase());
for (const p of prefix) { 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; return false;
}; };
@ -129,6 +129,7 @@ function formatCommitMessage(msg, author, options) {
if (splitted.length) { if (splitted.length) {
const platform = splitted[0].trim().toLowerCase(); const platform = splitted[0].trim().toLowerCase();
if (platform === 'api') subModule = 'api'; if (platform === 'api') subModule = 'api';
if (platform === 'plugins') subModule = 'plugins';
if (isPlatformPrefix(platform)) { if (isPlatformPrefix(platform)) {
splitted.splice(0, 1); splitted.splice(0, 1);
} }

View File

@ -17,6 +17,8 @@ module.exports = {
'**/Modules/TinyMCE/IconPack/**', '**/Modules/TinyMCE/IconPack/**',
'**/CliClient/tests/support/plugins/**', '**/CliClient/tests/support/plugins/**',
'**/plugin_types/**', '**/plugin_types/**',
'**/ReactNativeClient/android/**',
'**/ReactNativeClient/ios/**',
], ],
}).map(f => f.substr(rootDir.length + 1)); }).map(f => f.substr(rootDir.length + 1));

View File

@ -59,7 +59,6 @@
"ElectronClient/dist/": true, "ElectronClient/dist/": true,
"ElectronClient/fonts/": true, "ElectronClient/fonts/": true,
"ElectronClient/gui/note-viewer/highlight/styles/": true, "ElectronClient/gui/note-viewer/highlight/styles/": true,
"ElectronClient/lib/": true,
"ElectronClient/locale/": true, "ElectronClient/locale/": true,
"ElectronClient/build/": true, "ElectronClient/build/": true,
"node_modules/": true, "node_modules/": true,
@ -255,6 +254,7 @@
"ElectronClient/**/.DS_Store": true, "ElectronClient/**/.DS_Store": true,
"ElectronClient/**/gui/note-viewer/pluginAssets/": true, "ElectronClient/**/gui/note-viewer/pluginAssets/": true,
"ElectronClient/**/pluginAssets/": true, "ElectronClient/**/pluginAssets/": true,
"ElectronClient/lib/": true,
"Clipper/popup/**/node_modules": true, "Clipper/popup/**/node_modules": true,
"Clipper/popup/**/coverage": true, "Clipper/popup/**/coverage": true,
"Clipper/popup/**/build": true, "Clipper/popup/**/build": true,
@ -360,7 +360,9 @@
"CliClient/tests/support/plugins/settings/**/node_modules/": true, "CliClient/tests/support/plugins/settings/**/node_modules/": true,
"CliClient/tests/support/plugins/selected_text/dist/*": true, "CliClient/tests/support/plugins/selected_text/dist/*": true,
"CliClient/tests/support/plugins/selected_text/**/node_modules/": true, "CliClient/tests/support/plugins/selected_text/**/node_modules/": true,
"**/CliClient/build/": true "**/CliClient/build/": true,
"**/*.js": {"when": "$(basename).ts"},
"**/*?.js": { "when": "$(basename).tsx"},
}, },
"spellright.language": [ "spellright.language": [
"en" "en"

View File

@ -15,7 +15,7 @@
"generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json", "generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json",
"setupNewRelease": "node ./Tools/setupNewRelease", "setupNewRelease": "node ./Tools/setupNewRelease",
"linkChecker": "linkchecker https://joplinapp.org", "linkChecker": "linkchecker https://joplinapp.org",
"clean": "npm run clean", "clean": "gulp clean",
"postinstall": "cd Tools && npm i && cd .. && cd ReactNativeClient && npm i && cd .. && cd ElectronClient && npm i && cd .. && cd CliClient && npm i && cd .. && gulp build" "postinstall": "cd Tools && npm i && cd .. && cd ReactNativeClient && npm i && cd .. && cd ElectronClient && npm i && cd .. && cd CliClient && npm i && cd .. && gulp build"
}, },
"husky": { "husky": {

View File

@ -125,7 +125,7 @@ joplin.plugins.register({
}); });
``` ```
Later you will also need a way to generate the slug for each header. A slug is an identifier which is used to link to a particular header. Essentially a header text like "My Header" is converted to "my-header". And if there's already a slug with that name, a number is appended to it. Without going into too much details, you will need the "slug" package to generate this for you, so install it using `npm i -s uslug` from the root of your plugin directory. Later you will also need a way to generate the slug for each header. A slug is an identifier which is used to link to a particular header. Essentially a header text like "My Header" is converted to "my-header". And if there's already a slug with that name, a number is appended to it. Without going into too much details, you will need the "slug" package to generate this for you, so install it using `npm i -s 'git+https://github.com/laurent22/uslug.git#emoji-support'` from the root of your plugin directory (Note: you can also install the "uslug" package on its own, but it won't have emoji support).
Then this is the function you will need for Joplin, so copy it somewhere in your file: Then this is the function you will need for Joplin, so copy it somewhere in your file:
@ -322,9 +322,7 @@ joplin.plugins.register({
if (message.name === 'scrollToHash') { if (message.name === 'scrollToHash') {
// As the name says, the scrollToHash command makes the note scroll // As the name says, the scrollToHash command makes the note scroll
// to the provided hash. // to the provided hash.
joplin.commands.execute('scrollToHash', { joplin.commands.execute('scrollToHash', message.hash)
hash: message.hash,
})
} }
}); });