1
0
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:
Laurent Cozic 2020-10-18 21:52:10 +01:00
parent f529adac99
commit 3a57cfea02
78 changed files with 897 additions and 756 deletions

View File

@ -114,7 +114,7 @@ ElectronClient/gui/MainScreen/commands/showNoteProperties.js
ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
ElectronClient/gui/MainScreen/commands/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
View File

@ -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

View File

@ -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

View File

@ -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 }));
}));
});

View File

@ -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'));
},
});

View File

@ -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",

View File

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

View File

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

View File

@ -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;

View File

@ -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);

View File

@ -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');

View File

@ -1,13 +1,10 @@
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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',
};
};

View File

@ -1,11 +1,8 @@
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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',
};
};

View File

@ -1,12 +1,7 @@
import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
import CommandService, { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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') : '';
},
};
};

View File

@ -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:

View File

@ -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',
});

View File

@ -1,8 +1,8 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import eventManager from 'lib/eventManager';
import { _ } 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;
},
};
};

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import shim from 'lib/shim';
import 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',
};
};

View File

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

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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',
};
};

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
import { CommandContext, CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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');
},
};
};

View File

@ -1,8 +1,7 @@
import { utils, CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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',
};
};

View File

@ -1,4 +1,4 @@
import CommandService, { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
import CommandService, { CommandContext, CommandDeclaration, CommandRuntime } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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',
};
};

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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',
};
};

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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) {

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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({

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
const BaseModel = require('lib/BaseModel');
const 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({

View File

@ -1,4 +1,4 @@
import CommandService, { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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));
}
}

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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',
};
};

View File

@ -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,

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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',
};
};

View File

@ -1,5 +1,6 @@
import CommandService, { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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',
};
};

View File

@ -1,4 +1,4 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
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,

View File

@ -1,4 +1,4 @@
import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
import { CommandDeclaration, CommandRuntime, CommandContext } from 'lib/services/CommandService';
import Setting from 'lib/models/Setting';
import { 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',
};
};

View File

@ -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 = {

View File

@ -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',
};

View File

@ -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',
};
};

View File

@ -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,

View File

@ -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

View File

@ -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),
};
};

View File

@ -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],
};
};

View File

@ -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',
};
};

View File

@ -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',
};
};

View File

@ -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',
};
};

View File

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

View File

@ -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',
};
}

View File

@ -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');
}
}

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { CommandRuntime, CommandDeclaration, CommandContext } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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',
};
};

View File

@ -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}

View File

@ -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 = {

View File

@ -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),
};
};

View File

@ -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);

View File

@ -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');
}}
/>
);

View File

@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
import { CommandRuntime, CommandDeclaration } from 'lib/services/CommandService';
import { _ } from 'lib/locale';
import { 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',
};
};

View File

@ -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)
)
);

View File

@ -1,8 +1,12 @@
const React = require('react');
import * as React from 'react';
import { AppState } from '../app';
import CommandService, { SearchResult as CommandSearchResult } from 'lib/services/CommandService';
import KeymapService from 'lib/services/KeymapService';
import shim from 'lib/shim';
const { connect } = require('react-redux');
const { _ } = 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;

View File

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

View File

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

View File

@ -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',
};
};

View File

@ -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',
};
};

View File

@ -1,4 +1,4 @@
import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService';
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
import { _ } from 'lib/locale';
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,
};
},
};
};

View File

@ -2,6 +2,7 @@ import produce, { Draft } from 'immer';
import pluginServiceReducer, { stateRootKey as pluginServiceStateRootKey, defaultState as pluginServiceDefaultState, State as PluginServiceState } from 'lib/services/plugins/reducer';
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;
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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}`);
}
}
}

View File

@ -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;

View File

@ -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('_'));

View File

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

View File

@ -1,5 +1,7 @@
export default function propsHaveChanged(previous:any, next:any):boolean {
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;

View File

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

View File

@ -30,11 +30,11 @@ export default class JoplinCommands {
*
* // Create a new sub-notebook under the provided notebook
* // 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);

View File

@ -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
}
// =================================================================

View File

@ -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);
}

View File

@ -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));

View File

@ -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"

View File

@ -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": {

View File

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