mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +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:
parent
f529adac99
commit
3a57cfea02
@ -114,7 +114,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
|
||||
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleEditors.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/MainScreen.js
|
||||
ElectronClient/gui/MenuBar.js
|
||||
@ -187,7 +187,9 @@ ElectronClient/gui/ToolbarButton/styles/index.js
|
||||
ElectronClient/gui/ToolbarButton/ToolbarButton.js
|
||||
ElectronClient/gui/utils/NoteListUtils.js
|
||||
ElectronClient/InteropServiceHelper.js
|
||||
ElectronClient/plugins/GotoAnything.js
|
||||
ElectronClient/services/bridge.js
|
||||
ElectronClient/services/commands/types.js
|
||||
ElectronClient/services/plugins/hooks/useThemeCss.js
|
||||
ElectronClient/services/plugins/hooks/useViewIsReady.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/AlarmServiceDriverNode.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/propsHaveChanged.js
|
||||
ReactNativeClient/lib/services/commands/stateToWhenClauseContext.js
|
||||
ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js
|
||||
ReactNativeClient/lib/services/CommandService.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/utils/types.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/services/WhenClause.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
ReactNativeClient/lib/shim.js
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -108,7 +108,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
|
||||
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleEditors.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/MainScreen.js
|
||||
ElectronClient/gui/MenuBar.js
|
||||
@ -181,7 +181,9 @@ ElectronClient/gui/ToolbarButton/styles/index.js
|
||||
ElectronClient/gui/ToolbarButton/ToolbarButton.js
|
||||
ElectronClient/gui/utils/NoteListUtils.js
|
||||
ElectronClient/InteropServiceHelper.js
|
||||
ElectronClient/plugins/GotoAnything.js
|
||||
ElectronClient/services/bridge.js
|
||||
ElectronClient/services/commands/types.js
|
||||
ElectronClient/services/plugins/hooks/useThemeCss.js
|
||||
ElectronClient/services/plugins/hooks/useViewIsReady.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/AlarmServiceDriverNode.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/propsHaveChanged.js
|
||||
ReactNativeClient/lib/services/commands/stateToWhenClauseContext.js
|
||||
ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js
|
||||
ReactNativeClient/lib/services/CommandService.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/utils/types.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/services/WhenClause.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
ReactNativeClient/lib/shim.js
|
||||
|
8
.ignore
8
.ignore
@ -57,7 +57,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
|
||||
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
|
||||
ElectronClient/gui/MainScreen/commands/toggleEditors.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/MainScreen.js
|
||||
ElectronClient/gui/MenuBar.js
|
||||
@ -130,7 +130,9 @@ ElectronClient/gui/ToolbarButton/styles/index.js
|
||||
ElectronClient/gui/ToolbarButton/ToolbarButton.js
|
||||
ElectronClient/gui/utils/NoteListUtils.js
|
||||
ElectronClient/InteropServiceHelper.js
|
||||
ElectronClient/plugins/GotoAnything.js
|
||||
ElectronClient/services/bridge.js
|
||||
ElectronClient/services/commands/types.js
|
||||
ElectronClient/services/plugins/hooks/useThemeCss.js
|
||||
ElectronClient/services/plugins/hooks/useViewIsReady.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/AlarmServiceDriverNode.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/propsHaveChanged.js
|
||||
ReactNativeClient/lib/services/commands/stateToWhenClauseContext.js
|
||||
ReactNativeClient/lib/services/commands/ToolbarButtonUtils.js
|
||||
ReactNativeClient/lib/services/CommandService.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/utils/types.js
|
||||
ReactNativeClient/lib/services/UndoRedoService.js
|
||||
ReactNativeClient/lib/services/WhenClause.js
|
||||
ReactNativeClient/lib/ShareExtension.js
|
||||
ReactNativeClient/lib/shareHandler.js
|
||||
ReactNativeClient/lib/shim.js
|
||||
|
@ -2,7 +2,7 @@ import MenuUtils from 'lib/services/commands/MenuUtils';
|
||||
import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils';
|
||||
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 {
|
||||
declaration: CommandDeclaration,
|
||||
@ -11,7 +11,12 @@ interface TestCommand {
|
||||
|
||||
function newService():CommandService {
|
||||
const service = new CommandService();
|
||||
service.initialize({});
|
||||
const mockStore = {
|
||||
getState: () => {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
service.initialize(mockStore, true);
|
||||
return service;
|
||||
}
|
||||
|
||||
@ -24,8 +29,7 @@ function createCommand(name:string, options:any):TestCommand {
|
||||
execute: options.execute,
|
||||
};
|
||||
|
||||
if (options.mapStateToProps) runtime.mapStateToProps = options.mapStateToProps;
|
||||
if (options.isEnabled) runtime.isEnabled = options.isEnabled;
|
||||
if (options.enabledCondition) runtime.enabledCondition = options.enabledCondition;
|
||||
|
||||
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[1].onClick();
|
||||
@ -77,98 +81,78 @@ describe('services_CommandService', function() {
|
||||
|
||||
registerCommand(service, createCommand('test1', {
|
||||
execute: () => {},
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
selectedNoteId: state.selectedNoteId,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
};
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return props.selectedNoteId === 'abc';
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
}));
|
||||
|
||||
registerCommand(service, createCommand('test2', {
|
||||
execute: () => {},
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
selectedNoteId: state.selectedNoteId,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
};
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return props.selectedNoteId === '123';
|
||||
},
|
||||
enabledCondition: 'multipleNotesSelected',
|
||||
}));
|
||||
|
||||
const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons({
|
||||
selectedNoteId: '123',
|
||||
selectedFolderId: 'aaa',
|
||||
}, ['test1', 'test2']);
|
||||
const toolbarInfos = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
|
||||
oneNoteSelected: false,
|
||||
multipleNotesSelected: true,
|
||||
});
|
||||
|
||||
expect(toolbarInfos[0].enabled).toBe(false);
|
||||
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 () => {
|
||||
const service = newService();
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(service);
|
||||
|
||||
registerCommand(service, createCommand('test1', {
|
||||
execute: () => {},
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
selectedNoteId: state.selectedNoteId,
|
||||
};
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return props.selectedNoteId === 'ok';
|
||||
},
|
||||
enabledCondition: 'cond1',
|
||||
}));
|
||||
|
||||
registerCommand(service, createCommand('test2', {
|
||||
execute: () => {},
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
};
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return props.selectedFolderId === 'ok';
|
||||
},
|
||||
enabledCondition: 'cond2',
|
||||
}));
|
||||
|
||||
const toolbarInfos1 = toolbarButtonUtils.commandsToToolbarButtons({
|
||||
selectedNoteId: 'ok',
|
||||
selectedFolderId: 'notok',
|
||||
}, ['test1', 'test2']);
|
||||
const toolbarInfos1 = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: false,
|
||||
});
|
||||
|
||||
const toolbarInfos2 = toolbarButtonUtils.commandsToToolbarButtons({
|
||||
selectedNoteId: 'ok',
|
||||
selectedFolderId: 'notok',
|
||||
}, ['test1', 'test2']);
|
||||
const toolbarInfos2 = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: false,
|
||||
});
|
||||
|
||||
expect(toolbarInfos1).toBe(toolbarInfos2);
|
||||
expect(toolbarInfos1[0] === toolbarInfos2[0]).toBe(true);
|
||||
expect(toolbarInfos1[1] === toolbarInfos2[1]).toBe(true);
|
||||
|
||||
const toolbarInfos3 = toolbarButtonUtils.commandsToToolbarButtons({
|
||||
selectedNoteId: 'ok',
|
||||
selectedFolderId: 'ok',
|
||||
}, ['test1', 'test2']);
|
||||
const toolbarInfos3 = toolbarButtonUtils.commandsToToolbarButtons(['test1', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: true,
|
||||
});
|
||||
|
||||
expect(toolbarInfos2 === toolbarInfos3).toBe(false);
|
||||
expect(toolbarInfos2[0] === toolbarInfos3[0]).toBe(true);
|
||||
expect(toolbarInfos2[1] === toolbarInfos3[1]).toBe(false);
|
||||
|
||||
{
|
||||
expect(toolbarButtonUtils.commandsToToolbarButtons({
|
||||
selectedNoteId: 'ok',
|
||||
selectedFolderId: 'notok',
|
||||
}, ['test1', '-', 'test2'])).toBe(toolbarButtonUtils.commandsToToolbarButtons({
|
||||
selectedNoteId: 'ok',
|
||||
selectedFolderId: 'notok',
|
||||
}, ['test1', '-', 'test2']));
|
||||
expect(toolbarButtonUtils.commandsToToolbarButtons(['test1', '-', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: false,
|
||||
})).toBe(toolbarButtonUtils.commandsToToolbarButtons(['test1', '-', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: false,
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
@ -206,50 +190,37 @@ describe('services_CommandService', function() {
|
||||
const utils = new MenuUtils(service);
|
||||
|
||||
registerCommand(service, createCommand('test1', {
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
isOk: state.test1 === 'ok',
|
||||
};
|
||||
},
|
||||
execute: () => {},
|
||||
enabledCondition: 'cond1',
|
||||
}));
|
||||
|
||||
registerCommand(service, createCommand('test2', {
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
isOk: state.test2 === 'ok',
|
||||
};
|
||||
},
|
||||
execute: () => {},
|
||||
enabledCondition: 'cond2',
|
||||
}));
|
||||
|
||||
{
|
||||
const menuItemProps = utils.commandsToMenuItemProps({
|
||||
test1: 'ok',
|
||||
test2: 'notok',
|
||||
}, ['test1', 'test2']);
|
||||
const menuItemProps = utils.commandsToMenuItemProps(['test1', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: false,
|
||||
});
|
||||
|
||||
expect(menuItemProps.test1.isOk).toBe(true);
|
||||
expect(menuItemProps.test2.isOk).toBe(false);
|
||||
expect(menuItemProps.test1.enabled).toBe(true);
|
||||
expect(menuItemProps.test2.enabled).toBe(false);
|
||||
}
|
||||
|
||||
{
|
||||
const menuItemProps = utils.commandsToMenuItemProps({
|
||||
test1: 'ok',
|
||||
test2: 'ok',
|
||||
}, ['test1', 'test2']);
|
||||
const menuItemProps = utils.commandsToMenuItemProps(['test1', 'test2'], {
|
||||
cond1: true,
|
||||
cond2: true,
|
||||
});
|
||||
|
||||
expect(menuItemProps.test1.isOk).toBe(true);
|
||||
expect(menuItemProps.test2.isOk).toBe(true);
|
||||
expect(menuItemProps.test1.enabled).toBe(true);
|
||||
expect(menuItemProps.test2.enabled).toBe(true);
|
||||
}
|
||||
|
||||
expect(utils.commandsToMenuItemProps({
|
||||
test1: 'ok',
|
||||
test2: 'ok',
|
||||
}, ['test1', 'test2'])).toBe(utils.commandsToMenuItemProps({
|
||||
test1: 'ok',
|
||||
test2: 'ok',
|
||||
}, ['test1', 'test2']));
|
||||
expect(utils.commandsToMenuItemProps(['test1', 'test2'], { cond1: true, cond2: true }))
|
||||
.toBe(utils.commandsToMenuItemProps(['test1', 'test2'], { cond1: true, cond2: true }));
|
||||
}));
|
||||
|
||||
it('should create stateful menu items', asyncTest(async () => {
|
||||
@ -259,20 +230,30 @@ describe('services_CommandService', function() {
|
||||
let propValue = null;
|
||||
|
||||
registerCommand(service, createCommand('test1', {
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
isOk: state.test1 === 'ok',
|
||||
};
|
||||
},
|
||||
execute: (props:any) => {
|
||||
propValue = props.isOk;
|
||||
execute: (_context:any, greeting:string) => {
|
||||
propValue = greeting;
|
||||
},
|
||||
}));
|
||||
|
||||
const menuItem = utils.commandToStatefulMenuItem('test1', { isOk: 'hello' });
|
||||
const menuItem = utils.commandToStatefulMenuItem('test1', 'hello');
|
||||
menuItem.click();
|
||||
|
||||
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 }));
|
||||
}));
|
||||
|
||||
|
||||
});
|
||||
|
@ -45,7 +45,7 @@ joplin.plugins.register({
|
||||
newLines.push(newCells.join(' | '));
|
||||
}
|
||||
|
||||
await joplin.commands.execute('replaceSelection', { value: newLines.join('\n') });
|
||||
await joplin.commands.execute('replaceSelection', newLines.join('\n'));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -2420,6 +2420,11 @@
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@ -2741,6 +2746,14 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"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": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
@ -3429,11 +3442,6 @@
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"slug": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/slug/-/slug-3.3.4.tgz",
|
||||
"integrity": "sha512-VpHbtRCEWmgaZsrZcTsVl/Dhw98lcrOYDO17DNmJCNpppI6s3qJvnNu2Q3D4L84/2bi6vkW40mjNQI9oGQsflg=="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@ -3981,6 +3989,11 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
|
||||
|
@ -20,6 +20,6 @@
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"slug": "^3.3.4"
|
||||
"uslug": "git+https://github.com/laurent22/uslug.git#emoji-support"
|
||||
}
|
||||
}
|
||||
|
@ -49,9 +49,7 @@ joplin.plugins.register({
|
||||
|
||||
panels.onMessage(view, (message:any) => {
|
||||
if (message.name === 'scrollToHash') {
|
||||
joplin.commands.execute('scrollToHash', {
|
||||
hash: message.hash,
|
||||
})
|
||||
joplin.commands.execute('scrollToHash', message.hash)
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -155,7 +155,7 @@ export default class InteropServiceHelper {
|
||||
|
||||
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 = {};
|
||||
exportOptions.path = path;
|
||||
|
@ -58,7 +58,7 @@ const commands = [
|
||||
require('./gui/MainScreen/commands/showNoteProperties'),
|
||||
require('./gui/MainScreen/commands/showShareNoteDialog'),
|
||||
require('./gui/MainScreen/commands/toggleNoteList'),
|
||||
require('./gui/MainScreen/commands/toggleSidebar'),
|
||||
require('./gui/MainScreen/commands/toggleSideBar'),
|
||||
require('./gui/MainScreen/commands/toggleVisiblePanes'),
|
||||
require('./gui/MainScreen/commands/toggleEditors'),
|
||||
require('./gui/NoteEditor/commands/focusElementNoteBody'),
|
||||
@ -85,7 +85,7 @@ const globalCommands = [
|
||||
const editorCommandDeclarations = require('./gui/NoteEditor/commands/editorCommandDeclarations').default;
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything.min'),
|
||||
require('./plugins/GotoAnything').default,
|
||||
];
|
||||
|
||||
interface AppStateRoute {
|
||||
@ -513,7 +513,7 @@ class Application extends BaseApplication {
|
||||
|
||||
this.initRedux();
|
||||
|
||||
CommandService.instance().initialize(this.store());
|
||||
CommandService.instance().initialize(this.store(), Setting.value('env') == 'dev');
|
||||
|
||||
for (const command of commands) {
|
||||
CommandService.instance().registerDeclaration(command.declaration);
|
||||
|
@ -6,7 +6,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ target }:any) => {
|
||||
execute: async (_context:any, target:string) => {
|
||||
if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody');
|
||||
if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList');
|
||||
if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar');
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
const Note = require('lib/models/Note');
|
||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
|
||||
interface Props {
|
||||
noteId: string
|
||||
}
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'startExternalEditing',
|
||||
label: () => _('Edit in external editor'),
|
||||
@ -16,21 +13,16 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
execute: async (context:CommandContext, noteId:string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
try {
|
||||
const note = await Note.load(props.noteId);
|
||||
const note = await Note.load(noteId);
|
||||
ExternalEditWatcher.instance().openAndWatch(note);
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return {
|
||||
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
};
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||
|
||||
interface Props {
|
||||
noteId: string
|
||||
}
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'stopExternalEditing',
|
||||
label: () => _('Stop external editing'),
|
||||
@ -14,14 +11,10 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
ExternalEditWatcher.instance().stopWatching(props.noteId);
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
|
||||
execute: async (context:CommandContext, noteId:string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
ExternalEditWatcher.instance().stopWatching(noteId);
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
|
||||
import CommandService, { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
import { AppState } from '../app';
|
||||
import CommandService from 'lib/services/CommandService';
|
||||
|
||||
interface Props {
|
||||
noteId: string
|
||||
noteIsBeingWatched: boolean
|
||||
}
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
import { DesktopCommandContext } from '../services/commands/types';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'toggleExternalEditing',
|
||||
@ -16,27 +11,21 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
if (!props.noteId) return;
|
||||
execute: async (context:DesktopCommandContext, noteId:string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
if (props.noteIsBeingWatched) {
|
||||
CommandService.instance().execute('stopExternalEditing', { noteId: props.noteId });
|
||||
if (!noteId) return;
|
||||
|
||||
if (context.state.watchedNoteFiles.includes(noteId)) {
|
||||
CommandService.instance().execute('stopExternalEditing', noteId);
|
||||
} else {
|
||||
CommandService.instance().execute('startExternalEditing', { noteId: props.noteId });
|
||||
CommandService.instance().execute('startExternalEditing', noteId);
|
||||
}
|
||||
},
|
||||
isEnabled: (props:Props) => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:AppState):Props => {
|
||||
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') : '';
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
mapStateToTitle: (state:any) => {
|
||||
const noteId = stateUtils.selectedNoteId(state);
|
||||
return state.watchedNoteFiles.includes(noteId) ? _('Stop') : '';
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -26,6 +26,8 @@ const getLabel = (commandName: string) => {
|
||||
return _('Hide Joplin');
|
||||
case 'closeWindow':
|
||||
return _('Close Window');
|
||||
case 'commandPalette':
|
||||
return _('Command palette');
|
||||
case 'config':
|
||||
return shim.isMac() ? _('Preferences') : _('Options');
|
||||
default:
|
||||
|
@ -61,7 +61,7 @@ const commands = [
|
||||
require('./commands/showShareNoteDialog'),
|
||||
require('./commands/toggleEditors'),
|
||||
require('./commands/toggleNoteList'),
|
||||
require('./commands/toggleSidebar'),
|
||||
require('./commands/toggleSideBar'),
|
||||
require('./commands/toggleVisiblePanes'),
|
||||
];
|
||||
|
||||
@ -320,7 +320,7 @@ class MainScreenComponent extends React.Component<any, any> {
|
||||
window.removeEventListener('resize', this.window_resize);
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
toggleSideBar() {
|
||||
this.props.dispatch({
|
||||
type: 'SIDEBAR_VISIBILITY_TOGGLE',
|
||||
});
|
||||
|
@ -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 { _ } from 'lib/locale';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
const Note = require('lib/models/Note');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const { time } = require('lib/time-utils');
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -13,7 +13,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
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 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 {
|
||||
noteId: note ? noteId : null,
|
||||
noteIsTodo: note ? note.is_todo : false,
|
||||
noteTodoCompleted: note ? note.todo_completed : false,
|
||||
noteTodoDue: note ? note.todo_due : null,
|
||||
};
|
||||
enabledCondition: 'oneNoteSelected && noteIsTodo && !noteTodoCompleted',
|
||||
|
||||
mapStateToTitle: (state:any) => {
|
||||
const note = stateUtils.selectedNote(state);
|
||||
return note && note.todo_due ? time.formatMsToLocal(note.todo_due) : null;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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 InteropServiceHelper from '../../../InteropServiceHelper';
|
||||
import { _ } from 'lib/locale';
|
||||
@ -12,8 +12,10 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteIds }:any) => {
|
||||
execute: async (context:CommandContext, noteIds:string[] = null) => {
|
||||
try {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
|
||||
if (!noteIds.length) throw new Error('No notes selected for pdf export');
|
||||
|
||||
let path = null;
|
||||
@ -22,7 +24,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
|
||||
defaultPath: await InteropServiceHelper.defaultFilename(noteIds[0], 'pdf'),
|
||||
});
|
||||
|
||||
} else {
|
||||
path = bridge().showOpenDialog({
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
@ -50,13 +51,7 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return !!props.noteIds.length;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
noteIds: state.selectedNoteIds,
|
||||
};
|
||||
},
|
||||
|
||||
enabledCondition: 'someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
|
||||
import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'hideModalMessage',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Note = require('lib/models/Note');
|
||||
@ -10,7 +10,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteIds }:any) => {
|
||||
execute: async (context:CommandContext, noteIds:string[] = null) => {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
|
||||
const folders:any[] = await Folder.sortFolderTree();
|
||||
const startFolders:any[] = [];
|
||||
const maxDepth = 15;
|
||||
@ -42,13 +44,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return !!props.noteIds.length;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
noteIds: state.selectedNoteIds,
|
||||
};
|
||||
},
|
||||
enabledCondition: 'someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const Folder = require('lib/models/Folder');
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
@ -11,7 +11,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ parentId }:any) => {
|
||||
execute: async (_context:CommandContext, parentId:string = null) => {
|
||||
comp.setState({
|
||||
promptOptions: {
|
||||
label: _('Notebook title:'),
|
||||
@ -39,8 +39,5 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
title: () => {
|
||||
return _('New notebook');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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';
|
||||
const Setting = require('lib/models/Setting').default;
|
||||
const Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -13,7 +12,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ template, isTodo }:any) => {
|
||||
execute: async (_context:CommandContext, template:string = null, isTodo:boolean = false) => {
|
||||
const folderId = Setting.value('activeFolderId');
|
||||
if (!folderId) return;
|
||||
|
||||
@ -34,12 +33,6 @@ export const runtime = ():CommandRuntime => {
|
||||
id: newNote.id,
|
||||
});
|
||||
},
|
||||
isEnabled: () => {
|
||||
const { folders, selectedFolderId } = utils.store.getState();
|
||||
return !!folders.length && selectedFolderId !== Folder.conflictFolderId();
|
||||
},
|
||||
title: () => {
|
||||
return _('New note');
|
||||
},
|
||||
enabledCondition: 'oneFolderSelected && !inConflictFolder',
|
||||
};
|
||||
};
|
||||
|
@ -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';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -9,14 +9,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ template }:any) => {
|
||||
return CommandService.instance().execute('newNote', { template: template, isTodo: true });
|
||||
},
|
||||
isEnabled: () => {
|
||||
return CommandService.instance().isEnabled('newNote', {});
|
||||
},
|
||||
title: () => {
|
||||
return _('New to-do');
|
||||
execute: async (_context:CommandContext, template:string = null) => {
|
||||
return CommandService.instance().execute('newNote', template, true);
|
||||
},
|
||||
enabledCondition: 'oneFolderSelected && !inConflictFolder',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
|
||||
@ -10,8 +10,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteIds }:any) => {
|
||||
// TODO: test
|
||||
execute: async (context:CommandContext, noteIds:string[] = null) => {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
|
||||
try {
|
||||
if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.'));
|
||||
await comp.printTo_('printer', { noteId: noteIds[0] });
|
||||
@ -19,13 +20,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return !!props.noteIds.length;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
noteIds: state.selectedNoteIds,
|
||||
};
|
||||
},
|
||||
enabledCondition: 'someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const Folder = require('lib/models/Folder');
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
@ -10,7 +10,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ folderId }:any) => {
|
||||
execute: async (context:CommandContext, folderId:string = null) => {
|
||||
folderId = folderId || context.state.selectedFolderId;
|
||||
|
||||
const folder = await Folder.load(folderId);
|
||||
|
||||
if (folder) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const Tag = require('lib/models/Tag');
|
||||
const bridge = require('electron').remote.require('./bridge').default;
|
||||
@ -10,7 +10,10 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
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);
|
||||
if (tag) {
|
||||
comp.setState({
|
||||
|
@ -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 uuid = require('lib/uuid').default;
|
||||
|
||||
@ -9,7 +9,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ query }:any) => {
|
||||
execute: async (_context:CommandContext, query:string) => {
|
||||
if (!comp.searchId_) comp.searchId_ = uuid.create();
|
||||
|
||||
comp.props.dispatch({
|
||||
|
@ -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';
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
|
||||
@ -8,7 +8,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteType }:any) => {
|
||||
execute: async (_context:CommandContext, noteType:string) => {
|
||||
comp.setState({
|
||||
promptOptions: {
|
||||
label: _('Template file:'),
|
||||
@ -18,9 +18,9 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
onClose: async (answer:any) => {
|
||||
if (answer) {
|
||||
if (noteType === 'note' || noteType === 'todo') {
|
||||
CommandService.instance().execute('newNote', { template: answer.value, isTodo: noteType === 'todo' });
|
||||
CommandService.instance().execute('newNote', answer.value, noteType === 'todo');
|
||||
} else {
|
||||
CommandService.instance().execute('insertText', { value: TemplateUtils.render(answer.value) });
|
||||
CommandService.instance().execute('insertText', TemplateUtils.render(answer.value));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const Tag = require('lib/models/Tag');
|
||||
|
||||
@ -10,7 +10,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteIds }:any) => {
|
||||
execute: async (context:CommandContext, noteIds:string[] = null) => {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
|
||||
const tags = await Tag.commonTagsByNoteIds(noteIds);
|
||||
const startTags = tags
|
||||
.map((a:any) => {
|
||||
@ -64,11 +66,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !!props.noteIds.length;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { noteIds: state.selectedNoteIds };
|
||||
},
|
||||
enabledCondition: 'someNotesSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
|
||||
import { CommandDeclaration, CommandRuntime, CommandContext } from 'lib/services/CommandService';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'showModalMessage',
|
||||
@ -7,7 +7,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ message }:any) => {
|
||||
execute: async (_context:CommandContext, message:string) => {
|
||||
comp.setState({
|
||||
modalLayer: {
|
||||
visible: true,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
const Note = require('lib/models/Note');
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -9,7 +10,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteId }:any) => {
|
||||
execute: async (context:CommandContext, noteId:string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
const note = await Note.load(noteId);
|
||||
if (note) {
|
||||
comp.setState({
|
||||
@ -21,11 +24,7 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
});
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
|
||||
},
|
||||
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -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 { stateUtils } from 'lib/reducer';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'showNoteProperties',
|
||||
@ -9,7 +10,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteId }:any) => {
|
||||
execute: async (context:CommandContext, noteId:string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
comp.setState({
|
||||
notePropertiesDialogOptions: {
|
||||
noteId: noteId,
|
||||
@ -20,11 +23,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
},
|
||||
});
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -8,7 +8,9 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteIds }:any) => {
|
||||
execute: async (context:CommandContext, noteIds:string[] = null) => {
|
||||
noteIds = noteIds || context.state.selectedNoteIds;
|
||||
|
||||
comp.setState({
|
||||
shareNoteDialogOptions: {
|
||||
noteIds: noteIds,
|
||||
|
@ -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 { stateUtils } from 'lib/reducer';
|
||||
import { _ } from 'lib/locale';
|
||||
@ -11,22 +11,14 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:any) => {
|
||||
execute: async (context:CommandContext) => {
|
||||
// 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
|
||||
// TinyMCE because it won't have time to send its content before
|
||||
// being switch to Ace Editor.
|
||||
if (props.hasNotesBeingSaved) return;
|
||||
if (stateUtils.hasNotesBeingSaved(context.state)) return;
|
||||
Setting.toggle('editor.codeView');
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return !props.hasNotesBeingSaved && props.hasOneSelectedNote;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
hasNotesBeingSaved: stateUtils.hasNotesBeingSaved(state),
|
||||
hasOneSelectedNote: state.selectedNoteIds.length === 1,
|
||||
};
|
||||
},
|
||||
enabledCondition: '!notesAreBeingSaved && oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
|
||||
import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
|
||||
import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'toggleSidebar',
|
||||
name: 'toggleSideBar',
|
||||
label: () => _('Toggle sidebar'),
|
||||
iconName: 'fas fa-bars',
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
|
||||
import { CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -14,14 +14,7 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
type: 'NOTE_VISIBLE_PANES_TOGGLE',
|
||||
});
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return props.settingEditorCodeView && props.selectedNoteIds.length === 1;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
settingEditorCodeView: state.settings['editor.codeView'],
|
||||
};
|
||||
},
|
||||
|
||||
enabledCondition: 'markdownEditorVisible && oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ import { Module } from 'lib/services/interop/types';
|
||||
import InteropServiceHelper from '../InteropServiceHelper';
|
||||
import { _ } from 'lib/locale';
|
||||
import { MenuItem, MenuItemLocation } from 'lib/services/plugins/api/types';
|
||||
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
@ -108,7 +109,7 @@ const commandNames:string[] = [
|
||||
'attachFile',
|
||||
'focusSearch',
|
||||
'showLocalSearch',
|
||||
'toggleSidebar',
|
||||
'toggleSideBar',
|
||||
'toggleNoteList',
|
||||
'toggleVisiblePanes',
|
||||
'toggleExternalEditing',
|
||||
@ -157,7 +158,7 @@ function useMenu(props:Props) {
|
||||
|
||||
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 = {
|
||||
path,
|
||||
@ -299,12 +300,12 @@ function useMenu(props:Props) {
|
||||
templateItems.push({
|
||||
label: _('Create note from template'),
|
||||
click: () => {
|
||||
CommandService.instance().execute('selectTemplate', { noteType: 'note' });
|
||||
CommandService.instance().execute('selectTemplate', 'note');
|
||||
},
|
||||
}, {
|
||||
label: _('Create to-do from template'),
|
||||
click: () => {
|
||||
CommandService.instance().execute('selectTemplate', { noteType: 'todo' });
|
||||
CommandService.instance().execute('selectTemplate', 'todo');
|
||||
},
|
||||
}, {
|
||||
label: _('Insert template'),
|
||||
@ -532,7 +533,7 @@ function useMenu(props:Props) {
|
||||
view: {
|
||||
label: _('&View'),
|
||||
submenu: [
|
||||
menuItemDic.toggleSidebar,
|
||||
menuItemDic.toggleSideBar,
|
||||
menuItemDic.toggleNoteList,
|
||||
menuItemDic.toggleVisiblePanes,
|
||||
{
|
||||
@ -550,7 +551,6 @@ function useMenu(props:Props) {
|
||||
id: 'showNoteCounts',
|
||||
label: Setting.settingMetadata('showNoteCounts').label(),
|
||||
type: 'checkbox',
|
||||
// checked: Setting.value('showNoteCounts'),
|
||||
click: () => {
|
||||
Setting.setValue('showNoteCounts', !Setting.value('showNoteCounts'));
|
||||
},
|
||||
@ -558,7 +558,6 @@ function useMenu(props:Props) {
|
||||
id: 'uncompletedTodosOnTop',
|
||||
label: Setting.settingMetadata('uncompletedTodosOnTop').label(),
|
||||
type: 'checkbox',
|
||||
// checked: Setting.value('uncompletedTodosOnTop'),
|
||||
click: () => {
|
||||
Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
|
||||
},
|
||||
@ -566,7 +565,6 @@ function useMenu(props:Props) {
|
||||
id: 'showCompletedTodos',
|
||||
label: Setting.settingMetadata('showCompletedTodos').label(),
|
||||
type: 'checkbox',
|
||||
// checked: Setting.value('showCompletedTodos'),
|
||||
click: () => {
|
||||
Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
|
||||
},
|
||||
@ -786,9 +784,13 @@ function useMenu(props:Props) {
|
||||
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime]);
|
||||
|
||||
useEffect(() => {
|
||||
const whenClauseContext = CommandService.instance().currentWhenClauseContext();
|
||||
|
||||
for (const commandName in props.menuItemProps) {
|
||||
if (!props.menuItemProps[commandName]) continue;
|
||||
menuItemSetEnabled(commandName, CommandService.instance().isEnabled(commandName, props.menuItemProps[commandName]));
|
||||
const p = 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');
|
||||
@ -858,8 +860,10 @@ function MenuBar(props:Props):JSX.Element {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state:AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
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,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
layoutButtonSequence: state.settings.layoutButtonSequence,
|
||||
|
@ -46,8 +46,6 @@ function formatReadTime(readTimeMinutes: number) {
|
||||
}
|
||||
|
||||
export default function NoteContentPropertiesDialog(props:NoteContentPropertiesDialogProps) {
|
||||
|
||||
console.info('MMMMMMMMMMMM', props.markupLanguage);
|
||||
const theme = themeStyle(props.themeId);
|
||||
const tableBodyComps: JSX.Element[] = [];
|
||||
// For the source Markdown
|
||||
|
@ -5,6 +5,7 @@ import { utils as pluginUtils } from 'lib/services/plugins/reducer';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../../app';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from 'lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
interface ToolbarProps {
|
||||
@ -31,6 +32,8 @@ function Toolbar(props:ToolbarProps) {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
const commandNames = [
|
||||
'historyBackward',
|
||||
'historyForward',
|
||||
@ -53,7 +56,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'editorToolbar'));
|
||||
|
||||
return {
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(state, commandNames),
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(commandNames, whenClauseContext),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -25,6 +25,7 @@ import eventManager from 'lib/eventManager';
|
||||
import { AppState } from '../../app';
|
||||
import ToolbarButtonUtils from 'lib/services/commands/ToolbarButtonUtils';
|
||||
import { _ } from 'lib/locale';
|
||||
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
|
||||
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const { substrWithEllipsis } = require('lib/string-utils');
|
||||
@ -247,9 +248,9 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) {
|
||||
CommandService.instance().execute('focusElement', { target: 'noteList' });
|
||||
CommandService.instance().execute('focusElement', 'noteList');
|
||||
} else {
|
||||
CommandService.instance().execute('focusElement', { target: 'noteBody' });
|
||||
CommandService.instance().execute('focusElement', 'noteBody');
|
||||
}
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
@ -364,7 +365,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
function renderTagBar() {
|
||||
const theme = themeStyle(props.themeId);
|
||||
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;
|
||||
|
||||
return (
|
||||
@ -565,6 +566,7 @@ export {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null;
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
return {
|
||||
noteId: noteId,
|
||||
@ -587,15 +589,15 @@ const mapStateToProps = (state: AppState) => {
|
||||
watchedResources: state.watchedResources,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(state, [
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'historyBackward',
|
||||
'historyForward',
|
||||
'toggleEditors',
|
||||
'toggleExternalEditing',
|
||||
]),
|
||||
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons(state, [
|
||||
], whenClauseContext),
|
||||
setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'setTags',
|
||||
])[0],
|
||||
], whenClauseContext)[0],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -12,13 +12,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
execute: async () => {
|
||||
comp.editorRef.current.execCommand({ name: 'focus' });
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return props.hasOneNoteSelected;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
hasOneNoteSelected: state.selectedNoteIds.length === 1,
|
||||
};
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -13,14 +13,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
if (!comp.titleInputRef.current) return;
|
||||
comp.titleInputRef.current.focus();
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return props.hasOneNoteSelected;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
hasOneNoteSelected: state.selectedNoteIds.length === 1,
|
||||
};
|
||||
},
|
||||
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
@ -16,11 +16,6 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
if (comp.noteSearchBarRef.current) comp.noteSearchBarRef.current.wrappedInstance.focus();
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
|
||||
},
|
||||
enabledCondition: 'oneNoteSelected',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'showRevisions',
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { FormNote, ScrollOptionTypes } from './types';
|
||||
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 BaseModel = require('lib/BaseModel');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { MarkupToHtml } = require('lib/joplin-renderer');
|
||||
|
||||
const commandsWithDependencies = [
|
||||
require('../commands/showLocalSearch'),
|
||||
@ -25,49 +23,30 @@ interface HookDependencies {
|
||||
|
||||
function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):CommandRuntime {
|
||||
return {
|
||||
execute: async (props:any) => {
|
||||
// console.info('Running editor command:', declaration.name, props);
|
||||
execute: async (_context:CommandContext, ...args:any[]) => {
|
||||
if (!editorRef.current.execCommand) {
|
||||
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 {
|
||||
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: props.hash,
|
||||
});
|
||||
} else {
|
||||
return editorRef.current.execCommand({
|
||||
name: declaration.name,
|
||||
value: props.value,
|
||||
});
|
||||
}
|
||||
return editorRef.current.execCommand({
|
||||
name: declaration.name,
|
||||
value: args[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
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,
|
||||
};
|
||||
},
|
||||
enabledCondition: '!modalDialogVisible && markdownEditorPaneVisible && oneNoteSelected && noteIsMarkdown',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ const StyledRoot = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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 {
|
||||
@ -386,9 +386,9 @@ class NoteListComponent extends React.Component {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) {
|
||||
CommandService.instance().execute('focusElement', { target: 'sideBar' });
|
||||
CommandService.instance().execute('focusElement', 'sideBar');
|
||||
} else {
|
||||
CommandService.instance().execute('focusElement', { target: 'noteTitle' });
|
||||
CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'focusElementNoteList',
|
||||
@ -9,19 +10,14 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ noteId }:any) => {
|
||||
execute: async (context:CommandContext, noteId:string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
if (noteId) {
|
||||
const ref = comp.itemAnchorRef(noteId);
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any):boolean => {
|
||||
return !!props.noteId;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
};
|
||||
},
|
||||
enabledCondition: 'noteListHasNotes',
|
||||
};
|
||||
};
|
||||
|
@ -52,13 +52,13 @@ export default function NoteListControls(props:Props) {
|
||||
return (
|
||||
<ButtonContainer>
|
||||
<StyledButton
|
||||
tooltip={CommandService.instance().title('newTodo', {})}
|
||||
tooltip={CommandService.instance().label('newTodo')}
|
||||
iconName="far fa-check-square"
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={onNewTodoButtonClick}
|
||||
/>
|
||||
<StyledButton
|
||||
tooltip={CommandService.instance().title('newNote', {})}
|
||||
tooltip={CommandService.instance().label('newNote')}
|
||||
iconName="icon-note"
|
||||
level={ButtonLevel.Primary}
|
||||
onClick={onNewNoteButtonClick}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
|
@ -3,6 +3,7 @@ import CommandService from 'lib/services/CommandService';
|
||||
import ToolbarBase from '../ToolbarBase';
|
||||
import { utils as pluginUtils } from 'lib/services/plugins/reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from 'lib/services/commands/ToolbarButtonUtils';
|
||||
import stateToWhenClauseContext from 'lib/services/commands/stateToWhenClauseContext';
|
||||
const { connect } = require('react-redux');
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
@ -32,12 +33,14 @@ function NoteToolbar(props:NoteToolbarProps) {
|
||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
|
||||
const mapStateToProps = (state:any) => {
|
||||
const whenClauseContext = stateToWhenClauseContext(state);
|
||||
|
||||
return {
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons(state, [
|
||||
toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([
|
||||
'editAlarm',
|
||||
'toggleVisiblePanes',
|
||||
'showNoteProperties',
|
||||
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar'))),
|
||||
].concat(pluginUtils.commandNamesFromViews(state.pluginService.plugins, 'noteToolbar')), whenClauseContext),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ const debounce = require('debounce');
|
||||
export default function useSearch(query:string) {
|
||||
useEffect(() => {
|
||||
const search = debounce((query:string) => {
|
||||
CommandService.instance().execute('search', { query });
|
||||
CommandService.instance().execute('search', query);
|
||||
}, 500);
|
||||
|
||||
search(query);
|
||||
|
@ -230,7 +230,7 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||
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) {
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('renameFolder', { folderId: itemId })));
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem('renameFolder', itemId)));
|
||||
|
||||
menu.append(new MenuItem({ type: 'separator' }));
|
||||
|
||||
@ -290,7 +290,7 @@ class SideBarComponent extends React.Component<Props, State> {
|
||||
|
||||
if (itemType === BaseModel.TYPE_TAG) {
|
||||
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();
|
||||
|
||||
if (event.shiftKey) {
|
||||
CommandService.instance().execute('focusElement', { target: 'noteBody' });
|
||||
CommandService.instance().execute('focusElement', 'noteBody');
|
||||
} 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}
|
||||
title={label}
|
||||
onClick={() => {
|
||||
CommandService.instance().execute('synchronize', { syncStarted: type !== 'sync' });
|
||||
CommandService.instance().execute('synchronize', type !== 'sync');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
|
||||
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
import { DesktopCommandContext } from 'ElectronClient/services/commands/types';
|
||||
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'focusElementSideBar',
|
||||
@ -9,8 +10,10 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ sidebarVisibility }:any) => {
|
||||
if (sidebarVisibility) {
|
||||
execute: async (context:DesktopCommandContext) => {
|
||||
const sideBarVisible = !!context.state.sidebarVisibility;
|
||||
|
||||
if (sideBarVisible) {
|
||||
const item = comp.selectedItem();
|
||||
if (item) {
|
||||
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;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
sidebarVisibility: state.sidebarVisibility,
|
||||
};
|
||||
},
|
||||
|
||||
enabledCondition: 'sideBarVisible',
|
||||
};
|
||||
};
|
||||
|
@ -40,11 +40,11 @@ export default class NoteListUtils {
|
||||
|
||||
if (!hasEncrypted) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', { noteIds }))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', noteIds))
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', { noteIds }))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', noteIds))
|
||||
);
|
||||
|
||||
menu.append(
|
||||
@ -63,7 +63,7 @@ export default class NoteListUtils {
|
||||
|
||||
if (singleNoteId) {
|
||||
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) {
|
||||
@ -132,7 +132,7 @@ export default class NoteListUtils {
|
||||
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', { noteIds: noteIds.slice() })
|
||||
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())
|
||||
)
|
||||
);
|
||||
|
||||
@ -157,7 +157,7 @@ export default class NoteListUtils {
|
||||
|
||||
exportMenu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('exportPdf', { noteIds: noteIds })
|
||||
menuUtils.commandToStatefulMenuItem('exportPdf', noteIds)
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -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 { _ } = require('lib/locale');
|
||||
const { themeStyle } = require('lib/theme');
|
||||
const CommandService = require('lib/services/CommandService').default;
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Tag = require('lib/models/Tag');
|
||||
@ -12,32 +16,73 @@ const { ItemList } = require('../gui/ItemList.min');
|
||||
const HelpButton = require('../gui/HelpButton.min');
|
||||
const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('lib/string-utils.js');
|
||||
const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js');
|
||||
const PLUGIN_NAME = 'gotoAnything';
|
||||
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 {
|
||||
|
||||
onTrigger() {
|
||||
public dispatch:Function;
|
||||
public static Dialog:any;
|
||||
public static manifest:any;
|
||||
|
||||
onTrigger(event:any) {
|
||||
this.dispatch({
|
||||
type: 'PLUGINLEGACY_DIALOG_SET',
|
||||
open: true,
|
||||
pluginName: PLUGIN_NAME,
|
||||
userData: event.userData,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Dialog extends React.PureComponent {
|
||||
class Dialog extends React.PureComponent<Props, State> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
private fuzzy_:boolean;
|
||||
private styles_:any;
|
||||
private inputRef:any;
|
||||
private itemListRef:any;
|
||||
private listUpdateIID_:any;
|
||||
private markupToHtml_:any;
|
||||
|
||||
constructor(props:Props) {
|
||||
super(props);
|
||||
|
||||
this.fuzzy_ = false;
|
||||
|
||||
const startString = props?.userData?.startString ? props?.userData?.startString : '';
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
query: startString,
|
||||
results: [],
|
||||
selectedItemId: null,
|
||||
keywords: [],
|
||||
@ -55,19 +100,25 @@ class Dialog extends React.PureComponent {
|
||||
this.input_onChange = this.input_onChange.bind(this);
|
||||
this.input_onKeyDown = this.input_onKeyDown.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.helpButton_onClick = this.helpButton_onClick.bind(this);
|
||||
|
||||
if (startString) this.scheduleListUpdate();
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
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] = {
|
||||
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
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
@ -148,7 +199,7 @@ class Dialog extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
modalLayer_onClick(event) {
|
||||
modalLayer_onClick(event:any) {
|
||||
if (event.currentTarget == event.target) {
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
@ -162,7 +213,7 @@ class Dialog extends React.PureComponent {
|
||||
this.setState({ showHelp: !this.state.showHelp });
|
||||
}
|
||||
|
||||
input_onChange(event) {
|
||||
input_onChange(event:any) {
|
||||
this.setState({ query: event.target.value });
|
||||
|
||||
this.scheduleListUpdate();
|
||||
@ -177,7 +228,7 @@ class Dialog extends React.PureComponent {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
makeSearchQuery(query) {
|
||||
makeSearchQuery(query:string) {
|
||||
const output = [];
|
||||
const splitted = query.split(' ');
|
||||
|
||||
@ -190,7 +241,7 @@ class Dialog extends React.PureComponent {
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
async keywords(searchQuery) {
|
||||
async keywords(searchQuery:string) {
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, this.fuzzy_);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
@ -207,11 +258,28 @@ class Dialog extends React.PureComponent {
|
||||
if (!this.state.query) {
|
||||
this.setState({ results: [], keywords: [] });
|
||||
} else {
|
||||
let results = [];
|
||||
let results:SearchResult[] = [];
|
||||
let listType = null;
|
||||
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;
|
||||
searchQuery = `*${this.state.query.split(' ')[0].substr(1).trim()}*`;
|
||||
results = await Tag.searchAllWithNotes({ titlePattern: searchQuery });
|
||||
@ -230,7 +298,7 @@ class Dialog extends React.PureComponent {
|
||||
searchQuery = this.makeSearchQuery(this.state.query);
|
||||
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) {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
@ -241,7 +309,9 @@ class Dialog extends React.PureComponent {
|
||||
} else {
|
||||
const limit = 20;
|
||||
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), {});
|
||||
|
||||
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'
|
||||
// will result in 'Joplin is a free, open source note taking application'
|
||||
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
|
||||
if (mergedIndices.length && mergedIndices[mergedIndices.length - 1][1] !== body.length) fragments += ' ...';
|
||||
|
||||
@ -285,7 +355,7 @@ class Dialog extends React.PureComponent {
|
||||
}
|
||||
|
||||
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({
|
||||
listType: listType,
|
||||
results: results,
|
||||
keywords: await this.keywords(searchQuery),
|
||||
keywords: keywords ? keywords : await this.keywords(searchQuery),
|
||||
selectedItemId: results.length === 0 ? null : results[0].id,
|
||||
resultsInBody: resultsInBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async gotoItem(item) {
|
||||
async gotoItem(item:any) {
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
type: 'PLUGINLEGACY_DIALOG_SET',
|
||||
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) {
|
||||
const folderPath = await Folder.folderPath(this.props.folders, item.parent_id);
|
||||
|
||||
@ -329,7 +404,7 @@ class Dialog extends React.PureComponent {
|
||||
noteId: item.id,
|
||||
});
|
||||
|
||||
CommandService.instance().scheduleExecute('focusElement', { target: 'noteBody' });
|
||||
CommandService.instance().scheduleExecute('focusElement', 'noteBody');
|
||||
} else if (this.state.listType === BaseModel.TYPE_TAG) {
|
||||
this.props.dispatch({
|
||||
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 parentId = event.currentTarget.getAttribute('data-parent-id');
|
||||
const itemType = event.currentTarget.getAttribute('data-type');
|
||||
|
||||
this.gotoItem({
|
||||
id: itemId,
|
||||
parent_id: parentId,
|
||||
type: itemType,
|
||||
});
|
||||
}
|
||||
|
||||
listItemRenderer(item) {
|
||||
renderItem(item:SearchResult) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = this.style();
|
||||
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>;
|
||||
|
||||
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>
|
||||
{fragmentComp}
|
||||
{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 itemId === 'undefined') itemId = this.state.selectedItemId;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
@ -392,7 +469,7 @@ class Dialog extends React.PureComponent {
|
||||
return this.state.results[index];
|
||||
}
|
||||
|
||||
input_onKeyDown(event) {
|
||||
input_onKeyDown(event:any) {
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) { // DOWN / UP
|
||||
@ -428,7 +505,7 @@ class Dialog extends React.PureComponent {
|
||||
|
||||
const itemListStyle = {
|
||||
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 (
|
||||
@ -437,7 +514,7 @@ class Dialog extends React.PureComponent {
|
||||
itemHeight={style.itemHeight}
|
||||
items={this.state.results}
|
||||
style={itemListStyle}
|
||||
itemRenderer={this.listItemRenderer}
|
||||
itemRenderer={this.renderItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -445,7 +522,7 @@ class Dialog extends React.PureComponent {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
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 (
|
||||
<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 {
|
||||
folders: state.folders,
|
||||
themeId: state.settings.theme,
|
||||
@ -485,8 +562,18 @@ GotoAnything.manifest = {
|
||||
accelerator: () => KeymapService.instance().getAccelerator('gotoAnything'),
|
||||
screens: ['Main'],
|
||||
},
|
||||
{
|
||||
name: 'main',
|
||||
parent: 'tools',
|
||||
label: _('Command palette'),
|
||||
accelerator: () => KeymapService.instance().getAccelerator('commandPalette'),
|
||||
screens: ['Main'],
|
||||
userData: {
|
||||
startString: ':',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
module.exports = GotoAnything;
|
||||
export default GotoAnything;
|
5
ElectronClient/services/commands/types.ts
Normal file
5
ElectronClient/services/commands/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { AppState } from '../../app';
|
||||
|
||||
export interface DesktopCommandContext {
|
||||
state: AppState,
|
||||
}
|
@ -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++) {
|
||||
const e = BaseModel.typeEnum_[i];
|
||||
|
@ -4,27 +4,16 @@ import { _ } from 'lib/locale';
|
||||
export const declaration:CommandDeclaration = {
|
||||
name: 'historyBackward',
|
||||
label: () => _('Back'),
|
||||
// iconName: 'fa-arrow-left',
|
||||
iconName: 'icon-back',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
hasBackwardNotes: boolean,
|
||||
}
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
if (!props.hasBackwardNotes) return;
|
||||
execute: async () => {
|
||||
utils.store.dispatch({
|
||||
type: 'HISTORY_BACKWARD',
|
||||
});
|
||||
},
|
||||
isEnabled: (props:Props) => {
|
||||
return props.hasBackwardNotes;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { hasBackwardNotes: state.backwardHistoryNotes.length > 0 };
|
||||
},
|
||||
enabledCondition: 'historyhasBackwardNotes',
|
||||
};
|
||||
};
|
||||
|
@ -7,23 +7,13 @@ export const declaration:CommandDeclaration = {
|
||||
iconName: 'icon-forward',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
hasForwardNotes: boolean,
|
||||
}
|
||||
|
||||
export const runtime = ():CommandRuntime => {
|
||||
return {
|
||||
execute: async (props:Props) => {
|
||||
if (!props.hasForwardNotes) return;
|
||||
execute: async () => {
|
||||
utils.store.dispatch({
|
||||
type: 'HISTORY_FORWARD',
|
||||
});
|
||||
},
|
||||
isEnabled: (props:Props) => {
|
||||
return props.hasForwardNotes;
|
||||
},
|
||||
mapStateToProps: (state:any) => {
|
||||
return { hasForwardNotes: state.forwardHistoryNotes.length > 0 };
|
||||
},
|
||||
enabledCondition: 'historyhasForwardNotes',
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService';
|
||||
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
|
||||
import { _ } from 'lib/locale';
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
@ -8,9 +8,11 @@ export const declaration:CommandDeclaration = {
|
||||
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 => {
|
||||
return {
|
||||
execute: async ({ syncStarted }:any) => {
|
||||
execute: async (_context:CommandContext, syncStarted:boolean = false) => {
|
||||
const action = syncStarted ? 'cancel' : 'start';
|
||||
|
||||
if (!(await reg.syncTarget().isAuthenticated())) {
|
||||
@ -43,13 +45,5 @@ export const runtime = ():CommandRuntime => {
|
||||
return 'sync';
|
||||
}
|
||||
},
|
||||
isEnabled: (props:any) => {
|
||||
return !props.syncStarted;
|
||||
},
|
||||
mapStateToProps: (state:any):any => {
|
||||
return {
|
||||
syncStarted: state.syncStarted,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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';
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
|
||||
const { createSelectorCreator, defaultMemoize } = require('reselect');
|
||||
@ -160,8 +161,6 @@ for (const additionalReducer of additionalReducers) {
|
||||
|
||||
export const MAX_HISTORY = 200;
|
||||
|
||||
export const stateUtils:any = {};
|
||||
|
||||
const derivedStateCache_:any = {};
|
||||
|
||||
// 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
|
||||
// if its content hasn't changed.
|
||||
stateUtils.selectArrayShallow = createCachedSelector(
|
||||
const selectArrayShallow = createCachedSelector(
|
||||
(state:any) => state.array,
|
||||
(array:any[]) => array
|
||||
)({
|
||||
@ -197,85 +194,85 @@ stateUtils.selectArrayShallow = createCachedSelector(
|
||||
selectorCreator: createShallowArrayEqualSelector,
|
||||
});
|
||||
|
||||
stateUtils.hasOneSelectedNote = function(state:State):boolean {
|
||||
return state.selectedNoteIds.length === 1;
|
||||
};
|
||||
class StateUtils {
|
||||
|
||||
stateUtils.notesOrder = function(stateSettings:any) {
|
||||
if (stateSettings['notes.sortOrder.field'] === 'order') {
|
||||
return cacheEnabledOutput('notesOrder', [
|
||||
{
|
||||
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',
|
||||
},
|
||||
]);
|
||||
// Given an input array, this selector ensures that the same array is returned
|
||||
// if its content hasn't changed.
|
||||
public selectArrayShallow(props:any, cacheKey:any) {
|
||||
return selectArrayShallow(props, cacheKey);
|
||||
}
|
||||
};
|
||||
|
||||
stateUtils.foldersOrder = function(stateSettings:any) {
|
||||
return cacheEnabledOutput('foldersOrder', [
|
||||
{
|
||||
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;
|
||||
public oneNoteSelected(state:State):boolean {
|
||||
return state.selectedNoteIds.length === 1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
stateUtils.parentItem = function(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 };
|
||||
};
|
||||
|
||||
stateUtils.lastSelectedNoteIds = function(state:State):string[] {
|
||||
const parent = stateUtils.parentItem(state);
|
||||
if (!parent) return [];
|
||||
const output = (state.lastSelectedNotesIds as any)[parent.type][parent.id];
|
||||
return output ? output : [];
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
public notesOrder(stateSettings:any) {
|
||||
if (stateSettings['notes.sortOrder.field'] === 'order') {
|
||||
return cacheEnabledOutput('notesOrder', [
|
||||
{
|
||||
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',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
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[]) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
@ -526,8 +523,29 @@ const getContextFromHistory = (ctx:any) => {
|
||||
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) {
|
||||
const currentNote = stateUtils.getCurrentNote(draft);
|
||||
const currentNote = getNoteHistoryInfo(draft);
|
||||
switch (action.type) {
|
||||
case 'HISTORY_BACKWARD': {
|
||||
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 newPlugin = draft.pluginsLegacy[action.pluginName] ? Object.assign({}, draft.pluginsLegacy[action.pluginName]) : {};
|
||||
if ('open' in action) newPlugin.dialogOpen = action.open;
|
||||
if ('userData' in action) newPlugin.userData = action.userData;
|
||||
newPluginsLegacy[action.pluginName] = newPlugin;
|
||||
draft.pluginsLegacy = newPluginsLegacy;
|
||||
}
|
||||
|
@ -1,30 +1,23 @@
|
||||
import { State } from 'lib/reducer';
|
||||
import eventManager from 'lib/eventManager';
|
||||
import markdownUtils, { MarkdownTableHeader, MarkdownTableRow } from 'lib/markdownUtils';
|
||||
import BaseService from 'lib/services/BaseService';
|
||||
import shim from 'lib/shim';
|
||||
import WhenClause from './WhenClause';
|
||||
import stateToWhenClauseContext from './commands/stateToWhenClauseContext';
|
||||
|
||||
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 {
|
||||
execute(props:any):Promise<any>
|
||||
isEnabled?(props:any):boolean
|
||||
|
||||
// "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
|
||||
|
||||
execute(context:CommandContext, ...args:any[]):Promise<any>
|
||||
enabledCondition?: EnabledCondition;
|
||||
// Used for the (optional) toolbar button title
|
||||
title?(props:any):string,
|
||||
mapStateToTitle?(state:any):string,
|
||||
}
|
||||
|
||||
export interface CommandDeclaration {
|
||||
@ -33,6 +26,9 @@ export interface CommandDeclaration {
|
||||
// Used for the menu item label, and toolbar button tooltip
|
||||
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,
|
||||
// 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
|
||||
@ -88,30 +84,29 @@ interface CommandByNameOptions {
|
||||
runtimeMustBeRegistered?:boolean,
|
||||
}
|
||||
|
||||
interface CommandState {
|
||||
export interface SearchResult {
|
||||
commandName: string,
|
||||
title: string,
|
||||
enabled: boolean,
|
||||
}
|
||||
|
||||
interface CommandStates {
|
||||
[key:string]: CommandState
|
||||
}
|
||||
|
||||
export default class CommandService extends BaseService {
|
||||
|
||||
private static instance_:CommandService;
|
||||
|
||||
static instance():CommandService {
|
||||
public static instance():CommandService {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new CommandService();
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
private commands_:Commands = {};
|
||||
private commandPreviousStates_:CommandStates = {};
|
||||
private store_:any;
|
||||
private devMode_:boolean;
|
||||
|
||||
initialize(store:any) {
|
||||
public initialize(store:any, devMode:boolean) {
|
||||
utils.store = store;
|
||||
this.store_ = store;
|
||||
this.devMode_ = devMode;
|
||||
}
|
||||
|
||||
public on(eventName:string, callback:Function) {
|
||||
@ -122,6 +117,36 @@ export default class CommandService extends BaseService {
|
||||
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 {
|
||||
options = {
|
||||
mustExist: true,
|
||||
@ -140,7 +165,7 @@ export default class CommandService extends BaseService {
|
||||
return command;
|
||||
}
|
||||
|
||||
registerDeclaration(declaration:CommandDeclaration) {
|
||||
public registerDeclaration(declaration:CommandDeclaration) {
|
||||
declaration = { ...declaration };
|
||||
if (!declaration.label) declaration.label = '';
|
||||
if (!declaration.iconName) declaration.iconName = '';
|
||||
@ -148,83 +173,89 @@ export default class CommandService extends BaseService {
|
||||
this.commands_[declaration.name] = {
|
||||
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)}`);
|
||||
|
||||
const command = this.commandByName(commandName);
|
||||
|
||||
runtime = Object.assign({}, runtime);
|
||||
if (!runtime.isEnabled) runtime.isEnabled = () => true;
|
||||
if (!runtime.title) runtime.title = () => null;
|
||||
if (!runtime.enabledCondition) runtime.enabledCondition = 'true';
|
||||
command.runtime = runtime;
|
||||
|
||||
delete this.commandPreviousStates_[commandName];
|
||||
}
|
||||
|
||||
componentRegisterCommands(component:any, commands:any[]) {
|
||||
public componentRegisterCommands(component:any, commands:any[]) {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component));
|
||||
}
|
||||
}
|
||||
|
||||
componentUnregisterCommands(commands:any[]) {
|
||||
public componentUnregisterCommands(commands:any[]) {
|
||||
for (const command of commands) {
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
}
|
||||
|
||||
unregisterRuntime(commandName:string) {
|
||||
public unregisterRuntime(commandName:string) {
|
||||
const command = this.commandByName(commandName, { mustExist: false });
|
||||
if (!command || !command.runtime) return;
|
||||
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);
|
||||
this.logger().info('CommandService::execute:', commandName, props);
|
||||
return command.runtime.execute(props ? props : {});
|
||||
this.logger().info('CommandService::execute:', commandName, args);
|
||||
return command.runtime.execute({ state: this.store_.getState() }, ...args);
|
||||
}
|
||||
|
||||
scheduleExecute(commandName:string, args:any) {
|
||||
public scheduleExecute(commandName:string, args:any) {
|
||||
shim.setTimeout(() => {
|
||||
this.execute(commandName, args);
|
||||
}, 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);
|
||||
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 {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command.runtime) return null;
|
||||
if (!command.runtime.mapStateToProps) return {};
|
||||
return command.runtime.mapStateToProps(state);
|
||||
}
|
||||
|
||||
title(commandName:string, props:any):string {
|
||||
// The title is dynamic and derived from the state, which is why the state is passed
|
||||
// as an argument. Title can be used for example to display the alarm date on the
|
||||
// "set alarm" toolbar button.
|
||||
public title(commandName:string, state:any = null):string {
|
||||
const command = this.commandByName(commandName);
|
||||
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);
|
||||
if (!command) throw new Error(`No such command: ${commandName}`);
|
||||
if (variant === 'tinymce') return command.declaration.tinymceIconName ? command.declaration.tinymceIconName : 'preferences';
|
||||
return command.declaration.iconName;
|
||||
}
|
||||
|
||||
label(commandName:string, fullLabel:boolean = false):string {
|
||||
public label(commandName:string, fullLabel:boolean = false):string {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command) throw new Error(`Command: ${commandName} is not declared`);
|
||||
const output = [];
|
||||
@ -240,42 +271,15 @@ export default class CommandService extends BaseService {
|
||||
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 });
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const defaultKeymapItems = {
|
||||
{ accelerator: 'Cmd+N', command: 'newNote' },
|
||||
{ accelerator: 'Cmd+T', command: 'newTodo' },
|
||||
{ accelerator: 'Cmd+S', command: 'synchronize' },
|
||||
{ accelerator: 'Cmd+P', command: 'print' },
|
||||
{ accelerator: '', command: 'print' },
|
||||
{ accelerator: 'Cmd+H', command: 'hideApp' },
|
||||
{ accelerator: 'Cmd+Q', command: 'quit' },
|
||||
{ accelerator: 'Cmd+,', command: 'config' },
|
||||
@ -37,20 +37,21 @@ const defaultKeymapItems = {
|
||||
{ accelerator: 'Shift+Cmd+L', command: 'focusElementNoteList' },
|
||||
{ accelerator: 'Shift+Cmd+N', command: 'focusElementNoteTitle' },
|
||||
{ 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: 'Cmd+L', command: 'toggleVisiblePanes' },
|
||||
{ accelerator: 'Cmd+0', command: 'zoomActualSize' },
|
||||
{ accelerator: 'Cmd+E', command: 'toggleExternalEditing' },
|
||||
{ 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' },
|
||||
],
|
||||
default: [
|
||||
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
||||
{ accelerator: 'Ctrl+T', command: 'newTodo' },
|
||||
{ accelerator: 'Ctrl+S', command: 'synchronize' },
|
||||
{ accelerator: 'Ctrl+P', command: 'print' },
|
||||
{ accelerator: '', command: 'print' },
|
||||
{ accelerator: 'Ctrl+Q', command: 'quit' },
|
||||
{ accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' },
|
||||
{ accelerator: 'Ctrl+C', command: 'textCopy' },
|
||||
@ -68,14 +69,15 @@ const defaultKeymapItems = {
|
||||
{ accelerator: 'Ctrl+Shift+L', command: 'focusElementNoteList' },
|
||||
{ accelerator: 'Ctrl+Shift+N', command: 'focusElementNoteTitle' },
|
||||
{ accelerator: 'Ctrl+Shift+B', command: 'focusElementNoteBody' },
|
||||
{ accelerator: 'F10', command: 'toggleSidebar' },
|
||||
{ accelerator: 'F10', command: 'toggleSideBar' },
|
||||
{ accelerator: 'F11', command: 'toggleNoteList' },
|
||||
{ accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' },
|
||||
{ accelerator: 'Ctrl+0', command: 'zoomActualSize' },
|
||||
{ accelerator: 'Ctrl+E', command: 'toggleExternalEditing' },
|
||||
{ accelerator: 'Ctrl+Alt+T', command: 'setTags' },
|
||||
{ accelerator: 'Ctrl+,', command: 'config' },
|
||||
{ accelerator: 'Ctrl+G', command: 'gotoAnything' },
|
||||
{ accelerator: 'Ctrl+P', command: 'gotoAnything' },
|
||||
{ accelerator: 'Ctrl+Shift+P', command: 'commandPalette' },
|
||||
{ accelerator: 'F1', command: 'help' },
|
||||
],
|
||||
};
|
||||
@ -90,13 +92,14 @@ interface Keymap {
|
||||
}
|
||||
|
||||
export default class KeymapService extends BaseService {
|
||||
|
||||
private keymap: Keymap;
|
||||
private platform: string;
|
||||
private customKeymapPath: string;
|
||||
private defaultKeymapItems: KeymapItem[];
|
||||
private lastSaveTime_:number;
|
||||
|
||||
constructor() {
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.lastSaveTime_ = Date.now();
|
||||
@ -106,11 +109,11 @@ export default class KeymapService extends BaseService {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
get lastSaveTime():number {
|
||||
public get lastSaveTime():number {
|
||||
return this.lastSaveTime_;
|
||||
}
|
||||
|
||||
initialize(platform: string = shim.platformName()) {
|
||||
public initialize(platform: string = shim.platformName()) {
|
||||
this.platform = 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
|
||||
|
||||
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}`);
|
||||
|
||||
try {
|
||||
@ -161,7 +164,7 @@ export default class KeymapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
acceleratorExists(command: string) {
|
||||
public acceleratorExists(command: string) {
|
||||
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;
|
||||
}
|
||||
|
||||
getAccelerator(command: string) {
|
||||
public getAccelerator(command: string) {
|
||||
const item = this.keymap[command];
|
||||
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
|
||||
return item.accelerator;
|
||||
}
|
||||
|
||||
getDefaultAccelerator(command: string) {
|
||||
public getDefaultAccelerator(command: string) {
|
||||
const defaultItem = this.defaultKeymapItems.find((item => item.command === command));
|
||||
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
|
||||
return defaultItem.accelerator;
|
||||
}
|
||||
|
||||
getCommandNames() {
|
||||
public getCommandNames() {
|
||||
return Object.keys(this.keymap);
|
||||
}
|
||||
|
||||
getKeymapItems() {
|
||||
public getKeymapItems() {
|
||||
return Object.values(this.keymap);
|
||||
}
|
||||
|
||||
getCustomKeymapItems() {
|
||||
public getCustomKeymapItems() {
|
||||
const customkeymapItems: KeymapItem[] = [];
|
||||
this.defaultKeymapItems.forEach(({ command, accelerator }) => {
|
||||
const currentAccelerator = this.getAccelerator(command);
|
||||
@ -236,11 +239,11 @@ export default class KeymapService extends BaseService {
|
||||
return customkeymapItems;
|
||||
}
|
||||
|
||||
getDefaultKeymapItems() {
|
||||
public getDefaultKeymapItems() {
|
||||
return [...this.defaultKeymapItems];
|
||||
}
|
||||
|
||||
overrideKeymap(customKeymapItems: KeymapItem[]) {
|
||||
public overrideKeymap(customKeymapItems: KeymapItem[]) {
|
||||
try {
|
||||
for (let i = 0; i < customKeymapItems.length; 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();
|
||||
|
||||
// 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;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) {
|
||||
public domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) {
|
||||
const parts = [];
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = event;
|
||||
|
||||
@ -358,7 +361,7 @@ export default class KeymapService extends BaseService {
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
static domToElectronKey(domKey: string) {
|
||||
private static domToElectronKey(domKey: string) {
|
||||
let electronKey;
|
||||
|
||||
if (/^([a-z])$/.test(domKey)) {
|
||||
@ -398,7 +401,7 @@ export default class KeymapService extends BaseService {
|
||||
|
||||
private static instance_:KeymapService = null;
|
||||
|
||||
static instance():KeymapService {
|
||||
public static instance():KeymapService {
|
||||
if (this.instance_) return this.instance_;
|
||||
|
||||
this.instance_ = new KeymapService();
|
||||
|
@ -1,5 +1,4 @@
|
||||
const Logger = require('lib/Logger').default;
|
||||
const KeymapService = require('lib/services/KeymapService').default;
|
||||
|
||||
class PluginManager {
|
||||
constructor() {
|
||||
@ -52,6 +51,7 @@ class PluginManager {
|
||||
const p = this.pluginInstance_(event.pluginName);
|
||||
p.onTrigger({
|
||||
itemName: event.itemName,
|
||||
userData: event.userData,
|
||||
});
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ class PluginManager {
|
||||
|
||||
return {
|
||||
Dialog: Class.Dialog,
|
||||
props: this.dialogProps_(name),
|
||||
props: Object.assign({}, this.dialogProps_(name), { userData: p.userData }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -81,20 +81,24 @@ class PluginManager {
|
||||
|
||||
menuItems() {
|
||||
let output = [];
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
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;
|
||||
|
||||
for (const item of menuItems) {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
const item = Object.assign({}, menuItems[i]);
|
||||
|
||||
item.click = () => {
|
||||
this.onPluginMenuItemTrigger_({
|
||||
pluginName: name,
|
||||
itemName: item.name,
|
||||
userData: item.userData,
|
||||
});
|
||||
};
|
||||
item.accelerator = keymapService.getAccelerator(name);
|
||||
|
||||
item.accelerator = menuItems[i].accelerator();
|
||||
|
||||
menuItems[i] = item;
|
||||
}
|
||||
|
||||
output = output.concat(menuItems);
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { ContextKeyExpr, ContextKeyExpression } from './contextkey/contextkey';
|
||||
|
||||
export default class BooleanExpression {
|
||||
export default class WhenClause {
|
||||
|
||||
private expression_:string;
|
||||
private validate_:boolean;
|
||||
private rules_:ContextKeyExpression = null;
|
||||
|
||||
constructor(expression:string) {
|
||||
constructor(expression:string, validate:boolean) {
|
||||
this.expression_ = expression;
|
||||
this.validate_ = validate;
|
||||
}
|
||||
|
||||
private createContext(ctx: any) {
|
||||
@ -21,11 +23,20 @@ export default class BooleanExpression {
|
||||
if (!this.rules_) {
|
||||
this.rules_ = ContextKeyExpr.deserialize(this.expression_);
|
||||
}
|
||||
|
||||
return this.rules_;
|
||||
}
|
||||
|
||||
public evaluate(context:any):boolean {
|
||||
if (this.validate_) this.validate(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}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -87,9 +87,9 @@ export default class MenuUtils {
|
||||
return item;
|
||||
}
|
||||
|
||||
public commandToStatefulMenuItem(commandName:string, props:any = null):MenuItem {
|
||||
public commandToStatefulMenuItem(commandName:string, ...args:any[]):MenuItem {
|
||||
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;
|
||||
}
|
||||
|
||||
public commandsToMenuItemProps(state:any, commandNames:string[]):MenuItemProps {
|
||||
public commandsToMenuItemProps(commandNames:string[], whenClauseContext:any):MenuItemProps {
|
||||
const output:MenuItemProps = {};
|
||||
|
||||
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)) {
|
||||
output[commandName] = newProps;
|
||||
this.menuItemPropsCache_[commandName] = newProps;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import CommandService from '../CommandService';
|
||||
import propsHaveChanged from './propsHaveChanged';
|
||||
import CommandService from 'lib/services/CommandService';
|
||||
import { stateUtils } from 'lib/reducer';
|
||||
|
||||
const separatorItem = { type: 'separator' };
|
||||
@ -14,7 +13,6 @@ export interface ToolbarButtonInfo {
|
||||
}
|
||||
|
||||
interface ToolbarButtonCacheItem {
|
||||
props: any,
|
||||
info: ToolbarButtonInfo,
|
||||
}
|
||||
|
||||
@ -35,8 +33,15 @@ export default class ToolbarButtonUtils {
|
||||
return this.service_;
|
||||
}
|
||||
|
||||
private commandToToolbarButton(commandName:string, props:any):ToolbarButtonInfo {
|
||||
if (this.toolbarButtonCache_[commandName] && !propsHaveChanged(this.toolbarButtonCache_[commandName].props, props)) {
|
||||
private commandToToolbarButton(commandName:string, whenClauseContext:any):ToolbarButtonInfo {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -46,15 +51,14 @@ export default class ToolbarButtonUtils {
|
||||
name: commandName,
|
||||
tooltip: this.service.label(commandName),
|
||||
iconName: command.declaration.iconName,
|
||||
enabled: this.service.isEnabled(commandName, props),
|
||||
enabled: newEnabled,
|
||||
onClick: async () => {
|
||||
this.service.execute(commandName, props);
|
||||
this.service.execute(commandName);
|
||||
},
|
||||
title: this.service.title(commandName, props),
|
||||
title: newTitle,
|
||||
};
|
||||
|
||||
this.toolbarButtonCache_[commandName] = {
|
||||
props: props,
|
||||
info: output,
|
||||
};
|
||||
|
||||
@ -64,7 +68,7 @@ export default class ToolbarButtonUtils {
|
||||
// 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
|
||||
// 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[] = [];
|
||||
|
||||
for (const commandName of commandNames) {
|
||||
@ -73,8 +77,7 @@ export default class ToolbarButtonUtils {
|
||||
continue;
|
||||
}
|
||||
|
||||
const props = this.service.commandMapStateToProps(commandName, state);
|
||||
output.push(this.commandToToolbarButton(commandName, props));
|
||||
output.push(this.commandToToolbarButton(commandName, whenClauseContext));
|
||||
}
|
||||
|
||||
return stateUtils.selectArrayShallow({ array: output }, commandNames.join('_'));
|
||||
|
@ -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);
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
export default function propsHaveChanged(previous:any, next:any):boolean {
|
||||
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;
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -30,11 +30,11 @@ export default class JoplinCommands {
|
||||
*
|
||||
* // Create a new sub-notebook under the provided notebook
|
||||
* // 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> {
|
||||
return CommandService.instance().execute(commandName, props);
|
||||
async execute(commandName: string, ...args:any[]):Promise<any> {
|
||||
return CommandService.instance().execute(commandName, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,8 +65,7 @@ export default class JoplinCommands {
|
||||
execute: command.execute,
|
||||
};
|
||||
|
||||
if ('isEnabled' in command) runtime.isEnabled = command.isEnabled;
|
||||
if ('mapStateToProps' in command) runtime.mapStateToProps = command.mapStateToProps;
|
||||
if ('enabledCondition' in command) runtime.enabledCondition = command.enabledCondition;
|
||||
|
||||
CommandService.instance().registerDeclaration(declaration);
|
||||
CommandService.instance().registerRuntime(declaration.name, runtime);
|
||||
|
@ -3,12 +3,47 @@
|
||||
// =================================================================
|
||||
|
||||
export interface Command {
|
||||
/**
|
||||
* Name of command - must be globally unique
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Label to be displayed on menu items or keyboard shortcut editor for example
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* Icon to be used on toolbar buttons for example
|
||||
*/
|
||||
iconName?: string,
|
||||
|
||||
/**
|
||||
* Code to be ran when the command is executed. It maybe return a result.
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
@ -87,7 +87,7 @@ function filterLogs(logs, platform) {
|
||||
if (platform === 'android' && prefix.indexOf('android') >= 0) addIt = true;
|
||||
if (platform === 'ios' && prefix.indexOf('ios') >= 0) addIt = true;
|
||||
if (platform === 'desktop' && prefix.indexOf('desktop') >= 0) addIt = true;
|
||||
if (platform === 'desktop' && (prefix.indexOf('desktop') >= 0 || prefix.indexOf('api') >= 0)) addIt = true;
|
||||
if (platform === 'desktop' && (prefix.indexOf('desktop') >= 0 || prefix.indexOf('api') >= 0 || prefix.indexOf('plugins') >= 0)) addIt = true;
|
||||
if (platform === 'cli' && prefix.indexOf('cli') >= 0) addIt = true;
|
||||
if (platform === 'clipper' && prefix.indexOf('clipper') >= 0) addIt = true;
|
||||
|
||||
@ -121,7 +121,7 @@ function formatCommitMessage(msg, author, options) {
|
||||
const isPlatformPrefix = prefix => {
|
||||
prefix = prefix.split(',').map(p => p.trim().toLowerCase());
|
||||
for (const p of prefix) {
|
||||
if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all', 'api'].indexOf(p) >= 0) return true;
|
||||
if (['android', 'mobile', 'ios', 'desktop', 'cli', 'clipper', 'all', 'api', 'plugins'].indexOf(p) >= 0) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@ -129,6 +129,7 @@ function formatCommitMessage(msg, author, options) {
|
||||
if (splitted.length) {
|
||||
const platform = splitted[0].trim().toLowerCase();
|
||||
if (platform === 'api') subModule = 'api';
|
||||
if (platform === 'plugins') subModule = 'plugins';
|
||||
if (isPlatformPrefix(platform)) {
|
||||
splitted.splice(0, 1);
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ module.exports = {
|
||||
'**/Modules/TinyMCE/IconPack/**',
|
||||
'**/CliClient/tests/support/plugins/**',
|
||||
'**/plugin_types/**',
|
||||
'**/ReactNativeClient/android/**',
|
||||
'**/ReactNativeClient/ios/**',
|
||||
],
|
||||
}).map(f => f.substr(rootDir.length + 1));
|
||||
|
||||
|
@ -59,7 +59,6 @@
|
||||
"ElectronClient/dist/": true,
|
||||
"ElectronClient/fonts/": true,
|
||||
"ElectronClient/gui/note-viewer/highlight/styles/": true,
|
||||
"ElectronClient/lib/": true,
|
||||
"ElectronClient/locale/": true,
|
||||
"ElectronClient/build/": true,
|
||||
"node_modules/": true,
|
||||
@ -255,6 +254,7 @@
|
||||
"ElectronClient/**/.DS_Store": true,
|
||||
"ElectronClient/**/gui/note-viewer/pluginAssets/": true,
|
||||
"ElectronClient/**/pluginAssets/": true,
|
||||
"ElectronClient/lib/": true,
|
||||
"Clipper/popup/**/node_modules": true,
|
||||
"Clipper/popup/**/coverage": true,
|
||||
"Clipper/popup/**/build": true,
|
||||
@ -360,7 +360,9 @@
|
||||
"CliClient/tests/support/plugins/settings/**/node_modules/": true,
|
||||
"CliClient/tests/support/plugins/selected_text/dist/*": true,
|
||||
"CliClient/tests/support/plugins/selected_text/**/node_modules/": true,
|
||||
"**/CliClient/build/": true
|
||||
"**/CliClient/build/": true,
|
||||
"**/*.js": {"when": "$(basename).ts"},
|
||||
"**/*?.js": { "when": "$(basename).tsx"},
|
||||
},
|
||||
"spellright.language": [
|
||||
"en"
|
||||
|
@ -15,7 +15,7 @@
|
||||
"generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json",
|
||||
"setupNewRelease": "node ./Tools/setupNewRelease",
|
||||
"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"
|
||||
},
|
||||
"husky": {
|
||||
|
@ -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:
|
||||
|
||||
@ -322,9 +322,7 @@ joplin.plugins.register({
|
||||
if (message.name === 'scrollToHash') {
|
||||
// As the name says, the scrollToHash command makes the note scroll
|
||||
// to the provided hash.
|
||||
joplin.commands.execute('scrollToHash', {
|
||||
hash: message.hash,
|
||||
})
|
||||
joplin.commands.execute('scrollToHash', message.hash)
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user