diff --git a/.eslintignore b/.eslintignore
index 96158845f..d79232c44 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -61,9 +61,39 @@ Modules/TinyMCE/IconPack/postinstall.js
Modules/TinyMCE/langs/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
+ElectronClient/commands/focusElement.js
+ElectronClient/commands/startExternalEditing.js
+ElectronClient/commands/stopExternalEditing.js
ElectronClient/global.d.js
+ElectronClient/gui/ErrorBoundary.js
+ElectronClient/gui/Header/commands/focusSearch.js
+ElectronClient/gui/MainScreen/commands/editAlarm.js
+ElectronClient/gui/MainScreen/commands/exportPdf.js
+ElectronClient/gui/MainScreen/commands/hideModalMessage.js
+ElectronClient/gui/MainScreen/commands/moveToFolder.js
+ElectronClient/gui/MainScreen/commands/newNote.js
+ElectronClient/gui/MainScreen/commands/newNotebook.js
+ElectronClient/gui/MainScreen/commands/newTodo.js
+ElectronClient/gui/MainScreen/commands/print.js
+ElectronClient/gui/MainScreen/commands/renameFolder.js
+ElectronClient/gui/MainScreen/commands/renameTag.js
+ElectronClient/gui/MainScreen/commands/search.js
+ElectronClient/gui/MainScreen/commands/selectTemplate.js
+ElectronClient/gui/MainScreen/commands/setTags.js
+ElectronClient/gui/MainScreen/commands/showModalMessage.js
+ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js
+ElectronClient/gui/MainScreen/commands/showNoteProperties.js
+ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
+ElectronClient/gui/MainScreen/commands/toggleNoteList.js
+ElectronClient/gui/MainScreen/commands/toggleSidebar.js
+ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MultiNoteActions.js
ElectronClient/gui/NoteContentPropertiesDialog.js
+ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js
+ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
+ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
+ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
+ElectronClient/gui/NoteEditor/commands/showRevisions.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
@@ -95,12 +125,18 @@ ElectronClient/gui/NoteEditor/utils/useMessageHandler.js
ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js
ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js
ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js
+ElectronClient/gui/NoteList/commands/focusElementNoteList.js
ElectronClient/gui/NoteListItem.js
ElectronClient/gui/NoteToolbar/NoteToolbar.js
ElectronClient/gui/ResourceScreen.js
ElectronClient/gui/ShareNoteDialog.js
+ElectronClient/gui/SideBar/commands/focusElementSideBar.js
ReactNativeClient/lib/AsyncActionQueue.js
ReactNativeClient/lib/checkPermissions.js
+ReactNativeClient/lib/commands/historyBackward.js
+ReactNativeClient/lib/commands/historyForward.js
+ReactNativeClient/lib/commands/synchronize.js
+ReactNativeClient/lib/hooks/useEffectDebugger.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
@@ -108,6 +144,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
+ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
diff --git a/.gitignore b/.gitignore
index d95cdf5c9..b08e40c5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,9 +51,39 @@ Tools/commit_hook.txt
*.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
+ElectronClient/commands/focusElement.js
+ElectronClient/commands/startExternalEditing.js
+ElectronClient/commands/stopExternalEditing.js
ElectronClient/global.d.js
+ElectronClient/gui/ErrorBoundary.js
+ElectronClient/gui/Header/commands/focusSearch.js
+ElectronClient/gui/MainScreen/commands/editAlarm.js
+ElectronClient/gui/MainScreen/commands/exportPdf.js
+ElectronClient/gui/MainScreen/commands/hideModalMessage.js
+ElectronClient/gui/MainScreen/commands/moveToFolder.js
+ElectronClient/gui/MainScreen/commands/newNote.js
+ElectronClient/gui/MainScreen/commands/newNotebook.js
+ElectronClient/gui/MainScreen/commands/newTodo.js
+ElectronClient/gui/MainScreen/commands/print.js
+ElectronClient/gui/MainScreen/commands/renameFolder.js
+ElectronClient/gui/MainScreen/commands/renameTag.js
+ElectronClient/gui/MainScreen/commands/search.js
+ElectronClient/gui/MainScreen/commands/selectTemplate.js
+ElectronClient/gui/MainScreen/commands/setTags.js
+ElectronClient/gui/MainScreen/commands/showModalMessage.js
+ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js
+ElectronClient/gui/MainScreen/commands/showNoteProperties.js
+ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js
+ElectronClient/gui/MainScreen/commands/toggleNoteList.js
+ElectronClient/gui/MainScreen/commands/toggleSidebar.js
+ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js
ElectronClient/gui/MultiNoteActions.js
ElectronClient/gui/NoteContentPropertiesDialog.js
+ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js
+ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
+ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
+ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
+ElectronClient/gui/NoteEditor/commands/showRevisions.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
@@ -85,12 +115,18 @@ ElectronClient/gui/NoteEditor/utils/useMessageHandler.js
ElectronClient/gui/NoteEditor/utils/useNoteSearchBar.js
ElectronClient/gui/NoteEditor/utils/useSearchMarkers.js
ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.js
+ElectronClient/gui/NoteList/commands/focusElementNoteList.js
ElectronClient/gui/NoteListItem.js
ElectronClient/gui/NoteToolbar/NoteToolbar.js
ElectronClient/gui/ResourceScreen.js
ElectronClient/gui/ShareNoteDialog.js
+ElectronClient/gui/SideBar/commands/focusElementSideBar.js
ReactNativeClient/lib/AsyncActionQueue.js
ReactNativeClient/lib/checkPermissions.js
+ReactNativeClient/lib/commands/historyBackward.js
+ReactNativeClient/lib/commands/historyForward.js
+ReactNativeClient/lib/commands/synchronize.js
+ReactNativeClient/lib/hooks/useEffectDebugger.js
ReactNativeClient/lib/hooks/useImperativeHandlerDebugger.js
ReactNativeClient/lib/hooks/usePrevious.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/checkbox.js
@@ -98,6 +134,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/JoplinServerApi.js
+ReactNativeClient/lib/services/CommandService.js
ReactNativeClient/lib/services/keychain/KeychainService.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
diff --git a/ElectronClient/.gitignore b/ElectronClient/.gitignore
index 53f188b4d..2cfe42a9c 100644
--- a/ElectronClient/.gitignore
+++ b/ElectronClient/.gitignore
@@ -2,8 +2,7 @@ node_modules/
packageInfo.js
dist/
lib/
-gui/*.min.js
-plugins/*.min.js
+*.min.js
.DS_Store
gui/note-viewer/pluginAssets/
pluginAssets/
\ No newline at end of file
diff --git a/ElectronClient/InteropServiceHelper.js b/ElectronClient/InteropServiceHelper.js
index d0887f141..a870884ae 100644
--- a/ElectronClient/InteropServiceHelper.js
+++ b/ElectronClient/InteropServiceHelper.js
@@ -1,6 +1,7 @@
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const InteropService = require('lib/services/InteropService');
+const CommandService = require('lib/services/CommandService').default;
const Setting = require('lib/models/Setting');
const Note = require('lib/models/Note.js');
const { friendlySafeFilename } = require('lib/path-utils');
@@ -143,11 +144,7 @@ class InteropServiceHelper {
if (Array.isArray(path)) path = path[0];
- dispatch({
- type: 'WINDOW_COMMAND',
- name: 'showModalMessage',
- message: _('Exporting to "%s" as "%s" format. Please wait...', path, module.format),
- });
+ CommandService.instance().execute('showModalMessage', { message: _('Exporting to "%s" as "%s" format. Please wait...', path, module.format) });
const exportOptions = {};
exportOptions.path = path;
@@ -167,10 +164,7 @@ class InteropServiceHelper {
bridge().showErrorMessageBox(_('Could not export notes: %s', error.message));
}
- dispatch({
- type: 'WINDOW_COMMAND',
- name: 'hideModalMessage',
- });
+ CommandService.instance().execute('hideModalMessage');
}
}
diff --git a/ElectronClient/app.js b/ElectronClient/app.js
index 95f09482a..76d773871 100644
--- a/ElectronClient/app.js
+++ b/ElectronClient/app.js
@@ -5,9 +5,7 @@ const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
const Setting = require('lib/models/Setting.js');
const { shim } = require('lib/shim.js');
const MasterKey = require('lib/models/MasterKey');
-const Note = require('lib/models/Note');
const Folder = require('lib/models/Folder');
-const { MarkupToHtml } = require('lib/joplin-renderer');
const { _, setLocale } = require('lib/locale.js');
const { Logger } = require('lib/logger.js');
const fs = require('fs-extra');
@@ -30,9 +28,53 @@ const Menu = bridge().Menu;
const PluginManager = require('lib/services/PluginManager');
const RevisionService = require('lib/services/RevisionService');
const MigrationService = require('lib/services/MigrationService');
+const CommandService = require('lib/services/CommandService').default;
const TemplateUtils = require('lib/TemplateUtils');
const CssUtils = require('lib/CssUtils');
+const commands = [
+ require('./gui/Header/commands/focusSearch'),
+ require('./gui/MainScreen/commands/editAlarm'),
+ require('./gui/MainScreen/commands/exportPdf'),
+ require('./gui/MainScreen/commands/hideModalMessage'),
+ require('./gui/MainScreen/commands/moveToFolder'),
+ require('./gui/MainScreen/commands/newNote'),
+ require('./gui/MainScreen/commands/newNotebook'),
+ require('./gui/MainScreen/commands/newTodo'),
+ require('./gui/MainScreen/commands/print'),
+ require('./gui/MainScreen/commands/renameFolder'),
+ require('./gui/MainScreen/commands/renameTag'),
+ require('./gui/MainScreen/commands/search'),
+ require('./gui/MainScreen/commands/selectTemplate'),
+ require('./gui/MainScreen/commands/setTags'),
+ require('./gui/MainScreen/commands/showModalMessage'),
+ require('./gui/MainScreen/commands/showNoteContentProperties'),
+ require('./gui/MainScreen/commands/showNoteProperties'),
+ require('./gui/MainScreen/commands/showShareNoteDialog'),
+ require('./gui/MainScreen/commands/toggleNoteList'),
+ require('./gui/MainScreen/commands/toggleSidebar'),
+ require('./gui/MainScreen/commands/toggleVisiblePanes'),
+ require('./gui/NoteEditor/commands/focusElementNoteBody'),
+ require('./gui/NoteEditor/commands/focusElementNoteTitle'),
+ require('./gui/NoteEditor/commands/showLocalSearch'),
+ require('./gui/NoteEditor/commands/showRevisions'),
+ require('./gui/NoteList/commands/focusElementNoteList'),
+ require('./gui/SideBar/commands/focusElementSideBar'),
+];
+
+// Commands that are not tied to any particular component.
+// The runtime for these commands can be loaded when the app starts.
+const globalCommands = [
+ require('./commands/focusElement'),
+ require('./commands/startExternalEditing'),
+ require('./commands/stopExternalEditing'),
+ require('lib/commands/synchronize'),
+ require('lib/commands/historyBackward'),
+ require('lib/commands/historyForward'),
+];
+
+const editorCommandDeclarations = require('./gui/NoteEditor/commands/editorCommandDeclarations').default;
+
const pluginClasses = [
require('./plugins/GotoAnything.min'),
];
@@ -45,7 +87,6 @@ const appDefaultState = Object.assign({}, defaultState, {
},
navHistory: [],
fileToImport: null,
- windowCommand: null,
noteVisiblePanes: ['editor', 'viewer'],
sidebarVisibility: true,
noteListVisibility: true,
@@ -62,6 +103,14 @@ class Application extends BaseApplication {
this.lastMenuScreen_ = null;
this.bridge_nativeThemeUpdated = this.bridge_nativeThemeUpdated.bind(this);
+
+ this.commandService_commandsEnabledStateChange = this.commandService_commandsEnabledStateChange.bind(this);
+ CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
+ }
+
+ commandService_commandsEnabledStateChange() {
+ // TODO: only update if command is used in menu?
+ this.updateMenuItemStates();
}
hasGui() {
@@ -115,16 +164,6 @@ class Application extends BaseApplication {
newState.windowContentSize = action.size;
break;
- case 'WINDOW_COMMAND':
-
- {
- newState = Object.assign({}, state);
- const command = Object.assign({}, action);
- delete command.type;
- newState.windowCommand = command.name ? command : null;
- }
- break;
-
case 'NOTE_VISIBLE_PANES_TOGGLE':
{
@@ -252,8 +291,6 @@ class Application extends BaseApplication {
}
async generalMiddleware(store, next, action) {
- let mustUpdateMenuItemStates = false;
-
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'locale' || action.type == 'SETTING_UPDATE_ALL') {
setLocale(Setting.value('locale'));
// The bridge runs within the main process, with its own instance of locale.js
@@ -274,10 +311,6 @@ class Application extends BaseApplication {
webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
}
- if (action.type == 'SETTING_UPDATE_ONE' && ['editor.codeView'].includes(action.key) || action.type == 'SETTING_UPDATE_ALL') {
- mustUpdateMenuItemStates = true;
- }
-
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
}
@@ -301,13 +334,8 @@ class Application extends BaseApplication {
Setting.setValue('noteListVisibility', newState.noteListVisibility);
}
- if (action.type.indexOf('NOTE_SELECT') === 0 || action.type.indexOf('FOLDER_SELECT') === 0 || action.type === 'NOTE_VISIBLE_PANES_TOGGLE') {
- mustUpdateMenuItemStates = true;
- }
-
if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
this.toggleDevTools(newState.devToolsVisible);
- mustUpdateMenuItemStates = true;
}
if (action.type === 'FOLDER_AND_NOTE_SELECT') {
@@ -318,8 +346,6 @@ class Application extends BaseApplication {
this.handleThemeAutoDetect();
}
- if (mustUpdateMenuItemStates) this.updateMenuItemStates(newState);
-
return result;
}
@@ -339,17 +365,11 @@ class Application extends BaseApplication {
await this.updateMenu(screen);
}
- focusElement_(target) {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: target,
- });
- }
-
async updateMenu(screen) {
if (this.lastMenuScreen_ === screen) return;
+ const cmdService = CommandService.instance();
+
const sortNoteFolderItems = (type) => {
const sortItems = [];
const sortOptions = Setting.enumOptions(`${type}.sortOrder.field`);
@@ -386,31 +406,12 @@ class Application extends BaseApplication {
const sortNoteItems = sortNoteFolderItems('notes');
const sortFolderItems = sortNoteFolderItems('folders');
- const focusItems = [];
-
- focusItems.push({
- label: _('Sidebar'),
- click: () => { this.focusElement_('sideBar'); },
- accelerator: 'CommandOrControl+Shift+S',
- });
-
- focusItems.push({
- label: _('Note list'),
- click: () => { this.focusElement_('noteList'); },
- accelerator: 'CommandOrControl+Shift+L',
- });
-
- focusItems.push({
- label: _('Note title'),
- click: () => { this.focusElement_('noteTitle'); },
- accelerator: 'CommandOrControl+Shift+N',
- });
-
- focusItems.push({
- label: _('Note body'),
- click: () => { this.focusElement_('noteBody'); },
- accelerator: 'CommandOrControl+Shift+B',
- });
+ const focusItems = [
+ cmdService.commandToMenuItem('focusElementSideBar', 'CommandOrControl+Shift+S'),
+ cmdService.commandToMenuItem('focusElementNoteList', 'CommandOrControl+Shift+L'),
+ cmdService.commandToMenuItem('focusElementNoteTitle', 'CommandOrControl+Shift+N'),
+ cmdService.commandToMenuItem('focusElementNoteBody', 'CommandOrControl+Shift+B'),
+ ];
let toolsItems = [];
const importItems = [];
@@ -456,11 +457,7 @@ class Application extends BaseApplication {
if (Array.isArray(path)) path = path[0];
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'showModalMessage',
- message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format),
- });
+ cmdService.execute('showModalMessage', { message: _('Importing from "%s" as "%s" format. Please wait...', path, module.format) });
const importOptions = {
path,
@@ -481,28 +478,16 @@ class Application extends BaseApplication {
bridge().showErrorMessageBox(error.message);
}
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'hideModalMessage',
- });
+ cmdService.execute('hideModalMessage');
},
});
}
}
}
- exportItems.push({
- label: `PDF - ${_('PDF File')}`,
- screens: ['Main'],
- click: async () => {
- const selectedNoteIds = this.store().getState().selectedNoteIds;
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'exportPdf',
- noteIds: selectedNoteIds,
- });
- },
- });
+ exportItems.push(
+ cmdService.commandToMenuItem('exportPdf')
+ );
// We need a dummy entry, otherwise the ternary operator to show a
// menu item only on a specific OS does not work.
@@ -521,65 +506,10 @@ class Application extends BaseApplication {
},
};
- const newNoteItem = {
- label: _('New note'),
- accelerator: 'CommandOrControl+N',
- screens: ['Main'],
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'newNote',
- });
- },
- };
-
- const newTodoItem = {
- label: _('New to-do'),
- accelerator: 'CommandOrControl+T',
- screens: ['Main'],
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'newTodo',
- });
- },
- };
-
- const newNotebookItem = {
- label: _('New notebook'),
- screens: ['Main'],
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'newNotebook',
- });
- },
- };
-
- const newSubNotebookItem = {
- label: _('New sub-notebook'),
- screens: ['Main'],
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'newSubNotebook',
- activeFolderId: Setting.value('activeFolderId'),
- });
- },
- };
-
- const printItem = {
- label: _('Print'),
- accelerator: 'CommandOrControl+P',
- screens: ['Main'],
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'print',
- noteIds: this.store().getState().selectedNoteIds,
- });
- },
- };
+ const newNoteItem = cmdService.commandToMenuItem('newNote', 'CommandOrControl+N');
+ const newTodoItem = cmdService.commandToMenuItem('newTodo', 'CommandOrControl+T');
+ const newNotebookItem = cmdService.commandToMenuItem('newNotebook');
+ const printItem = cmdService.commandToMenuItem('print');
toolsItemsFirst.push(syncStatusItem, {
type: 'separator',
@@ -592,31 +522,20 @@ class Application extends BaseApplication {
label: _('Create note from template'),
visible: templateDirExists,
click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'selectTemplate',
- noteType: 'note',
- });
+ cmdService.execute('selectTemplate', { noteType: 'note' });
},
}, {
label: _('Create to-do from template'),
visible: templateDirExists,
click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'selectTemplate',
- noteType: 'todo',
- });
+ cmdService.execute('selectTemplate', { noteType: 'todo' });
},
}, {
label: _('Insert template'),
visible: templateDirExists,
accelerator: 'CommandOrControl+Alt+I',
click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'selectTemplate',
- });
+ cmdService.execute('selectTemplate');
},
}, {
label: _('Open template directory'),
@@ -740,8 +659,7 @@ class Application extends BaseApplication {
},
shim.isMac() ? noItem : newNoteItem,
shim.isMac() ? noItem : newTodoItem,
- shim.isMac() ? noItem : newNotebookItem,
- shim.isMac() ? noItem : newSubNotebookItem, {
+ shim.isMac() ? noItem : newNotebookItem, {
type: 'separator',
visible: shim.isMac() ? false : true,
}, {
@@ -761,17 +679,11 @@ class Application extends BaseApplication {
submenu: exportItems,
}, {
type: 'separator',
- }, {
- label: _('Synchronise'),
- accelerator: 'CommandOrControl+S',
- screens: ['Main'],
- click: async () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'synchronize',
- });
- },
- }, shim.isMac() ? syncStatusItem : noItem, {
+ },
+
+ cmdService.commandToMenuItem('synchronize', 'CommandOrControl+S'),
+
+ shim.isMac() ? syncStatusItem : noItem, {
type: 'separator',
}, shim.isMac() ? noItem : printItem, {
type: 'separator',
@@ -796,8 +708,7 @@ class Application extends BaseApplication {
submenu: [
newNoteItem,
newTodoItem,
- newNotebookItem,
- newSubNotebookItem, {
+ newNotebookItem, {
label: _('Close Window'),
platforms: ['darwin'],
accelerator: 'Command+W',
@@ -833,272 +744,127 @@ class Application extends BaseApplication {
},
}));
+ const separator = () => {
+ return {
+ type: 'separator',
+ screens: ['Main'],
+ };
+ };
+
const rootMenus = {
edit: {
id: 'edit',
label: _('&Edit'),
- submenu: [{
- id: 'edit:copy',
- label: _('Copy'),
- role: 'copy',
- accelerator: 'CommandOrControl+C',
- }, {
- id: 'edit:cut',
- label: _('Cut'),
- role: 'cut',
- accelerator: 'CommandOrControl+X',
- }, {
- id: 'edit:paste',
- label: _('Paste'),
- role: 'paste',
- accelerator: 'CommandOrControl+V',
- }, {
- id: 'edit:selectAll',
- label: _('Select all'),
- role: 'selectall',
- accelerator: 'CommandOrControl+A',
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- id: 'edit:bold',
- label: _('Bold'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+B',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textBold',
- });
- },
- }, {
- id: 'edit:italic',
- label: _('Italic'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+I',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textItalic',
- });
- },
- }, {
- id: 'edit:link',
- label: _('Link'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+K',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textLink',
- });
- },
- }, {
- id: 'edit:code',
- label: _('Code'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+`',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textCode',
- });
- },
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- id: 'edit:insertDateTime',
- label: _('Insert Date Time'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+Shift+T',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'insertDateTime',
- });
- },
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- id: 'edit:focusSearch',
- label: _('Search in all the notes'),
- screens: ['Main'],
- accelerator: shim.isMac() ? 'Shift+Command+F' : 'F6',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusSearch',
- });
- },
- }, {
- id: 'edit:showLocalSearch',
- label: _('Search in current note'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+F',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'showLocalSearch',
- });
- },
- }],
+ submenu: [
+ cmdService.commandToMenuItem('textCopy', 'CommandOrControl+C'),
+ cmdService.commandToMenuItem('textCut', 'CommandOrControl+X'),
+ cmdService.commandToMenuItem('textPaste', 'CommandOrControl+V'),
+ cmdService.commandToMenuItem('textSelectAll', 'CommandOrControl+A'),
+ separator(),
+ cmdService.commandToMenuItem('textBold', 'CommandOrControl+B'),
+ cmdService.commandToMenuItem('textItalic', 'CommandOrControl+I'),
+ cmdService.commandToMenuItem('textLink', 'CommandOrControl+K'),
+ cmdService.commandToMenuItem('textCode', 'CommandOrControl+`'),
+ separator(),
+ cmdService.commandToMenuItem('insertDateTime', 'CommandOrControl+Shift+T'),
+ separator(),
+ cmdService.commandToMenuItem('focusSearch', shim.isMac() ? 'Shift+Command+F' : 'F6'),
+ cmdService.commandToMenuItem('showLocalSearch', 'CommandOrControl+F'),
+ ],
},
view: {
label: _('&View'),
- submenu: [{
- label: _('Toggle sidebar'),
- screens: ['Main'],
- accelerator: shim.isMac() ? 'Option+Command+S' : 'F10',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'toggleSidebar',
- });
+ submenu: [
+ CommandService.instance().commandToMenuItem('toggleSidebar', shim.isMac() ? 'Option+Command+S' : 'F10'),
+ CommandService.instance().commandToMenuItem('toggleNoteList'),
+ CommandService.instance().commandToMenuItem('toggleVisiblePanes', 'CommandOrControl+L'),
+ {
+ label: _('Layout button sequence'),
+ screens: ['Main'],
+ submenu: layoutButtonSequenceOptions,
},
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- label: _('Layout button sequence'),
- screens: ['Main'],
- submenu: layoutButtonSequenceOptions,
- }, {
- label: _('Toggle note list'),
- screens: ['Main'],
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'toggleNoteList',
- });
+ separator(),
+ {
+ label: Setting.settingMetadata('notes.sortOrder.field').label(),
+ screens: ['Main'],
+ submenu: sortNoteItems,
+ }, {
+ label: Setting.settingMetadata('folders.sortOrder.field').label(),
+ screens: ['Main'],
+ submenu: sortFolderItems,
+ }, {
+ label: Setting.settingMetadata('showNoteCounts').label(),
+ type: 'checkbox',
+ checked: Setting.value('showNoteCounts'),
+ screens: ['Main'],
+ click: () => {
+ Setting.setValue('showNoteCounts', !Setting.value('showNoteCounts'));
+ },
+ }, {
+ label: Setting.settingMetadata('uncompletedTodosOnTop').label(),
+ type: 'checkbox',
+ checked: Setting.value('uncompletedTodosOnTop'),
+ screens: ['Main'],
+ click: () => {
+ Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
+ },
+ }, {
+ label: Setting.settingMetadata('showCompletedTodos').label(),
+ type: 'checkbox',
+ checked: Setting.value('showCompletedTodos'),
+ screens: ['Main'],
+ click: () => {
+ Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
+ },
},
- }, {
- label: _('Toggle editor layout'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+L',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'toggleVisiblePanes',
- });
+ separator(),
+ {
+ label: _('Focus'),
+ screens: ['Main'],
+ submenu: focusItems,
},
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- label: Setting.settingMetadata('notes.sortOrder.field').label(),
- screens: ['Main'],
- submenu: sortNoteItems,
- }, {
- label: Setting.settingMetadata('folders.sortOrder.field').label(),
- screens: ['Main'],
- submenu: sortFolderItems,
- }, {
- label: Setting.settingMetadata('showNoteCounts').label(),
- type: 'checkbox',
- checked: Setting.value('showNoteCounts'),
- screens: ['Main'],
- click: () => {
- Setting.setValue('showNoteCounts', !Setting.value('showNoteCounts'));
- },
- }, {
- label: Setting.settingMetadata('uncompletedTodosOnTop').label(),
- type: 'checkbox',
- checked: Setting.value('uncompletedTodosOnTop'),
- screens: ['Main'],
- click: () => {
- Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
- },
- }, {
- label: Setting.settingMetadata('showCompletedTodos').label(),
- type: 'checkbox',
- checked: Setting.value('showCompletedTodos'),
- screens: ['Main'],
- click: () => {
- Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
- },
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- label: _('Focus'),
- screens: ['Main'],
- submenu: focusItems,
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- label: _('Actual Size'),
- click: () => {
- Setting.setValue('windowContentZoomFactor', 100);
- },
- accelerator: 'CommandOrControl+0',
- }, {
+ separator(),
+ {
+ label: _('Actual Size'),
+ click: () => {
+ Setting.setValue('windowContentZoomFactor', 100);
+ },
+ accelerator: 'CommandOrControl+0',
+ }, {
// There are 2 shortcuts for the action 'zoom in', mainly to increase the user experience.
// Most applications handle this the same way. These applications indicate Ctrl +, but actually mean Ctrl =.
// In fact they allow both: + and =. On the English keyboard layout - and = are used without the shift key.
// So to use Ctrl + would mean to use the shift key, but this is not the case in any of the apps that show Ctrl +.
// Additionally it allows the use of the plus key on the numpad.
- label: _('Zoom In'),
- click: () => {
- Setting.incValue('windowContentZoomFactor', 10);
- },
- accelerator: 'CommandOrControl+Plus',
- }, {
- label: _('Zoom In'),
- visible: false,
- click: () => {
- Setting.incValue('windowContentZoomFactor', 10);
- },
- accelerator: 'CommandOrControl+=',
- }, {
- label: _('Zoom Out'),
- click: () => {
- Setting.incValue('windowContentZoomFactor', -10);
- },
- accelerator: 'CommandOrControl+-',
- }],
+ label: _('Zoom In'),
+ click: () => {
+ Setting.incValue('windowContentZoomFactor', 10);
+ },
+ accelerator: 'CommandOrControl+Plus',
+ }, {
+ label: _('Zoom In'),
+ visible: false,
+ click: () => {
+ Setting.incValue('windowContentZoomFactor', 10);
+ },
+ accelerator: 'CommandOrControl+=',
+ }, {
+ label: _('Zoom Out'),
+ click: () => {
+ Setting.incValue('windowContentZoomFactor', -10);
+ },
+ accelerator: 'CommandOrControl+-',
+ }],
},
note: {
label: _('&Note'),
- submenu: [{
- id: 'edit:commandStartExternalEditing',
- label: _('Edit in external editor'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+E',
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'commandStartExternalEditing',
- });
- },
- }, {
- id: 'edit:setTags',
- label: _('Tags'),
- screens: ['Main'],
- accelerator: 'CommandOrControl+Alt+T',
- click: () => {
- const selectedNoteIds = this.store().getState().selectedNoteIds;
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'setTags',
- noteIds: selectedNoteIds,
- });
- },
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
- id: 'note:statistics',
- label: _('Statistics...'),
- click: () => {
- this.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'commandContentProperties',
- });
- },
- }],
+ submenu: [
+
+ CommandService.instance().commandToMenuItem('startExternalEditing', 'CommandOrControl+E'),
+ CommandService.instance().commandToMenuItem('setTags', 'CommandOrControl+Alt+T'),
+ separator(),
+ CommandService.instance().commandToMenuItem('showNoteContentProperties'),
+ ],
},
tools: {
label: _('&Tools'),
@@ -1121,14 +887,11 @@ class Application extends BaseApplication {
label: _('Check for updates...'),
visible: shim.isMac() ? false : true,
click: () => _checkForUpdates(this),
- }, {
- type: 'separator',
- screens: ['Main'],
- }, {
+ },
+ separator(),
+ {
id: 'help:toggleDevTools',
- type: 'checkbox',
label: _('Toggle development tools'),
- visible: true,
click: () => {
this.dispatch({
type: 'NOTE_DEVTOOLS_TOGGLE',
@@ -1240,49 +1003,22 @@ class Application extends BaseApplication {
if (!state) state = this.store().getState();
- const selectedNoteIds = state.selectedNoteIds;
- const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
- const aceEditorViewerOnly = state.settings['editor.codeView'] && state.noteVisiblePanes.length === 1 && state.noteVisiblePanes[0] === 'viewer';
+ const menuEnabledState = CommandService.instance().commandsEnabledState(this.previousMenuEnabledState);
+ this.previousMenuEnabledState = menuEnabledState;
- // Only enabled when there's only one active note, and that note
- // is a Markdown note (markup_language = MARKDOWN), and the
- // editor is in edit mode (not viewer-only mode).
- const singleMarkdownNoteMenuItems = [
- 'edit:bold',
- 'edit:italic',
- 'edit:link',
- 'edit:code',
- 'edit:insertDateTime',
- ];
+ const menu = Menu.getApplicationMenu();
- // Only enabled when there's only one active note.
- const singleNoteMenuItems = [
- 'edit:copy',
- 'edit:paste',
- 'edit:cut',
- 'edit:selectAll',
- 'edit:showLocalSearch',
- 'edit:commandStartExternalEditing',
- 'note:statistics',
- ];
-
- for (const itemId of singleMarkdownNoteMenuItems) {
- const menuItem = Menu.getApplicationMenu().getMenuItemById(itemId);
+ for (const itemId in menuEnabledState) {
+ const menuItem = menu.getMenuItemById(itemId);
if (!menuItem) continue;
- menuItem.enabled = !aceEditorViewerOnly && !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
+ menuItem.enabled = menuEnabledState[itemId];
}
- for (const itemId of singleNoteMenuItems) {
- const menuItem = Menu.getApplicationMenu().getMenuItemById(itemId);
- if (!menuItem) continue;
- menuItem.enabled = selectedNoteIds.length === 1;
- }
-
- const sortNoteReverseItem = Menu.getApplicationMenu().getMenuItemById('sort:notes:reverse');
+ const sortNoteReverseItem = menu.getMenuItemById('sort:notes:reverse');
sortNoteReverseItem.enabled = state.settings['notes.sortOrder.field'] !== 'order';
- const menuItem = Menu.getApplicationMenu().getMenuItemById('help:toggleDevTools');
- menuItem.checked = state.devToolsVisible;
+ // const devToolsMenuItem = menu.getMenuItemById('help:toggleDevTools');
+ // devToolsMenuItem.checked = state.devToolsVisible;
}
bridge_nativeThemeUpdated() {
@@ -1400,10 +1136,25 @@ class Application extends BaseApplication {
PluginManager.instance().setLogger(reg.logger());
PluginManager.instance().register(pluginClasses);
- this.updateMenu('Main');
-
this.initRedux();
+ CommandService.instance().initialize(this.store());
+
+ for (const command of commands) {
+ CommandService.instance().registerDeclaration(command.declaration);
+ }
+
+ for (const command of globalCommands) {
+ CommandService.instance().registerDeclaration(command.declaration);
+ CommandService.instance().registerRuntime(command.declaration.name, command.runtime());
+ }
+
+ for (const declaration of editorCommandDeclarations) {
+ CommandService.instance().registerDeclaration(declaration);
+ }
+
+ this.updateMenu('Main');
+
// Since the settings need to be loaded before the store is created, it will never
// receive the SETTING_UPDATE_ALL even, which mean state.settings will not be
// initialised. So we manually call dispatchUpdateAll() to force an update.
diff --git a/ElectronClient/commands/focusElement.ts b/ElectronClient/commands/focusElement.ts
new file mode 100644
index 000000000..1e47b0826
--- /dev/null
+++ b/ElectronClient/commands/focusElement.ts
@@ -0,0 +1,17 @@
+import CommandService, { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
+
+export const declaration:CommandDeclaration = {
+ name: 'focusElement',
+};
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async ({ target }:any) => {
+ if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody');
+ if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList');
+ if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar');
+ if (target === 'noteTitle') return CommandService.instance().execute('focusElementNoteTitle');
+ throw new Error(`Invalid focus target: ${target}`);
+ },
+ };
+};
diff --git a/ElectronClient/commands/startExternalEditing.ts b/ElectronClient/commands/startExternalEditing.ts
new file mode 100644
index 000000000..8666fdd48
--- /dev/null
+++ b/ElectronClient/commands/startExternalEditing.ts
@@ -0,0 +1,36 @@
+import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
+const { _ } = require('lib/locale');
+const Note = require('lib/models/Note');
+const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
+const { bridge } = require('electron').remote.require('./bridge');
+
+interface Props {
+ noteId: string
+}
+
+export const declaration:CommandDeclaration = {
+ name: 'startExternalEditing',
+ label: () => _('Edit in external editor'),
+ iconName: 'fa-share-square',
+};
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async (props:Props) => {
+ try {
+ const note = await Note.load(props.noteId);
+ ExternalEditWatcher.instance().openAndWatch(note);
+ } catch (error) {
+ bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
+ }
+
+ // await comp.saveNoteAndWait(comp.formNote);
+ },
+ isEnabled: (props:any) => {
+ return !!props.noteId;
+ },
+ mapStateToProps: (state:any) => {
+ return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
+ },
+ };
+};
diff --git a/ElectronClient/commands/stopExternalEditing.ts b/ElectronClient/commands/stopExternalEditing.ts
new file mode 100644
index 000000000..5a3f9f68a
--- /dev/null
+++ b/ElectronClient/commands/stopExternalEditing.ts
@@ -0,0 +1,27 @@
+import { CommandRuntime, CommandDeclaration } from '../lib/services/CommandService';
+const { _ } = require('lib/locale');
+const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
+
+interface Props {
+ noteId: string
+}
+
+export const declaration:CommandDeclaration = {
+ name: 'stopExternalEditing',
+ label: () => _('Stop external editing'),
+ iconName: 'fa-stop',
+};
+
+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 };
+ },
+ };
+};
diff --git a/ElectronClient/gui/DropboxLoginScreen.jsx b/ElectronClient/gui/DropboxLoginScreen.jsx
index cf8b60e9c..c46906d88 100644
--- a/ElectronClient/gui/DropboxLoginScreen.jsx
+++ b/ElectronClient/gui/DropboxLoginScreen.jsx
@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const { bridge } = require('electron').remote.require('./bridge');
-const { Header } = require('./Header.min.js');
+const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const Shared = require('lib/components/shared/dropbox-login-shared');
diff --git a/ElectronClient/gui/ErrorBoundary.tsx b/ElectronClient/gui/ErrorBoundary.tsx
new file mode 100644
index 000000000..25feb636e
--- /dev/null
+++ b/ElectronClient/gui/ErrorBoundary.tsx
@@ -0,0 +1,48 @@
+import * as React from 'react';
+
+export default class ErrorBoundary extends React.Component {
+
+ state:any = { error: null, errorInfo: null };
+
+ componentDidCatch(error:any, errorInfo:any) {
+ this.setState({ error: error, errorInfo: errorInfo });
+ }
+
+ render() {
+ if (this.state.error) {
+ try {
+ const output = [];
+ output.push(
Message
);
+ output.push({this.state.error.message}
);
+
+ if (this.state.error.stack) {
+ output.push(Stack trace
);
+ output.push({this.state.error.stack}
);
+ }
+
+ if (this.state.errorInfo) {
+ if (this.state.errorInfo.componentStack) {
+ output.push(Component stack
);
+ output.push({this.state.errorInfo.componentStack}
);
+ }
+ }
+
+ return (
+
+
Error
+
Joplin encountered a fatal error and could not continue. To report the error, please copy the *entire content* of this page and post it on Joplin forum or GitHub.
+ {output}
+
+ );
+ } catch (error) {
+ return (
+
+ {JSON.stringify(this.state)}
+
+ );
+ }
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/ElectronClient/gui/Header.jsx b/ElectronClient/gui/Header/Header.jsx
similarity index 94%
rename from ElectronClient/gui/Header.jsx
rename to ElectronClient/gui/Header/Header.jsx
index fdc2f78c2..3a68a100b 100644
--- a/ElectronClient/gui/Header.jsx
+++ b/ElectronClient/gui/Header/Header.jsx
@@ -3,6 +3,11 @@ const { connect } = require('react-redux');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
+const CommandService = require('lib/services/CommandService').default;
+
+const commands = [
+ require('./commands/focusSearch'),
+];
class HeaderComponent extends React.Component {
constructor() {
@@ -13,6 +18,10 @@ class HeaderComponent extends React.Component {
showButtonLabels: true,
};
+ for (const command of commands) {
+ CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this));
+ }
+
this.scheduleSearchChangeEventIid_ = null;
this.searchOnQuery_ = null;
this.searchElement_ = null;
@@ -72,12 +81,6 @@ class HeaderComponent extends React.Component {
};
}
- async UNSAFE_componentWillReceiveProps(nextProps) {
- if (nextProps.windowCommand) {
- this.doCommand(nextProps.windowCommand);
- }
- }
-
componentDidUpdate(prevProps) {
if (prevProps.notesParentType !== this.props.notesParentType && this.props.notesParentType !== 'Search' && this.state.searchQuery) {
this.resetSearch();
@@ -97,6 +100,10 @@ class HeaderComponent extends React.Component {
clearTimeout(this.hideSearchUsageLinkIID_);
this.hideSearchUsageLinkIID_ = null;
}
+
+ for (const command of commands) {
+ CommandService.instance().unregisterRuntime(command.declaration.name);
+ }
}
determineButtonLabelState() {
@@ -110,25 +117,6 @@ class HeaderComponent extends React.Component {
}
}
- async doCommand(command) {
- if (!command) return;
-
- let commandProcessed = true;
-
- if (command.name === 'focusSearch' && this.searchElement_) {
- this.searchElement_.focus();
- } else {
- commandProcessed = false;
- }
-
- if (commandProcessed) {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: null,
- });
- }
- }
-
back_click() {
this.props.dispatch({ type: 'NAV_BACK' });
}
@@ -329,7 +317,6 @@ class HeaderComponent extends React.Component {
const mapStateToProps = state => {
return {
theme: state.settings.theme,
- windowCommand: state.windowCommand,
notesParentType: state.notesParentType,
size: state.windowContentSize,
zoomFactor: state.settings.windowContentZoomFactor / 100,
diff --git a/ElectronClient/gui/Header/commands/focusSearch.ts b/ElectronClient/gui/Header/commands/focusSearch.ts
new file mode 100644
index 000000000..9abe4c800
--- /dev/null
+++ b/ElectronClient/gui/Header/commands/focusSearch.ts
@@ -0,0 +1,15 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'focusSearch',
+ label: () => _('Search in all the notes'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ if (comp.searchElement_) comp.searchElement_.focus();
+ },
+ };
+};
diff --git a/ElectronClient/gui/ImportScreen.jsx b/ElectronClient/gui/ImportScreen.jsx
index 1fffe8935..e037106f8 100644
--- a/ElectronClient/gui/ImportScreen.jsx
+++ b/ElectronClient/gui/ImportScreen.jsx
@@ -1,7 +1,7 @@
const React = require('react');
const { connect } = require('react-redux');
const Folder = require('lib/models/Folder.js');
-const { Header } = require('./Header.min.js');
+const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { filename, basename } = require('lib/path-utils.js');
diff --git a/ElectronClient/gui/MainScreen.jsx b/ElectronClient/gui/MainScreen/MainScreen.jsx
similarity index 50%
rename from ElectronClient/gui/MainScreen.jsx
rename to ElectronClient/gui/MainScreen/MainScreen.jsx
index 815d1dcfe..0b9f71573 100644
--- a/ElectronClient/gui/MainScreen.jsx
+++ b/ElectronClient/gui/MainScreen/MainScreen.jsx
@@ -1,33 +1,50 @@
const React = require('react');
const { connect } = require('react-redux');
-const { Header } = require('./Header.min.js');
-const { SideBar } = require('./SideBar.min.js');
-const { NoteList } = require('./NoteList.min.js');
-const NoteEditor = require('./NoteEditor/NoteEditor.js').default;
+const { Header } = require('../Header/Header.min.js');
+const { SideBar } = require('../SideBar/SideBar.min.js');
+const { NoteList } = require('../NoteList/NoteList.min.js');
+const NoteEditor = require('../NoteEditor/NoteEditor.js').default;
const { stateUtils } = require('lib/reducer.js');
-const { PromptDialog } = require('./PromptDialog.min.js');
-const NoteContentPropertiesDialog = require('./NoteContentPropertiesDialog.js').default;
-const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
-const ShareNoteDialog = require('./ShareNoteDialog.js').default;
-const InteropServiceHelper = require('../InteropServiceHelper.js');
+const { PromptDialog } = require('../PromptDialog.min.js');
+const NoteContentPropertiesDialog = require('../NoteContentPropertiesDialog.js').default;
+const NotePropertiesDialog = require('../NotePropertiesDialog.min.js');
+const ShareNoteDialog = require('../ShareNoteDialog.js').default;
+const InteropServiceHelper = require('../../InteropServiceHelper.js');
const Setting = require('lib/models/Setting.js');
-const BaseModel = require('lib/BaseModel.js');
-const Tag = require('lib/models/Tag.js');
-const Note = require('lib/models/Note.js');
-const { uuid } = require('lib/uuid.js');
const { shim } = require('lib/shim');
-const Folder = require('lib/models/Folder.js');
const { themeStyle } = require('lib/theme.js');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
-const eventManager = require('../eventManager');
-const VerticalResizer = require('./VerticalResizer.min');
+const VerticalResizer = require('../VerticalResizer.min');
const PluginManager = require('lib/services/PluginManager');
-const TemplateUtils = require('lib/TemplateUtils');
const EncryptionService = require('lib/services/EncryptionService');
+const CommandService = require('lib/services/CommandService').default;
const ipcRenderer = require('electron').ipcRenderer;
const { time } = require('lib/time-utils.js');
+const commands = [
+ require('./commands/editAlarm'),
+ require('./commands/exportPdf'),
+ require('./commands/hideModalMessage'),
+ require('./commands/moveToFolder'),
+ require('./commands/newNote'),
+ require('./commands/newNotebook'),
+ require('./commands/newTodo'),
+ require('./commands/print'),
+ require('./commands/renameFolder'),
+ require('./commands/renameTag'),
+ require('./commands/search'),
+ require('./commands/selectTemplate'),
+ require('./commands/setTags'),
+ require('./commands/showModalMessage'),
+ require('./commands/showNoteContentProperties'),
+ require('./commands/showNoteProperties'),
+ require('./commands/showShareNoteDialog'),
+ require('./commands/toggleNoteList'),
+ require('./commands/toggleSidebar'),
+ require('./commands/toggleVisiblePanes'),
+];
+
class MainScreenComponent extends React.Component {
constructor() {
super();
@@ -43,15 +60,16 @@ class MainScreenComponent extends React.Component {
shareNoteDialogOptions: {},
};
+ this.registerCommands();
+
this.setupAppCloseHandling();
+ this.commandService_commandsEnabledStateChange = this.commandService_commandsEnabledStateChange.bind(this);
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
this.noteContentPropertiesDialog_close = this.noteContentPropertiesDialog_close.bind(this);
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
this.noteList_onDrag = this.noteList_onDrag.bind(this);
- this.commandSavePdf = this.commandSavePdf.bind(this);
- this.commandPrint = this.commandPrint.bind(this);
}
setupAppCloseHandling() {
@@ -105,17 +123,31 @@ class MainScreenComponent extends React.Component {
this.setState({ shareNoteDialogOptions: {} });
}
- UNSAFE_componentWillReceiveProps(newProps) {
- // Execute a command if any, and if we haven't already executed it
- if (newProps.windowCommand && newProps.windowCommand !== this.props.windowCommand) {
- this.doCommand(newProps.windowCommand);
+ commandService_commandsEnabledStateChange(event) {
+ const buttonCommandNames = [
+ 'toggleSidebar',
+ 'toggleNoteList',
+ 'newNote',
+ 'newTodo',
+ 'newNotebook',
+ 'toggleVisiblePanes',
+ ];
+
+ for (const n of buttonCommandNames) {
+ if (event.commands[n]) {
+ this.forceUpdate();
+ return;
+ }
}
}
- toggleVisiblePanes() {
- this.props.dispatch({
- type: 'NOTE_VISIBLE_PANES_TOGGLE',
- });
+ componentDidMount() {
+ CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
+ }
+
+ componentWillUnmount() {
+ CommandService.instance().off('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
+ this.unregisterCommands();
}
toggleSidebar() {
@@ -130,361 +162,6 @@ class MainScreenComponent extends React.Component {
});
}
- async doCommand(command) {
- if (!command) return;
-
- const createNewNote = async (template, isTodo) => {
- const folderId = Setting.value('activeFolderId');
- if (!folderId) return;
-
- const body = template ? TemplateUtils.render(template) : '';
-
- const defaultValues = Note.previewFieldsWithDefaultValues({ includeTimestamps: false });
-
- let newNote = Object.assign({}, defaultValues, {
- parent_id: folderId,
- is_todo: isTodo ? 1 : 0,
- body: body,
- });
-
- newNote = await Note.save(newNote, { provisional: true });
-
- this.props.dispatch({
- type: 'NOTE_SELECT',
- id: newNote.id,
- });
- };
-
- let commandProcessed = true;
-
- let delayedFunction = null;
- let delayedArgs = null;
-
- if (command.name === 'newNote') {
- if (!this.props.folders.length) {
- bridge().showErrorMessageBox(_('Please create a notebook first.'));
- } else {
- await createNewNote(null, false);
- }
- } else if (command.name === 'newTodo') {
- if (!this.props.folders.length) {
- bridge().showErrorMessageBox(_('Please create a notebook first'));
- } else {
- await createNewNote(null, true);
- }
- } else if (command.name === 'newNotebook' || (command.name === 'newSubNotebook' && command.activeFolderId)) {
- this.setState({
- promptOptions: {
- label: _('Notebook title:'),
- onClose: async answer => {
- if (answer) {
- let folder = null;
- try {
- folder = await Folder.save({ title: answer }, { userSideValidation: true });
- if (command.name === 'newSubNotebook') folder = await Folder.moveToFolder(folder.id, command.activeFolderId);
- } catch (error) {
- bridge().showErrorMessageBox(error.message);
- }
-
- if (folder) {
- this.props.dispatch({
- type: 'FOLDER_SELECT',
- id: folder.id,
- });
- }
- }
-
- this.setState({ promptOptions: null });
- },
- },
- });
- } else if (command.name === 'setTags') {
- const tags = await Tag.commonTagsByNoteIds(command.noteIds);
- const startTags = tags
- .map(a => {
- return { value: a.id, label: a.title };
- })
- .sort((a, b) => {
- // sensitivity accent will treat accented characters as differemt
- // but treats caps as equal
- return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
- });
- const allTags = await Tag.allWithNotes();
- const tagSuggestions = allTags.map(a => {
- return { value: a.id, label: a.title };
- })
- .sort((a, b) => {
- // sensitivity accent will treat accented characters as differemt
- // but treats caps as equal
- return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
- });
-
- this.setState({
- promptOptions: {
- label: _('Add or remove tags:'),
- inputType: 'tags',
- value: startTags,
- autocomplete: tagSuggestions,
- onClose: async answer => {
- if (answer !== null) {
- const endTagTitles = answer.map(a => {
- return a.label.trim();
- });
- if (command.noteIds.length === 1) {
- await Tag.setNoteTagsByTitles(command.noteIds[0], endTagTitles);
- } else {
- const startTagTitles = startTags.map(a => { return a.label.trim(); });
- const addTags = endTagTitles.filter(value => !startTagTitles.includes(value));
- const delTags = startTagTitles.filter(value => !endTagTitles.includes(value));
-
- // apply the tag additions and deletions to each selected note
- for (let i = 0; i < command.noteIds.length; i++) {
- const tags = await Tag.tagsByNoteId(command.noteIds[i]);
- let tagTitles = tags.map(a => { return a.title; });
- tagTitles = tagTitles.concat(addTags);
- tagTitles = tagTitles.filter(value => !delTags.includes(value));
- await Tag.setNoteTagsByTitles(command.noteIds[i], tagTitles);
- }
- }
- }
- this.setState({ promptOptions: null });
- },
- },
- });
- } else if (command.name === 'moveToFolder') {
- const folders = await Folder.sortFolderTree();
- const startFolders = [];
- const maxDepth = 15;
-
- const addOptions = (folders, depth) => {
- for (let i = 0; i < folders.length; i++) {
- const folder = folders[i];
- startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
- if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
- }
- };
-
- addOptions(folders, 0);
-
- this.setState({
- promptOptions: {
- label: _('Move to notebook:'),
- inputType: 'dropdown',
- value: '',
- autocomplete: startFolders,
- onClose: async answer => {
- if (answer != null) {
- for (let i = 0; i < command.noteIds.length; i++) {
- await Note.moveToFolder(command.noteIds[i], answer.value);
- }
- }
- this.setState({ promptOptions: null });
- },
- },
- });
- } else if (command.name === 'renameFolder') {
- const folder = await Folder.load(command.id);
-
- if (folder) {
- this.setState({
- promptOptions: {
- label: _('Rename notebook:'),
- value: folder.title,
- onClose: async answer => {
- if (answer !== null) {
- try {
- folder.title = answer;
- await Folder.save(folder, { fields: ['title'], userSideValidation: true });
- } catch (error) {
- bridge().showErrorMessageBox(error.message);
- }
- }
- this.setState({ promptOptions: null });
- },
- },
- });
- }
- } else if (command.name === 'renameTag') {
- const tag = await Tag.load(command.id);
- if (tag) {
- this.setState({
- promptOptions: {
- label: _('Rename tag:'),
- value: tag.title,
- onClose: async answer => {
- if (answer !== null) {
- try {
- tag.title = answer;
- await Tag.save(tag, { fields: ['title'], userSideValidation: true });
- } catch (error) {
- bridge().showErrorMessageBox(error.message);
- }
- }
- this.setState({ promptOptions: null });
- },
- },
- });
- }
- } else if (command.name === 'search') {
- if (!this.searchId_) this.searchId_ = uuid.create();
-
- this.props.dispatch({
- type: 'SEARCH_UPDATE',
- search: {
- id: this.searchId_,
- title: command.query,
- query_pattern: command.query,
- query_folder_id: null,
- type_: BaseModel.TYPE_SEARCH,
- },
- });
-
- if (command.query) {
- this.props.dispatch({
- type: 'SEARCH_SELECT',
- id: this.searchId_,
- });
- } else {
- const note = await Note.load(this.props.selectedNoteId);
- if (note) {
- this.props.dispatch({
- type: 'FOLDER_AND_NOTE_SELECT',
- folderId: note.parent_id,
- noteId: note.id,
- });
- }
- }
- } else if (command.name === 'commandNoteProperties') {
- this.setState({
- notePropertiesDialogOptions: {
- noteId: command.noteId,
- visible: true,
- onRevisionLinkClick: command.onRevisionLinkClick,
- },
- });
- } else if (command.name === 'commandContentProperties') {
- const note = await Note.load(this.props.selectedNoteId);
- if (note) {
- this.setState({
- noteContentPropertiesDialogOptions: {
- visible: true,
- text: note.body,
- // lines: command.lines,
- },
- });
- }
- } else if (command.name === 'commandShareNoteDialog') {
- this.setState({
- shareNoteDialogOptions: {
- noteIds: command.noteIds,
- visible: true,
- },
- });
- } else if (command.name === 'toggleVisiblePanes') {
- this.toggleVisiblePanes();
- } else if (command.name === 'toggleSidebar') {
- this.toggleSidebar();
- } else if (command.name === 'toggleNoteList') {
- this.toggleNoteList();
- } else if (command.name === 'showModalMessage') {
- this.setState({
- modalLayer: {
- visible: true,
- message:
- ,
- },
- });
- } else if (command.name === 'hideModalMessage') {
- this.setState({ modalLayer: { visible: false, message: '' } });
- } else if (command.name === 'editAlarm') {
- const note = await Note.load(command.noteId);
-
- const defaultDate = new Date(Date.now() + 2 * 3600 * 1000);
- defaultDate.setMinutes(0);
- defaultDate.setSeconds(0);
-
- this.setState({
- promptOptions: {
- label: _('Set alarm:'),
- inputType: 'datetime',
- buttons: ['ok', 'cancel', 'clear'],
- value: note.todo_due ? new Date(note.todo_due) : defaultDate,
- onClose: async (answer, buttonType) => {
- let newNote = null;
-
- if (buttonType === 'clear') {
- newNote = {
- id: note.id,
- todo_due: 0,
- };
- } else if (answer !== null) {
- newNote = {
- id: note.id,
- todo_due: answer.getTime(),
- };
- }
-
- if (newNote) {
- await Note.save(newNote);
- eventManager.emit('alarmChange', { noteId: note.id, note: newNote });
- }
-
- this.setState({ promptOptions: null });
- },
- },
- });
- } else if (command.name === 'selectTemplate') {
- this.setState({
- promptOptions: {
- label: _('Template file:'),
- inputType: 'dropdown',
- value: this.props.templates[0], // Need to start with some value
- autocomplete: this.props.templates,
- onClose: async answer => {
- if (answer) {
- if (command.noteType === 'note' || command.noteType === 'todo') {
- createNewNote(answer.value, command.noteType === 'todo');
- } else {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'insertTemplate',
- value: answer.value,
- });
- }
- }
-
- this.setState({ promptOptions: null });
- },
- },
- });
- } else if (command.name === 'exportPdf') {
- delayedFunction = this.commandSavePdf;
- delayedArgs = { noteIds: command.noteIds };
- } else if (command.name === 'print') {
- delayedFunction = this.commandPrint;
- delayedArgs = { noteIds: command.noteIds };
- } else {
- commandProcessed = false;
- }
-
- if (commandProcessed) {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: null,
- });
- }
-
- if (delayedFunction) {
- requestAnimationFrame(() => {
- delayedFunction = delayedFunction.bind(this);
- delayedFunction(delayedArgs);
- });
- }
- }
-
async waitForNoteToSaved(noteId) {
while (noteId && this.props.editorNoteStatuses[noteId] === 'saving') {
console.info('Waiting for note to be saved...', this.props.editorNoteStatuses);
@@ -531,59 +208,6 @@ class MainScreenComponent extends React.Component {
this.isPrinting_ = false;
}
- async commandSavePdf(args) {
- try {
- const noteIds = args.noteIds;
-
- if (!noteIds.length) throw new Error('No notes selected for pdf export');
-
- let path = null;
- if (noteIds.length === 1) {
- path = bridge().showSaveDialog({
- filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
- defaultPath: await InteropServiceHelper.defaultFilename(noteIds[0], 'pdf'),
- });
-
- } else {
- path = bridge().showOpenDialog({
- properties: ['openDirectory', 'createDirectory'],
- });
- }
-
- if (!path) return;
-
- for (let i = 0; i < noteIds.length; i++) {
- const note = await Note.load(noteIds[i]);
-
- let pdfPath = '';
-
- if (noteIds.length === 1) {
- pdfPath = path;
- } else {
- const n = await InteropServiceHelper.defaultFilename(note.id, 'pdf');
- pdfPath = await shim.fsDriver().findUniqueFilename(`${path}/${n}`);
- }
-
- await this.printTo_('pdf', { path: pdfPath, noteId: note.id });
- }
- } catch (error) {
- console.error(error);
- bridge().showErrorMessageBox(error.message);
- }
- }
-
- async commandPrint(args) {
- // TODO: test
- try {
- const noteIds = args.noteIds;
- if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.'));
-
- await this.printTo_('printer', { noteId: noteIds[0] });
- } catch (error) {
- bridge().showErrorMessageBox(error.message);
- }
- }
-
styles(themeId, width, height, messageBoxVisible, isSidebarVisible, isNoteListVisible, sidebarWidth, noteListWidth) {
const styleKey = [themeId, width, height, messageBoxVisible, +isSidebarVisible, +isNoteListVisible, sidebarWidth, noteListWidth].join('_');
if (styleKey === this.styleKey_) return this.styles_;
@@ -750,6 +374,18 @@ class MainScreenComponent extends React.Component {
return this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage || this.props.showNeedUpgradingMasterKeyMessage || this.props.showShouldReencryptMessage || this.props.hasDisabledEncryptionItems;
}
+ registerCommands() {
+ for (const command of commands) {
+ CommandService.instance().registerRuntime(command.declaration.name, command.runtime(this));
+ }
+ }
+
+ unregisterCommands() {
+ for (const command of commands) {
+ CommandService.instance().unregisterRuntime(command.declaration.name);
+ }
+ }
+
render() {
const theme = themeStyle(this.props.theme);
const style = Object.assign(
@@ -760,58 +396,18 @@ class MainScreenComponent extends React.Component {
this.props.style,
);
const promptOptions = this.state.promptOptions;
- const folders = this.props.folders;
const notes = this.props.notes;
const sidebarVisibility = this.props.sidebarVisibility;
const noteListVisibility = this.props.noteListVisibility;
const styles = this.styles(this.props.theme, style.width, style.height, this.messageBoxVisible(), sidebarVisibility, noteListVisibility, this.props.sidebarWidth, this.props.noteListWidth);
- const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
const headerItems = [];
- headerItems.push({
- title: _('Toggle sidebar'),
- iconName: 'fa-bars',
- iconRotation: this.props.sidebarVisibility ? 0 : 90,
- onClick: () => {
- this.doCommand({ name: 'toggleSidebar' });
- },
- });
-
- headerItems.push({
- title: _('Toggle note list'),
- iconName: 'fa-align-justify',
- iconRotation: noteListVisibility ? 0 : 90,
- onClick: () => {
- this.doCommand({ name: 'toggleNoteList' });
- },
- });
-
- headerItems.push({
- title: _('New note'),
- iconName: 'fa-file',
- enabled: !!folders.length && !onConflictFolder,
- onClick: () => {
- this.doCommand({ name: 'newNote' });
- },
- });
-
- headerItems.push({
- title: _('New to-do'),
- iconName: 'fa-check-square',
- enabled: !!folders.length && !onConflictFolder,
- onClick: () => {
- this.doCommand({ name: 'newTodo' });
- },
- });
-
- headerItems.push({
- title: _('New notebook'),
- iconName: 'fa-book',
- onClick: () => {
- this.doCommand({ name: 'newNotebook' });
- },
- });
+ headerItems.push(CommandService.instance().commandToToolbarButton('toggleSidebar', { iconRotation: sidebarVisibility ? 0 : 90 }));
+ headerItems.push(CommandService.instance().commandToToolbarButton('toggleNoteList', { iconRotation: noteListVisibility ? 0 : 90 }));
+ headerItems.push(CommandService.instance().commandToToolbarButton('newNote'));
+ headerItems.push(CommandService.instance().commandToToolbarButton('newTodo'));
+ headerItems.push(CommandService.instance().commandToToolbarButton('newNotebook'));
headerItems.push({
title: _('Code View'),
@@ -829,22 +425,13 @@ class MainScreenComponent extends React.Component {
},
});
- if (this.props.settingEditorCodeView) {
- headerItems.push({
- title: _('Layout'),
- iconName: 'fa-columns',
- enabled: !!notes.length,
- onClick: () => {
- this.doCommand({ name: 'toggleVisiblePanes' });
- },
- });
- }
+ headerItems.push(CommandService.instance().commandToToolbarButton('toggleVisiblePanes'));
headerItems.push({
title: _('Search...'),
iconName: 'fa-search',
onQuery: query => {
- this.doCommand({ name: 'search', query: query });
+ CommandService.instance().execute('search', { query });
},
type: 'search',
});
@@ -896,7 +483,6 @@ const mapStateToProps = state => {
return {
theme: state.settings.theme,
settingEditorCodeView: state.settings['editor.codeView'],
- windowCommand: state.windowCommand,
sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility,
folders: state.folders,
diff --git a/ElectronClient/gui/MainScreen/commands/editAlarm.ts b/ElectronClient/gui/MainScreen/commands/editAlarm.ts
new file mode 100644
index 000000000..5a51c7b5d
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/editAlarm.ts
@@ -0,0 +1,73 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Note = require('lib/models/Note');
+const BaseModel = require('lib/BaseModel');
+const { _ } = require('lib/locale');
+const eventManager = require('lib/eventManager');
+const { time } = require('lib/time-utils');
+
+export const declaration:CommandDeclaration = {
+ name: 'editAlarm',
+ label: () => _('Set alarm'),
+ iconName: 'fa-clock',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteId }:any) => {
+ const note = await Note.load(noteId);
+
+ const defaultDate = new Date(Date.now() + 2 * 3600 * 1000);
+ defaultDate.setMinutes(0);
+ defaultDate.setSeconds(0);
+
+ comp.setState({
+ promptOptions: {
+ label: _('Set alarm:'),
+ inputType: 'datetime',
+ buttons: ['ok', 'cancel', 'clear'],
+ value: note.todo_due ? new Date(note.todo_due) : defaultDate,
+ onClose: async (answer:any, buttonType:string) => {
+ let newNote = null;
+
+ if (buttonType === 'clear') {
+ newNote = {
+ id: note.id,
+ todo_due: 0,
+ };
+ } else if (answer !== null) {
+ newNote = {
+ id: note.id,
+ todo_due: answer.getTime(),
+ };
+ }
+
+ if (newNote) {
+ await Note.save(newNote);
+ eventManager.emit('alarmChange', { noteId: note.id, note: newNote });
+ }
+
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ },
+ title: (props:any):string => {
+ const note = BaseModel.byId(props.notes, props.noteId);
+ if (!note || !note.todo_due) return null;
+ return time.formatMsToLocal(note.todo_due);
+ },
+ isEnabled: (props:any):boolean => {
+ const { notes, noteId } = props;
+ if (!noteId) return false;
+ const note = BaseModel.byId(notes, noteId);
+ if (!note) return false;
+ return !!note.is_todo && !note.todo_completed;
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
+ notes: state.notes,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/exportPdf.ts b/ElectronClient/gui/MainScreen/commands/exportPdf.ts
new file mode 100644
index 000000000..94af19f30
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/exportPdf.ts
@@ -0,0 +1,62 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Note = require('lib/models/Note');
+const { _ } = require('lib/locale');
+const { shim } = require('lib/shim');
+const { bridge } = require('electron').remote.require('./bridge');
+const InteropServiceHelper = require('../../../InteropServiceHelper.js');
+
+export const declaration:CommandDeclaration = {
+ name: 'exportPdf',
+ label: () => `PDF - ${_('PDF File')}`,
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteIds }:any) => {
+ try {
+ if (!noteIds.length) throw new Error('No notes selected for pdf export');
+
+ let path = null;
+ if (noteIds.length === 1) {
+ path = bridge().showSaveDialog({
+ filters: [{ name: _('PDF File'), extensions: ['pdf'] }],
+ defaultPath: await InteropServiceHelper.defaultFilename(noteIds[0], 'pdf'),
+ });
+
+ } else {
+ path = bridge().showOpenDialog({
+ properties: ['openDirectory', 'createDirectory'],
+ });
+ }
+
+ if (!path) return;
+
+ for (let i = 0; i < noteIds.length; i++) {
+ const note = await Note.load(noteIds[i]);
+
+ let pdfPath = '';
+
+ if (noteIds.length === 1) {
+ pdfPath = path;
+ } else {
+ const n = await InteropServiceHelper.defaultFilename(note.id, 'pdf');
+ pdfPath = await shim.fsDriver().findUniqueFilename(`${path}/${n}`);
+ }
+
+ await comp.printTo_('pdf', { path: pdfPath, noteId: note.id });
+ }
+ } catch (error) {
+ console.error(error);
+ bridge().showErrorMessageBox(error.message);
+ }
+ },
+ isEnabled: (props:any):boolean => {
+ return !!props.noteIds.length;
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ noteIds: state.selectedNoteIds,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/hideModalMessage.ts b/ElectronClient/gui/MainScreen/commands/hideModalMessage.ts
new file mode 100644
index 000000000..582d97071
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/hideModalMessage.ts
@@ -0,0 +1,13 @@
+import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+
+export const declaration:CommandDeclaration = {
+ name: 'hideModalMessage',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ comp.setState({ modalLayer: { visible: false, message: '' } });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/moveToFolder.ts b/ElectronClient/gui/MainScreen/commands/moveToFolder.ts
new file mode 100644
index 000000000..55fdf4210
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/moveToFolder.ts
@@ -0,0 +1,46 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Folder = require('lib/models/Folder');
+const Note = require('lib/models/Note');
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'moveToFolder',
+ label: () => _('Move to notebook'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteIds }:any) => {
+ const folders:any[] = await Folder.sortFolderTree();
+ const startFolders:any[] = [];
+ const maxDepth = 15;
+
+ const addOptions = (folders:any[], depth:number) => {
+ for (let i = 0; i < folders.length; i++) {
+ const folder = folders[i];
+ startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
+ if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
+ }
+ };
+
+ addOptions(folders, 0);
+
+ comp.setState({
+ promptOptions: {
+ label: _('Move to notebook:'),
+ inputType: 'dropdown',
+ value: '',
+ autocomplete: startFolders,
+ onClose: async (answer:any) => {
+ if (answer != null) {
+ for (let i = 0; i < noteIds.length; i++) {
+ await Note.moveToFolder(noteIds[i], answer.value);
+ }
+ }
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/newNote.ts b/ElectronClient/gui/MainScreen/commands/newNote.ts
new file mode 100644
index 000000000..6467447ec
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/newNote.ts
@@ -0,0 +1,42 @@
+import { utils, CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Setting = require('lib/models/Setting');
+const Note = require('lib/models/Note');
+const Folder = require('lib/models/Folder');
+const TemplateUtils = require('lib/TemplateUtils');
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'newNote',
+ label: () => _('New note'),
+ iconName: 'fa-file',
+};
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async ({ template, isTodo }:any) => {
+ const folderId = Setting.value('activeFolderId');
+ if (!folderId) return;
+
+ const body = template ? TemplateUtils.render(template) : '';
+
+ const defaultValues = Note.previewFieldsWithDefaultValues({ includeTimestamps: false });
+
+ let newNote = Object.assign({}, defaultValues, {
+ parent_id: folderId,
+ is_todo: isTodo ? 1 : 0,
+ body: body,
+ });
+
+ newNote = await Note.save(newNote, { provisional: true });
+
+ utils.store.dispatch({
+ type: 'NOTE_SELECT',
+ id: newNote.id,
+ });
+ },
+ isEnabled: () => {
+ const { folders, selectedFolderId } = utils.store.getState();
+ return !!folders.length && selectedFolderId !== Folder.conflictFolderId();
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/newNotebook.ts b/ElectronClient/gui/MainScreen/commands/newNotebook.ts
new file mode 100644
index 000000000..bca3296ba
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/newNotebook.ts
@@ -0,0 +1,49 @@
+import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+const Folder = require('lib/models/Folder');
+const { bridge } = require('electron').remote.require('./bridge');
+
+export const declaration:CommandDeclaration = {
+ name: 'newNotebook',
+ label: () => _('New notebook'),
+ iconName: 'fa-book',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ parentId }:any) => {
+ comp.setState({
+ promptOptions: {
+ label: _('Notebook title:'),
+ onClose: async (answer:string) => {
+ if (answer) {
+ let folder = null;
+ try {
+ const toSave:any = { title: answer };
+ if (parentId) toSave.parent_id = parentId;
+ folder = await Folder.save(toSave, { userSideValidation: true });
+ } catch (error) {
+ bridge().showErrorMessageBox(error.message);
+ }
+
+ if (folder) {
+ comp.props.dispatch({
+ type: 'FOLDER_SELECT',
+ id: folder.id,
+ });
+ }
+ }
+
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ selectedNoteIds: state.selectedNoteIds,
+ notes: state.notes,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/newTodo.ts b/ElectronClient/gui/MainScreen/commands/newTodo.ts
new file mode 100644
index 000000000..143ac16e2
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/newTodo.ts
@@ -0,0 +1,19 @@
+import CommandService, { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'newTodo',
+ label: () => _('New to-do'),
+ iconName: 'fa-check-square',
+};
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async ({ template }:any) => {
+ return CommandService.instance().execute('newNote', { template: template, isTodo: true });
+ },
+ isEnabled: () => {
+ return CommandService.instance().isEnabled('newNote');
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/print.ts b/ElectronClient/gui/MainScreen/commands/print.ts
new file mode 100644
index 000000000..5ab98e987
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/print.ts
@@ -0,0 +1,31 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+const { bridge } = require('electron').remote.require('./bridge');
+
+export const declaration:CommandDeclaration = {
+ name: 'print',
+ label: () => _('Print'),
+ iconName: 'fa-file',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteIds }:any) => {
+ // TODO: test
+ try {
+ if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.'));
+ await comp.printTo_('printer', { noteId: noteIds[0] });
+ } catch (error) {
+ bridge().showErrorMessageBox(error.message);
+ }
+ },
+ isEnabled: (props:any):boolean => {
+ return !!props.noteIds.length;
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ noteIds: state.selectedNoteIds,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/renameFolder.ts b/ElectronClient/gui/MainScreen/commands/renameFolder.ts
new file mode 100644
index 000000000..28e25d075
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/renameFolder.ts
@@ -0,0 +1,37 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Folder = require('lib/models/Folder');
+const { _ } = require('lib/locale');
+const { bridge } = require('electron').remote.require('./bridge');
+
+export const declaration:CommandDeclaration = {
+ name: 'renameFolder',
+ label: () => _('Rename'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ folderId }:any) => {
+ const folder = await Folder.load(folderId);
+
+ if (folder) {
+ comp.setState({
+ promptOptions: {
+ label: _('Rename notebook:'),
+ value: folder.title,
+ onClose: async (answer:string) => {
+ if (answer !== null) {
+ try {
+ folder.title = answer;
+ await Folder.save(folder, { fields: ['title'], userSideValidation: true });
+ } catch (error) {
+ bridge().showErrorMessageBox(error.message);
+ }
+ }
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ }
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/renameTag.ts b/ElectronClient/gui/MainScreen/commands/renameTag.ts
new file mode 100644
index 000000000..52d55a5de
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/renameTag.ts
@@ -0,0 +1,36 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Tag = require('lib/models/Tag');
+const { _ } = require('lib/locale');
+const { bridge } = require('electron').remote.require('./bridge');
+
+export const declaration:CommandDeclaration = {
+ name: 'renameTag',
+ label: () => _('Rename'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ tagId }:any) => {
+ const tag = await Tag.load(tagId);
+ if (tag) {
+ comp.setState({
+ promptOptions: {
+ label: _('Rename tag:'),
+ value: tag.title,
+ onClose: async (answer:string) => {
+ if (answer !== null) {
+ try {
+ tag.title = answer;
+ await Tag.save(tag, { fields: ['title'], userSideValidation: true });
+ } catch (error) {
+ bridge().showErrorMessageBox(error.message);
+ }
+ }
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ }
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/search.ts b/ElectronClient/gui/MainScreen/commands/search.ts
new file mode 100644
index 000000000..dfe7f3947
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/search.ts
@@ -0,0 +1,46 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Note = require('lib/models/Note');
+const BaseModel = require('lib/BaseModel');
+// const { _ } = require('lib/locale');
+const { uuid } = require('lib/uuid.js');
+
+export const declaration:CommandDeclaration = {
+ name: 'search',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ query }:any) => {
+ console.info('RUNTIME', query);
+
+ if (!comp.searchId_) comp.searchId_ = uuid.create();
+
+ comp.props.dispatch({
+ type: 'SEARCH_UPDATE',
+ search: {
+ id: comp.searchId_,
+ title: query,
+ query_pattern: query,
+ query_folder_id: null,
+ type_: BaseModel.TYPE_SEARCH,
+ },
+ });
+
+ if (query) {
+ comp.props.dispatch({
+ type: 'SEARCH_SELECT',
+ id: comp.searchId_,
+ });
+ } else {
+ const note = await Note.load(comp.props.selectedNoteId);
+ if (note) {
+ comp.props.dispatch({
+ type: 'FOLDER_AND_NOTE_SELECT',
+ folderId: note.parent_id,
+ noteId: note.id,
+ });
+ }
+ }
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/selectTemplate.ts b/ElectronClient/gui/MainScreen/commands/selectTemplate.ts
new file mode 100644
index 000000000..7647ac80a
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/selectTemplate.ts
@@ -0,0 +1,33 @@
+import CommandService, { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+const TemplateUtils = require('lib/TemplateUtils');
+
+export const declaration:CommandDeclaration = {
+ name: 'selectTemplate',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteType }:any) => {
+ comp.setState({
+ promptOptions: {
+ label: _('Template file:'),
+ inputType: 'dropdown',
+ value: comp.props.templates[0], // Need to start with some value
+ autocomplete: comp.props.templates,
+ onClose: async (answer:any) => {
+ if (answer) {
+ if (noteType === 'note' || noteType === 'todo') {
+ CommandService.instance().execute('newNote', { template: answer.value, isTodo: noteType === 'todo' });
+ } else {
+ CommandService.instance().execute('insertText', { value: TemplateUtils.render(answer.value) });
+ }
+ }
+
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/setTags.ts b/ElectronClient/gui/MainScreen/commands/setTags.ts
new file mode 100644
index 000000000..d2d5cbda6
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/setTags.ts
@@ -0,0 +1,74 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Tag = require('lib/models/Tag');
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'setTags',
+ label: () => _('Tags'),
+ iconName: 'fa-tags',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteIds }:any) => {
+ const tags = await Tag.commonTagsByNoteIds(noteIds);
+ const startTags = tags
+ .map((a:any) => {
+ return { value: a.id, label: a.title };
+ })
+ .sort((a:any, b:any) => {
+ // sensitivity accent will treat accented characters as differemt
+ // but treats caps as equal
+ return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
+ });
+ const allTags = await Tag.allWithNotes();
+ const tagSuggestions = allTags.map((a:any) => {
+ return { value: a.id, label: a.title };
+ })
+ .sort((a:any, b:any) => {
+ // sensitivity accent will treat accented characters as differemt
+ // but treats caps as equal
+ return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
+ });
+
+ comp.setState({
+ promptOptions: {
+ label: _('Add or remove tags:'),
+ inputType: 'tags',
+ value: startTags,
+ autocomplete: tagSuggestions,
+ onClose: async (answer:any[]) => {
+ if (answer !== null) {
+ const endTagTitles = answer.map(a => {
+ return a.label.trim();
+ });
+ if (noteIds.length === 1) {
+ await Tag.setNoteTagsByTitles(noteIds[0], endTagTitles);
+ } else {
+ const startTagTitles = startTags.map((a:any) => { return a.label.trim(); });
+ const addTags = endTagTitles.filter((value:string) => !startTagTitles.includes(value));
+ const delTags = startTagTitles.filter((value:string) => !endTagTitles.includes(value));
+
+ // apply the tag additions and deletions to each selected note
+ for (let i = 0; i < noteIds.length; i++) {
+ const tags = await Tag.tagsByNoteId(noteIds[i]);
+ let tagTitles = tags.map((a:any) => { return a.title; });
+ tagTitles = tagTitles.concat(addTags);
+ tagTitles = tagTitles.filter((value:string) => !delTags.includes(value));
+ await Tag.setNoteTagsByTitles(noteIds[i], tagTitles);
+ }
+ }
+ }
+ comp.setState({ promptOptions: null });
+ },
+ },
+ });
+ },
+ isEnabled: (props:any) => {
+ return !!props.noteIds.length;
+ },
+ mapStateToProps: (state:any) => {
+ return { noteIds: state.selectedNoteIds };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/showModalMessage.tsx b/ElectronClient/gui/MainScreen/commands/showModalMessage.tsx
new file mode 100644
index 000000000..1bcdaeeaa
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/showModalMessage.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+
+export const declaration:CommandDeclaration = {
+ name: 'showModalMessage',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ message }:any) => {
+ comp.setState({
+ modalLayer: {
+ visible: true,
+ message:
+ ,
+ },
+ });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/showNoteContentProperties.ts b/ElectronClient/gui/MainScreen/commands/showNoteContentProperties.ts
new file mode 100644
index 000000000..f63f2e521
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/showNoteContentProperties.ts
@@ -0,0 +1,30 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const Note = require('lib/models/Note');
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'showNoteContentProperties',
+ label: () => _('Statistics...'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteId }:any) => {
+ const note = await Note.load(noteId);
+ if (note) {
+ comp.setState({
+ noteContentPropertiesDialogOptions: {
+ visible: true,
+ text: note.body,
+ },
+ });
+ }
+ },
+ isEnabled: (props:any) => {
+ return !!props.noteId;
+ },
+ mapStateToProps: (state:any) => {
+ return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts b/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts
new file mode 100644
index 000000000..4ad44e42f
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/showNoteProperties.ts
@@ -0,0 +1,30 @@
+import CommandService, { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'showNoteProperties',
+ label: () => _('Note properties'),
+ iconName: 'fa-info-circle',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteId }:any) => {
+ comp.setState({
+ notePropertiesDialogOptions: {
+ noteId: noteId,
+ visible: true,
+ onRevisionLinkClick: () => {
+ CommandService.instance().execute('showRevisions');
+ },
+ },
+ });
+ },
+ isEnabled: (props:any) => {
+ return !!props.noteId;
+ },
+ mapStateToProps: (state:any) => {
+ return { noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null };
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/showShareNoteDialog.ts b/ElectronClient/gui/MainScreen/commands/showShareNoteDialog.ts
new file mode 100644
index 000000000..83889e756
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/showShareNoteDialog.ts
@@ -0,0 +1,20 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'showShareNoteDialog',
+ label: () => _('Share note...'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ noteIds }:any) => {
+ comp.setState({
+ shareNoteDialogOptions: {
+ noteIds: noteIds,
+ visible: true,
+ },
+ });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/toggleNoteList.ts b/ElectronClient/gui/MainScreen/commands/toggleNoteList.ts
new file mode 100644
index 000000000..df21c4d45
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/toggleNoteList.ts
@@ -0,0 +1,18 @@
+import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'toggleNoteList',
+ label: () => _('Toggle note list'),
+ iconName: 'fa-align-justify',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ comp.props.dispatch({
+ type: 'NOTELIST_VISIBILITY_TOGGLE',
+ });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/toggleSidebar.ts b/ElectronClient/gui/MainScreen/commands/toggleSidebar.ts
new file mode 100644
index 000000000..7299e4dca
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/toggleSidebar.ts
@@ -0,0 +1,18 @@
+import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'toggleSidebar',
+ label: () => _('Toggle sidebar'),
+ iconName: 'fa-bars',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ comp.props.dispatch({
+ type: 'SIDEBAR_VISIBILITY_TOGGLE',
+ });
+ },
+ };
+};
diff --git a/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts b/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts
new file mode 100644
index 000000000..24f2d3446
--- /dev/null
+++ b/ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.ts
@@ -0,0 +1,27 @@
+import { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'toggleVisiblePanes',
+ label: () => _('Toggle editor layout'),
+ iconName: 'fa-columns',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ comp.props.dispatch({
+ 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'],
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.tsx b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.tsx
index d9824ce2b..79d113d1f 100644
--- a/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.tsx
+++ b/ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
+import CommandService from '../../../../lib/services/CommandService';
const ToolbarBase = require('../../../Toolbar.min.js');
-const { _ } = require('lib/locale');
const { buildStyle, themeStyle } = require('lib/theme');
interface ToolbarProps {
@@ -26,144 +26,23 @@ function styles_(props:ToolbarProps) {
export default function Toolbar(props:ToolbarProps) {
const styles = styles_(props);
- function createToolbarItems() {
- const toolbarItems = [];
+ const cmdService = CommandService.instance();
- toolbarItems.push({
- tooltip: _('Bold'),
- iconName: 'fa-bold',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textBold',
- });
- },
- });
+ const toolbarItems = [
+ cmdService.commandToToolbarButton('textBold'),
+ cmdService.commandToToolbarButton('textItalic'),
+ { type: 'separator' },
+ cmdService.commandToToolbarButton('textLink'),
+ cmdService.commandToToolbarButton('textCode'),
+ cmdService.commandToToolbarButton('attachFile'),
+ { type: 'separator' },
+ cmdService.commandToToolbarButton('textNumberedList'),
+ cmdService.commandToToolbarButton('textBulletedList'),
+ cmdService.commandToToolbarButton('textCheckbox'),
+ cmdService.commandToToolbarButton('textHeading'),
+ cmdService.commandToToolbarButton('textHorizontalRule'),
+ cmdService.commandToToolbarButton('insertDateTime'),
+ ];
- toolbarItems.push({
- tooltip: _('Italic'),
- iconName: 'fa-italic',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textItalic',
- });
- },
- });
-
- toolbarItems.push({
- type: 'separator',
- });
-
- toolbarItems.push({
- tooltip: _('Hyperlink'),
- iconName: 'fa-link',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textLink',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Code'),
- iconName: 'fa-code',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textCode',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Attach file'),
- iconName: 'fa-paperclip',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'attachFile',
- });
- },
- });
-
- toolbarItems.push({
- type: 'separator',
- });
-
- toolbarItems.push({
- tooltip: _('Numbered List'),
- iconName: 'fa-list-ol',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textNumberedList',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Bulleted List'),
- iconName: 'fa-list-ul',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textBulletedList',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Checkbox'),
- iconName: 'fa-check-square',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textCheckbox',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Heading'),
- iconName: 'fa-heading',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textHeading',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Horizontal Rule'),
- iconName: 'fa-ellipsis-h',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textHorizontalRule',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Insert Date Time'),
- iconName: 'fa-calendar-plus',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'insertDateTime',
- });
- },
- });
-
- toolbarItems.push({
- type: 'separator',
- });
-
- return toolbarItems;
- }
-
- return ;
+ return ;
}
diff --git a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx
index d9824ce2b..79d113d1f 100644
--- a/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx
+++ b/ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
+import CommandService from '../../../../lib/services/CommandService';
const ToolbarBase = require('../../../Toolbar.min.js');
-const { _ } = require('lib/locale');
const { buildStyle, themeStyle } = require('lib/theme');
interface ToolbarProps {
@@ -26,144 +26,23 @@ function styles_(props:ToolbarProps) {
export default function Toolbar(props:ToolbarProps) {
const styles = styles_(props);
- function createToolbarItems() {
- const toolbarItems = [];
+ const cmdService = CommandService.instance();
- toolbarItems.push({
- tooltip: _('Bold'),
- iconName: 'fa-bold',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textBold',
- });
- },
- });
+ const toolbarItems = [
+ cmdService.commandToToolbarButton('textBold'),
+ cmdService.commandToToolbarButton('textItalic'),
+ { type: 'separator' },
+ cmdService.commandToToolbarButton('textLink'),
+ cmdService.commandToToolbarButton('textCode'),
+ cmdService.commandToToolbarButton('attachFile'),
+ { type: 'separator' },
+ cmdService.commandToToolbarButton('textNumberedList'),
+ cmdService.commandToToolbarButton('textBulletedList'),
+ cmdService.commandToToolbarButton('textCheckbox'),
+ cmdService.commandToToolbarButton('textHeading'),
+ cmdService.commandToToolbarButton('textHorizontalRule'),
+ cmdService.commandToToolbarButton('insertDateTime'),
+ ];
- toolbarItems.push({
- tooltip: _('Italic'),
- iconName: 'fa-italic',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textItalic',
- });
- },
- });
-
- toolbarItems.push({
- type: 'separator',
- });
-
- toolbarItems.push({
- tooltip: _('Hyperlink'),
- iconName: 'fa-link',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textLink',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Code'),
- iconName: 'fa-code',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textCode',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Attach file'),
- iconName: 'fa-paperclip',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'attachFile',
- });
- },
- });
-
- toolbarItems.push({
- type: 'separator',
- });
-
- toolbarItems.push({
- tooltip: _('Numbered List'),
- iconName: 'fa-list-ol',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textNumberedList',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Bulleted List'),
- iconName: 'fa-list-ul',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textBulletedList',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Checkbox'),
- iconName: 'fa-check-square',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textCheckbox',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Heading'),
- iconName: 'fa-heading',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textHeading',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Horizontal Rule'),
- iconName: 'fa-ellipsis-h',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'textHorizontalRule',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Insert Date Time'),
- iconName: 'fa-calendar-plus',
- onClick: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'insertDateTime',
- });
- },
- });
-
- toolbarItems.push({
- type: 'separator',
- });
-
- return toolbarItems;
- }
-
- return ;
+ return ;
}
diff --git a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
index 8d46027c6..604eaa074 100644
--- a/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
+++ b/ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx
@@ -4,6 +4,7 @@ import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps }
import { resourcesStatus, commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
import useScroll from './utils/useScroll';
import { menuItems, ContextMenuOptions, ContextMenuItemType } from '../../utils/contextMenu';
+import CommandService from '../../../../lib/services/CommandService';
const { MarkupToHtml } = require('lib/joplin-renderer');
const taboverride = require('taboverride');
const { reg } = require('lib/registry.js');
@@ -604,10 +605,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
tooltip: _('Insert Date Time'),
icon: 'insert-time',
onAction: function() {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'insertDateTime',
- });
+ CommandService.instance().execute('insertDateTime');
},
});
diff --git a/ElectronClient/gui/NoteEditor/NoteEditor.tsx b/ElectronClient/gui/NoteEditor/NoteEditor.tsx
index 1c800eac3..23e3fe1d5 100644
--- a/ElectronClient/gui/NoteEditor/NoteEditor.tsx
+++ b/ElectronClient/gui/NoteEditor/NoteEditor.tsx
@@ -18,6 +18,7 @@ import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
import ResourceEditWatcher from '../../lib/services/ResourceEditWatcher';
+import CommandService from '../../lib/services/CommandService';
const { themeStyle } = require('lib/theme');
const NoteSearchBar = require('../NoteSearchBar.min.js');
@@ -30,10 +31,14 @@ const { _ } = require('lib/locale');
const Note = require('lib/models/Note.js');
const { bridge } = require('electron').remote.require('./bridge');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
-const eventManager = require('../../eventManager');
+const eventManager = require('lib/eventManager');
const NoteRevisionViewer = require('../NoteRevisionViewer.min');
const TagList = require('../TagList.min.js');
+const commands = [
+ require('./commands/showRevisions'),
+];
+
function NoteEditor(props: NoteEditorProps) {
const [showRevisions, setShowRevisions] = useState(false);
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
@@ -222,7 +227,7 @@ function NoteEditor(props: NoteEditorProps) {
}
}, [handleProvisionalFlag, formNote, isNewNote, titleHasBeenManuallyChanged]);
- useWindowCommandHandler({ windowCommand: props.windowCommand, dispatch: props.dispatch, formNote, setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, saveNoteAndWait });
+ useWindowCommandHandler({ dispatch: props.dispatch, formNote, setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, saveNoteAndWait });
const onDrop = useDropHandler({ editorRef });
@@ -238,17 +243,9 @@ function NoteEditor(props: NoteEditorProps) {
event.preventDefault();
if (event.shiftKey) {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'noteList',
- });
+ CommandService.instance().execute('focusElement', { target: 'noteList' });
} else {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'noteBody',
- });
+ CommandService.instance().execute('focusElement', { target: 'noteBody' });
}
}
}, [props.dispatch]);
@@ -314,52 +311,17 @@ function NoteEditor(props: NoteEditorProps) {
};
}, [externalEditWatcher_noteChange, onNotePropertyChange]);
- const noteToolbar_buttonClick = useCallback((event: any) => {
- const cases: any = {
-
- 'startExternalEditing': async () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'commandStartExternalEditing',
- });
- },
-
- 'stopExternalEditing': () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'commandStopExternalEditing',
- });
- },
-
- 'setTags': async () => {
- await saveNoteAndWait(formNote);
-
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'setTags',
- noteIds: [formNote.id],
- });
- },
-
- 'setAlarm': async () => {
- await saveNoteAndWait(formNote);
-
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'editAlarm',
- noteId: formNote.id,
- });
- },
-
- 'showRevisions': () => {
- setShowRevisions(true);
- },
+ useEffect(() => {
+ const dependencies = {
+ setShowRevisions,
};
- if (!cases[event.name]) throw new Error(`Unsupported event: ${event.name}`);
+ CommandService.instance().componentRegisterCommands(dependencies, commands);
- cases[event.name]();
- }, [formNote]);
+ return () => {
+ CommandService.instance().componentUnregisterCommands(commands);
+ };
+ }, [setShowRevisions]);
const onScroll = useCallback((event: any) => {
props.dispatch({
@@ -389,7 +351,6 @@ function NoteEditor(props: NoteEditorProps) {
theme={props.theme}
note={formNote}
style={toolbarStyle}
- onButtonClick={noteToolbar_buttonClick}
/>;
}
@@ -474,7 +435,6 @@ function NoteEditor(props: NoteEditorProps) {
padding: theme.margin,
verticalAlign: 'top',
boxSizing: 'border-box',
-
};
return (
@@ -560,7 +520,6 @@ const mapStateToProps = (state: any) => {
syncStarted: state.syncStarted,
theme: state.settings.theme,
watchedNoteFiles: state.watchedNoteFiles,
- windowCommand: state.windowCommand,
notesParentType: state.notesParentType,
selectedNoteTags: state.selectedNoteTags,
lastEditorScrollPercents: state.lastEditorScrollPercents,
diff --git a/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts b/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts
new file mode 100644
index 000000000..5d2e3b919
--- /dev/null
+++ b/ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.ts
@@ -0,0 +1,85 @@
+import { CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+const declarations:CommandDeclaration[] = [
+ {
+ name: 'insertText',
+ },
+ {
+ name: 'textCopy',
+ label: () => _('Copy'),
+ role: 'copy',
+ },
+ {
+ name: 'textCut',
+ label: () => _('Cut'),
+ role: 'cut',
+ },
+ {
+ name: 'textPaste',
+ label: () => _('Paste'),
+ role: 'paste',
+ },
+ {
+ name: 'textSelectAll',
+ label: () => _('Select all'),
+ role: 'selectAll',
+ },
+ {
+ name: 'textBold',
+ label: () => _('Bold'),
+ iconName: 'fa-bold',
+ },
+ {
+ name: 'textItalic',
+ label: () => _('Italic'),
+ iconName: 'fa-italic',
+ },
+ {
+ name: 'textLink',
+ label: () => _('Hyperlink'),
+ iconName: 'fa-link',
+ },
+ {
+ name: 'textCode',
+ label: () => _('Code'),
+ iconName: 'fa-code',
+ },
+ {
+ name: 'attachFile',
+ label: () => _('Attach file'),
+ iconName: 'fa-paperclip',
+ },
+ {
+ name: 'textNumberedList',
+ label: () => _('Numbered List'),
+ iconName: 'fa-list-ol',
+ },
+ {
+ name: 'textBulletedList',
+ label: () => _('Bulleted List'),
+ iconName: 'fa-list-ul',
+ },
+ {
+ name: 'textCheckbox',
+ label: () => _('Checkbox'),
+ iconName: 'fa-check-square',
+ },
+ {
+ name: 'textHeading',
+ label: () => _('Heading'),
+ iconName: 'fa-heading',
+ },
+ {
+ name: 'textHorizontalRule',
+ label: () => _('Horizontal Rule'),
+ iconName: 'fa-ellipsis-h',
+ },
+ {
+ name: 'insertDateTime',
+ label: () => _('Insert Date Time'),
+ iconName: 'fa-calendar-plus',
+ },
+];
+
+export default declarations;
diff --git a/ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.ts b/ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.ts
new file mode 100644
index 000000000..9bec4f2a4
--- /dev/null
+++ b/ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.ts
@@ -0,0 +1,23 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'focusElementNoteBody',
+ label: () => _('Note body'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ comp.editorRef.current.execCommand({ name: 'focus' });
+ },
+ // isEnabled: (props:any):boolean => {
+ // return props.sidebarVisibility;
+ // },
+ // mapStateToProps: (state:any):any => {
+ // return {
+ // sidebarVisibility: state.sidebarVisibility,
+ // };
+ // },
+ };
+};
diff --git a/ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.ts b/ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.ts
new file mode 100644
index 000000000..3c3d3048b
--- /dev/null
+++ b/ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.ts
@@ -0,0 +1,19 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'focusElementNoteTitle',
+ label: () => _('Note title'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ if (!comp.titleInputRef.current) return;
+ comp.titleInputRef.current.focus();
+ },
+ isEnabled: ():boolean => {
+ return !!comp.titleInputRef.current;
+ },
+ };
+};
diff --git a/ElectronClient/gui/NoteEditor/commands/showLocalSearch.ts b/ElectronClient/gui/NoteEditor/commands/showLocalSearch.ts
new file mode 100644
index 000000000..7a64e8b23
--- /dev/null
+++ b/ElectronClient/gui/NoteEditor/commands/showLocalSearch.ts
@@ -0,0 +1,26 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'showLocalSearch',
+ label: () => _('Search in current note'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ if (comp.editorRef.current && comp.editorRef.current.supportsCommand('search')) {
+ comp.editorRef.current.execCommand({ name: 'search' });
+ } else {
+ comp.setShowLocalSearch(true);
+ 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 };
+ },
+ };
+};
diff --git a/ElectronClient/gui/NoteEditor/commands/showRevisions.ts b/ElectronClient/gui/NoteEditor/commands/showRevisions.ts
new file mode 100644
index 000000000..a12ede5ec
--- /dev/null
+++ b/ElectronClient/gui/NoteEditor/commands/showRevisions.ts
@@ -0,0 +1,13 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+
+export const declaration:CommandDeclaration = {
+ name: 'showRevisions',
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async () => {
+ comp.setShowRevisions(true);
+ },
+ };
+};
diff --git a/ElectronClient/gui/NoteEditor/utils/types.ts b/ElectronClient/gui/NoteEditor/utils/types.ts
index 9d65bb141..956a9b1f2 100644
--- a/ElectronClient/gui/NoteEditor/utils/types.ts
+++ b/ElectronClient/gui/NoteEditor/utils/types.ts
@@ -13,7 +13,6 @@ export interface NoteEditorProps {
editorNoteStatuses: any;
syncStarted: boolean;
bodyEditor: string;
- windowCommand: any;
folders: any[];
notesParentType: string;
selectedNoteTags: any[];
diff --git a/ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.ts b/ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.ts
index 0f1e2ee1f..e19418a6f 100644
--- a/ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.ts
+++ b/ElectronClient/gui/NoteEditor/utils/useWindowCommandHandler.ts
@@ -1,12 +1,19 @@
import { useEffect } from 'react';
-import { FormNote, EditorCommand } from './types';
+import { FormNote } from './types';
+import editorCommandDeclarations from '../commands/editorCommandDeclarations';
+import CommandService, { CommandDeclaration, CommandRuntime } from '../../../lib/services/CommandService';
const { time } = require('lib/time-utils.js');
+const BaseModel = require('lib/BaseModel');
const { reg } = require('lib/registry.js');
-const NoteListUtils = require('../../utils/NoteListUtils');
-const TemplateUtils = require('lib/TemplateUtils');
+const { MarkupToHtml } = require('lib/joplin-renderer');
+
+const commandsWithDependencies = [
+ require('../commands/showLocalSearch'),
+ require('../commands/focusElementNoteTitle'),
+ require('../commands/focusElementNoteBody'),
+];
interface HookDependencies {
- windowCommand: any,
formNote:FormNote,
setShowLocalSearch:Function,
dispatch:Function,
@@ -16,96 +23,73 @@ interface HookDependencies {
saveNoteAndWait: Function,
}
+function editorCommandRuntime(declaration:CommandDeclaration, editorRef:any):CommandRuntime {
+ return {
+ execute: (props:any) => {
+ console.info('Running editor command:', declaration.name, props);
+ if (!editorRef.current.execCommand) {
+ reg.logger().warn('Received command, but editor cannot execute commands', declaration.name);
+ } else {
+ const execArgs = {
+ name: declaration.name,
+ value: props.value,
+ };
+
+ if (declaration.name === 'insertDateTime') {
+ execArgs.name = 'insertText';
+ execArgs.value = time.formatMsToLocal(new Date().getTime());
+ }
+
+ editorRef.current.execCommand(execArgs);
+ }
+ },
+ isEnabled: (props:any) => {
+ if (props.markdownEditorViewerOnly) return false;
+ if (!props.noteId) return false;
+ const note = BaseModel.byId(props.notes, props.noteId);
+ if (!note) return false;
+ return note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
+ },
+ mapStateToProps: (state:any) => {
+ 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',
+ noteVisiblePanes: state.noteVisiblePanes,
+ notes: state.notes,
+ noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
+ };
+ },
+ };
+}
+
export default function useWindowCommandHandler(dependencies:HookDependencies) {
- const { windowCommand, dispatch, formNote, setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, saveNoteAndWait } = dependencies;
+ const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef } = dependencies;
useEffect(() => {
- async function processCommand() {
- const command = windowCommand;
-
- if (!command || !formNote) return;
-
- reg.logger().debug('NoteEditor::useWindowCommandHandler:', command);
-
- const editorCmd: EditorCommand = { name: '', value: command.value };
- let fn: Function = null;
-
- // These commands can be forwarded directly to the note body editor
- // without transformation.
- const directMapCommands = [
- 'textCode',
- 'textBold',
- 'textItalic',
- 'textLink',
- 'attachFile',
- 'textNumberedList',
- 'textBulletedList',
- 'textCheckbox',
- 'textHeading',
- 'textHorizontalRule',
- ];
-
- if (directMapCommands.includes(command.name)) {
- editorCmd.name = command.name;
- } else if (command.name === 'commandStartExternalEditing') {
- fn = async () => {
- await saveNoteAndWait(formNote);
- NoteListUtils.startExternalEditing(formNote.id);
- };
- } else if (command.name === 'commandStopExternalEditing') {
- fn = () => {
- NoteListUtils.stopExternalEditing(formNote.id);
- };
- } else if (command.name === 'insertDateTime') {
- editorCmd.name = 'insertText',
- editorCmd.value = time.formatMsToLocal(new Date().getTime());
- } else if (command.name === 'showLocalSearch') {
- if (editorRef.current && editorRef.current.supportsCommand('search')) {
- editorCmd.name = 'search';
- } else {
- fn = () => {
- setShowLocalSearch(true);
- if (noteSearchBarRef.current) noteSearchBarRef.current.wrappedInstance.focus();
- };
- }
- } else if (command.name === 'insertTemplate') {
- editorCmd.name = 'insertText';
- editorCmd.value = TemplateUtils.render(command.value);
- }
-
- if (command.name === 'focusElement' && command.target === 'noteTitle') {
- fn = () => {
- if (!titleInputRef.current) return;
- titleInputRef.current.focus();
- };
- }
-
- if (command.name === 'focusElement' && command.target === 'noteBody') {
- editorCmd.name = 'focus';
- }
-
- reg.logger().debug('NoteEditor::useWindowCommandHandler: Dispatch:', editorCmd, fn);
-
- if (!editorCmd.name && !fn) return;
-
- dispatch({
- type: 'WINDOW_COMMAND',
- name: null,
- });
-
- requestAnimationFrame(() => {
- if (fn) {
- fn();
- } else {
- if (!editorRef.current.execCommand) {
- reg.logger().warn('Received command, but editor cannot execute commands', editorCmd);
- } else {
- editorRef.current.execCommand(editorCmd);
- }
- }
- });
+ for (const declaration of editorCommandDeclarations) {
+ CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef));
}
- processCommand();
- }, [windowCommand, dispatch, formNote, saveNoteAndWait]);
+ const dependencies = {
+ editorRef,
+ setShowLocalSearch,
+ noteSearchBarRef,
+ titleInputRef,
+ };
+
+ for (const command of commandsWithDependencies) {
+ CommandService.instance().registerRuntime(command.declaration.name, command.runtime(dependencies));
+ }
+
+ return () => {
+ for (const declaration of editorCommandDeclarations) {
+ CommandService.instance().unregisterRuntime(declaration.name);
+ }
+
+ for (const command of commandsWithDependencies) {
+ CommandService.instance().unregisterRuntime(command.declaration.name);
+ }
+ };
+ }, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef]);
}
diff --git a/ElectronClient/gui/NoteList.jsx b/ElectronClient/gui/NoteList/NoteList.jsx
similarity index 92%
rename from ElectronClient/gui/NoteList.jsx
rename to ElectronClient/gui/NoteList/NoteList.jsx
index df3aaa638..d1ee63779 100644
--- a/ElectronClient/gui/NoteList.jsx
+++ b/ElectronClient/gui/NoteList/NoteList.jsx
@@ -1,4 +1,4 @@
-const { ItemList } = require('./ItemList.min.js');
+const { ItemList } = require('../ItemList.min.js');
const React = require('react');
const { connect } = require('react-redux');
const { time } = require('lib/time-utils.js');
@@ -6,17 +6,24 @@ const { themeStyle } = require('lib/theme');
const BaseModel = require('lib/BaseModel');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
-const eventManager = require('../eventManager');
+const eventManager = require('lib/eventManager');
const SearchEngine = require('lib/services/SearchEngine');
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting');
-const NoteListUtils = require('./utils/NoteListUtils');
-const NoteListItem = require('./NoteListItem').default;
+const NoteListUtils = require('../utils/NoteListUtils');
+const NoteListItem = require('../NoteListItem').default;
+const CommandService = require('lib/services/CommandService.js').default;
+
+const commands = [
+ require('./commands/focusElementNoteList'),
+];
class NoteListComponent extends React.Component {
constructor() {
super();
+ CommandService.instance().componentRegisterCommands(this, commands);
+
this.itemHeight = 34;
this.state = {
@@ -260,33 +267,7 @@ class NoteListComponent extends React.Component {
return null;
}
- doCommand(command) {
- if (!command) return;
-
- let commandProcessed = true;
-
- if (command.name === 'focusElement' && command.target === 'noteList') {
- if (this.props.selectedNoteIds.length) {
- const ref = this.itemAnchorRef(this.props.selectedNoteIds[0]);
- if (ref) ref.focus();
- }
- } else {
- commandProcessed = false;
- }
-
- if (commandProcessed) {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: null,
- });
- }
- }
-
componentDidUpdate(prevProps) {
- if (prevProps.windowCommand !== this.props.windowCommand) {
- this.doCommand(this.props.windowCommand);
- }
-
if (prevProps.selectedNoteIds !== this.props.selectedNoteIds && this.props.selectedNoteIds.length === 1) {
const id = this.props.selectedNoteIds[0];
const doRefocus = this.props.notes.length < prevProps.notes.length;
@@ -387,17 +368,9 @@ class NoteListComponent extends React.Component {
event.preventDefault();
if (event.shiftKey) {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'sideBar',
- });
+ CommandService.instance().execute('focusElement', { target: 'sideBar' });
} else {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'noteTitle',
- });
+ CommandService.instance().execute('focusElement', { target: 'noteTitle' });
}
}
@@ -435,6 +408,8 @@ class NoteListComponent extends React.Component {
clearInterval(this.focusItemIID_);
this.focusItemIID_ = null;
}
+
+ CommandService.instance().componentUnregisterCommands(commands);
}
render() {
@@ -482,7 +457,6 @@ const mapStateToProps = state => {
searches: state.searches,
selectedSearchId: state.selectedSearchId,
watchedNoteFiles: state.watchedNoteFiles,
- windowCommand: state.windowCommand,
provisionalNoteIds: state.provisionalNoteIds,
isInsertingNotes: state.isInsertingNotes,
noteSortOrder: state.settings['notes.sortOrder.field'],
diff --git a/ElectronClient/gui/NoteList/commands/focusElementNoteList.ts b/ElectronClient/gui/NoteList/commands/focusElementNoteList.ts
new file mode 100644
index 000000000..1e8dfa1cf
--- /dev/null
+++ b/ElectronClient/gui/NoteList/commands/focusElementNoteList.ts
@@ -0,0 +1,26 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'focusElementNoteList',
+ label: () => _('Note list'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ selectedNoteIds }:any) => {
+ if (selectedNoteIds.length) {
+ const ref = comp.itemAnchorRef(selectedNoteIds[0]);
+ if (ref) ref.focus();
+ }
+ },
+ isEnabled: (props:any):boolean => {
+ return !!props.selectedNoteIds.length;
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ selectedNoteIds: state.selectedNoteIds,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx b/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx
index 5ea306032..f0084cb0f 100644
--- a/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx
+++ b/ElectronClient/gui/NoteToolbar/NoteToolbar.tsx
@@ -1,10 +1,10 @@
import * as React from 'react';
+import { useEffect, useCallback, useState } from 'react';
+import CommandService from '../../lib/services/CommandService';
const { connect } = require('react-redux');
const { buildStyle } = require('lib/theme');
const Toolbar = require('../Toolbar.min.js');
-const Note = require('lib/models/Note');
const Folder = require('lib/models/Folder');
-const { time } = require('lib/time-utils.js');
const { _ } = require('lib/locale');
const { substrWithEllipsis } = require('lib/string-utils');
@@ -36,116 +36,61 @@ function styles_(props:NoteToolbarProps) {
});
}
-function useToolbarItems(props:NoteToolbarProps) {
- const { note, folders, watchedNoteFiles, notesParentType, dispatch
- , onButtonClick, backwardHistoryNotes, forwardHistoryNotes } = props;
-
- const toolbarItems = [];
-
- const selectedNoteFolder = Folder.byId(folders, note.parent_id);
-
- toolbarItems.push({
- tooltip: _('Back'),
- iconName: 'fa-arrow-left',
- enabled: (backwardHistoryNotes.length > 0),
- onClick: () => {
- if (!backwardHistoryNotes.length) return;
- props.dispatch({
- type: 'HISTORY_BACKWARD',
- });
- },
- });
-
- toolbarItems.push({
- tooltip: _('Forward'),
- iconName: 'fa-arrow-right',
- enabled: (forwardHistoryNotes.length > 0),
- onClick: () => {
- if (!forwardHistoryNotes.length) return;
- props.dispatch({
- type: 'HISTORY_FORWARD',
- });
- },
- });
-
- if (selectedNoteFolder && ['Search', 'Tag', 'SmartFilter'].includes(notesParentType)) {
- toolbarItems.push({
- title: _('In: %s', substrWithEllipsis(selectedNoteFolder.title, 0, 16)),
- iconName: 'fa-book',
- onClick: () => {
- props.dispatch({
- type: 'FOLDER_AND_NOTE_SELECT',
- folderId: selectedNoteFolder.id,
- noteId: note.id,
- });
- },
- });
- }
-
- toolbarItems.push({
- tooltip: _('Note properties'),
- iconName: 'fa-info-circle',
- onClick: () => {
- dispatch({
- type: 'WINDOW_COMMAND',
- name: 'commandNoteProperties',
- noteId: note.id,
- onRevisionLinkClick: () => {
- onButtonClick({ name: 'showRevisions' });
- },
- });
- },
- });
-
- if (watchedNoteFiles.indexOf(note.id) >= 0) {
- toolbarItems.push({
- tooltip: _('Click to stop external editing'),
- title: _('Watching...'),
- iconName: 'fa-share-square',
- onClick: () => {
- onButtonClick({ name: 'stopExternalEditing' });
- },
- });
- } else {
- toolbarItems.push({
- tooltip: _('Edit in external editor'),
- iconName: 'fa-share-square',
- onClick: () => {
- onButtonClick({ name: 'startExternalEditing' });
- },
- });
- }
-
- if (note.is_todo) {
- const item:any = {
- iconName: 'fa-clock',
- enabled: !note.todo_completed,
- onClick: () => {
- onButtonClick({ name: 'setAlarm' });
- },
- };
- if (Note.needAlarm(note)) {
- item.title = time.formatMsToLocal(note.todo_due);
- } else {
- item.tooltip = _('Set alarm');
- }
- toolbarItems.push(item);
- }
-
- toolbarItems.push({
- tooltip: _('Tags'),
- iconName: 'fa-tags',
- onClick: () => {
- onButtonClick({ name: 'setTags' });
- },
- });
-
- return toolbarItems;
-}
-
function NoteToolbar(props:NoteToolbarProps) {
const styles = styles_(props);
- const toolbarItems = useToolbarItems(props);
+ const [toolbarItems, setToolbarItems] = useState([]);
+ const selectedNoteFolder = Folder.byId(props.folders, props.note.parent_id);
+
+ const cmdService = CommandService.instance();
+
+ const updateToolbarItems = useCallback(() => {
+ const output = [];
+
+ output.push(
+ cmdService.commandToToolbarButton('historyBackward')
+ );
+
+ output.push(
+ cmdService.commandToToolbarButton('historyForward')
+ );
+
+ if (selectedNoteFolder.id && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) {
+ output.push({
+ title: _('In: %s', substrWithEllipsis(selectedNoteFolder.title, 0, 16)),
+ iconName: 'fa-book',
+ onClick: () => {
+ props.dispatch({
+ type: 'FOLDER_AND_NOTE_SELECT',
+ folderId: selectedNoteFolder.id,
+ noteId: props.note.id,
+ });
+ },
+ });
+ }
+
+ output.push(cmdService.commandToToolbarButton('showNoteProperties'));
+
+ if (props.watchedNoteFiles.indexOf(props.note.id) >= 0) {
+ output.push(cmdService.commandToToolbarButton('stopExternalEditing'));
+ } else {
+ output.push(cmdService.commandToToolbarButton('startExternalEditing'));
+ }
+
+ output.push(cmdService.commandToToolbarButton('editAlarm'));
+
+ output.push(cmdService.commandToToolbarButton('setTags'));
+
+ setToolbarItems(output);
+ }, [props.note.id, selectedNoteFolder.id, selectedNoteFolder.title, props.watchedNoteFiles, props.notesParentType]);
+
+ useEffect(() => {
+ updateToolbarItems();
+ cmdService.on('commandsEnabledStateChange', updateToolbarItems);
+ return () => {
+ cmdService.off('commandsEnabledStateChange', updateToolbarItems);
+ };
+ }, [updateToolbarItems]);
+
return ;
}
diff --git a/ElectronClient/gui/OneDriveLoginScreen.jsx b/ElectronClient/gui/OneDriveLoginScreen.jsx
index 2deefe475..ef9f058fa 100644
--- a/ElectronClient/gui/OneDriveLoginScreen.jsx
+++ b/ElectronClient/gui/OneDriveLoginScreen.jsx
@@ -3,7 +3,7 @@ const { connect } = require('react-redux');
const { reg } = require('lib/registry.js');
const Setting = require('lib/models/Setting');
const { bridge } = require('electron').remote.require('./bridge');
-const { Header } = require('./Header.min.js');
+const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('lib/onedrive-api-node-utils.js');
diff --git a/ElectronClient/gui/ResourceScreen.tsx b/ElectronClient/gui/ResourceScreen.tsx
index cbd62b2e6..5244e04d2 100644
--- a/ElectronClient/gui/ResourceScreen.tsx
+++ b/ElectronClient/gui/ResourceScreen.tsx
@@ -4,7 +4,7 @@ const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
-const { Header } = require('./Header.min.js');
+const { Header } = require('./Header/Header.min.js');
const prettyBytes = require('pretty-bytes');
const Resource = require('lib/models/Resource.js');
diff --git a/ElectronClient/gui/Root.jsx b/ElectronClient/gui/Root.jsx
index 54e8fc36f..030a17540 100644
--- a/ElectronClient/gui/Root.jsx
+++ b/ElectronClient/gui/Root.jsx
@@ -5,7 +5,8 @@ const { connect, Provider } = require('react-redux');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
-const { MainScreen } = require('./MainScreen.min.js');
+const { MainScreen } = require('.//MainScreen/MainScreen.min.js');
+const ErrorBoundary = require('./ErrorBoundary').default;
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
const { DropboxLoginScreen } = require('./DropboxLoginScreen.min.js');
const { StatusScreen } = require('./StatusScreen.min.js');
@@ -14,7 +15,6 @@ const { ConfigScreen } = require('./ConfigScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
const { Navigator } = require('./Navigator.min.js');
const WelcomeUtils = require('lib/WelcomeUtils');
-
const { app } = require('../app');
const { bridge } = require('electron').remote.require('./bridge');
@@ -112,7 +112,9 @@ const store = app().store();
render(
-
+
+
+
,
document.getElementById('react-root')
);
diff --git a/ElectronClient/gui/SideBar.jsx b/ElectronClient/gui/SideBar/SideBar.jsx
similarity index 87%
rename from ElectronClient/gui/SideBar.jsx
rename to ElectronClient/gui/SideBar/SideBar.jsx
index 643f166e1..4c4b0926f 100644
--- a/ElectronClient/gui/SideBar.jsx
+++ b/ElectronClient/gui/SideBar/SideBar.jsx
@@ -2,6 +2,7 @@ const React = require('react');
const { connect } = require('react-redux');
const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
+const CommandService = require('lib/services/CommandService.js').default;
const BaseModel = require('lib/BaseModel.js');
const Setting = require('lib/models/Setting.js');
const Folder = require('lib/models/Folder.js');
@@ -12,14 +13,20 @@ const { themeStyle } = require('lib/theme');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
-const InteropServiceHelper = require('../InteropServiceHelper.js');
+const InteropServiceHelper = require('../../InteropServiceHelper.js');
const { substrWithEllipsis } = require('lib/string-utils');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
+const commands = [
+ require('./commands/focusElementSideBar'),
+];
+
class SideBarComponent extends React.Component {
constructor() {
super();
+ CommandService.instance().componentRegisterCommands(this, commands);
+
this.onFolderDragStart_ = event => {
const folderId = event.currentTarget.getAttribute('folderid');
if (!folderId) return;
@@ -213,61 +220,10 @@ class SideBarComponent extends React.Component {
}
}
- doCommand(command) {
- if (!command) return;
-
- let commandProcessed = true;
-
- if (command.name === 'focusElement' && command.target === 'sideBar') {
- if (this.props.sidebarVisibility) {
- const item = this.selectedItem();
- if (item) {
- const anchorRef = this.anchorItemRefs[item.type][item.id];
- if (anchorRef) anchorRef.current.focus();
- } else {
- const anchorRef = this.firstAnchorItemRef('folder');
- console.info('anchorRef', anchorRef);
- if (anchorRef) anchorRef.current.focus();
- }
- }
- } else if (command.name === 'synchronize') {
- if (!this.props.syncStarted) this.sync_click();
- } else {
- commandProcessed = false;
- }
-
- if (commandProcessed) {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: null,
- });
- }
- }
-
componentWillUnmount() {
this.clearForceUpdateDuringSync();
- }
- componentDidUpdate(prevProps) {
- if (prevProps.windowCommand !== this.props.windowCommand) {
- this.doCommand(this.props.windowCommand);
- }
-
- // if (shim.isLinux()) {
- // // For some reason, the UI seems to sleep in some Linux distro during
- // // sync. Cannot find the reason for it and cannot replicate, so here
- // // as a test force the update at regular intervals.
- // // https://github.com/laurent22/joplin/issues/312#issuecomment-429472193
- // if (!prevProps.syncStarted && this.props.syncStarted) {
- // this.clearForceUpdateDuringSync();
-
- // this.forceUpdateDuringSyncIID_ = setInterval(() => {
- // this.forceUpdate();
- // }, 2000);
- // }
-
- // if (prevProps.syncStarted && !this.props.syncStarted) this.clearForceUpdateDuringSync();
- // }
+ CommandService.instance().componentUnregisterCommands(commands);
}
async itemContextMenu(event) {
@@ -299,16 +255,7 @@ class SideBarComponent extends React.Component {
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(
- new MenuItem({
- label: _('New sub-notebook'),
- click: () => {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'newSubNotebook',
- activeFolderId: itemId,
- });
- },
- })
+ new MenuItem(CommandService.instance().commandToMenuItem('newNotebook', null, itemId)),
);
}
@@ -337,31 +284,7 @@ class SideBarComponent extends React.Component {
);
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
- menu.append(
- new MenuItem({
- label: _('Rename'),
- click: async () => {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'renameFolder',
- id: itemId,
- });
- },
- })
- );
-
- // menu.append(
- // new MenuItem({
- // label: _("Move"),
- // click: async () => {
- // this.props.dispatch({
- // type: "WINDOW_COMMAND",
- // name: "renameFolder",
- // id: itemId,
- // });
- // },
- // })
- // );
+ menu.append(new MenuItem(CommandService.instance().commandToMenuItem('renameFolder', null, { folderId: itemId })));
menu.append(new MenuItem({ type: 'separator' }));
@@ -393,18 +316,9 @@ class SideBarComponent extends React.Component {
}
if (itemType === BaseModel.TYPE_TAG) {
- menu.append(
- new MenuItem({
- label: _('Rename'),
- click: async () => {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'renameTag',
- id: itemId,
- });
- },
- })
- );
+ menu.append(new MenuItem(
+ CommandService.instance().commandToMenuItem('renameTag', null, { tagId: itemId })
+ ));
}
menu.popup(bridge().window());
@@ -424,9 +338,9 @@ class SideBarComponent extends React.Component {
});
}
- async sync_click() {
- await shared.synchronize_press(this);
- }
+ // async sync_click() {
+ // await shared.synchronize_press(this);
+ // }
anchorItemRef(type, id) {
if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {};
@@ -662,17 +576,9 @@ class SideBarComponent extends React.Component {
event.preventDefault();
if (event.shiftKey) {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'noteBody',
- });
+ CommandService.instance().execute('focusElement', { target: 'noteBody' });
} else {
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'noteList',
- });
+ CommandService.instance().execute('focusElement', { target: 'noteList' });
}
}
@@ -728,7 +634,8 @@ class SideBarComponent extends React.Component {
href="#"
key="sync_button"
onClick={() => {
- this.sync_click();
+ CommandService.instance().execute('synchronize');
+ // this.sync_click();
}}
>
{icon}
@@ -849,7 +756,6 @@ const mapStateToProps = state => {
collapsedFolderIds: state.collapsedFolderIds,
decryptionWorker: state.decryptionWorker,
resourceFetcher: state.resourceFetcher,
- windowCommand: state.windowCommand,
sidebarVisibility: state.sidebarVisibility,
noteListVisibility: state.noteListVisibility,
};
diff --git a/ElectronClient/gui/SideBar/commands/focusElementSideBar.ts b/ElectronClient/gui/SideBar/commands/focusElementSideBar.ts
new file mode 100644
index 000000000..90238d8d1
--- /dev/null
+++ b/ElectronClient/gui/SideBar/commands/focusElementSideBar.ts
@@ -0,0 +1,32 @@
+import { CommandRuntime, CommandDeclaration } from '../../../lib/services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'focusElementSideBar',
+ label: () => _('Sidebar'),
+};
+
+export const runtime = (comp:any):CommandRuntime => {
+ return {
+ execute: async ({ sidebarVisibility }:any) => {
+ if (sidebarVisibility) {
+ const item = comp.selectedItem();
+ if (item) {
+ const anchorRef = comp.anchorItemRefs[item.type][item.id];
+ if (anchorRef) anchorRef.current.focus();
+ } else {
+ const anchorRef = comp.firstAnchorItemRef('folder');
+ if (anchorRef) anchorRef.current.focus();
+ }
+ }
+ },
+ isEnabled: (props:any):boolean => {
+ return props.sidebarVisibility;
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ sidebarVisibility: state.sidebarVisibility,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/gui/StatusScreen.jsx b/ElectronClient/gui/StatusScreen.jsx
index bcf802a53..8f568b3c8 100644
--- a/ElectronClient/gui/StatusScreen.jsx
+++ b/ElectronClient/gui/StatusScreen.jsx
@@ -2,7 +2,7 @@ const React = require('react');
const { connect } = require('react-redux');
const Setting = require('lib/models/Setting.js');
const { bridge } = require('electron').remote.require('./bridge');
-const { Header } = require('./Header.min.js');
+const { Header } = require('./Header/Header.min.js');
const { themeStyle } = require('lib/theme');
const { _ } = require('lib/locale.js');
const { ReportService } = require('lib/services/report.js');
diff --git a/ElectronClient/gui/utils/NoteListUtils.js b/ElectronClient/gui/utils/NoteListUtils.js
index 58e698159..4d11c114b 100644
--- a/ElectronClient/gui/utils/NoteListUtils.js
+++ b/ElectronClient/gui/utils/NoteListUtils.js
@@ -3,15 +3,17 @@ const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
-const eventManager = require('../../eventManager');
+const eventManager = require('lib/eventManager');
const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('../../InteropServiceHelper.js');
const Note = require('lib/models/Note');
-const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
+const CommandService = require('lib/services/CommandService').default;
const { substrWithEllipsis } = require('lib/string-utils');
class NoteListUtils {
static makeContextMenu(noteIds, props) {
+ const cmdService = CommandService.instance();
+
const notes = noteIds.map(id => BaseModel.byId(props.notes, id));
let hasEncrypted = false;
@@ -23,29 +25,11 @@ class NoteListUtils {
if (!hasEncrypted) {
menu.append(
- new MenuItem({
- label: _('Add or remove tags'),
- click: async () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'setTags',
- noteIds: noteIds,
- });
- },
- })
+ new MenuItem(cmdService.commandToMenuItem('setTags'))
);
menu.append(
- new MenuItem({
- label: _('Move to notebook'),
- click: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'moveToFolder',
- noteIds: noteIds,
- });
- },
- })
+ new MenuItem(cmdService.commandToMenuItem('moveToFolder'))
);
menu.append(
@@ -64,23 +48,11 @@ class NoteListUtils {
if (props.watchedNoteFiles.indexOf(noteIds[0]) < 0) {
menu.append(
- new MenuItem({
- label: _('Edit in external editor'),
- enabled: noteIds.length === 1,
- click: async () => {
- this.startExternalEditing(noteIds[0]);
- },
- })
+ new MenuItem(cmdService.commandToMenuItem('startExternalEditing', null, { noteId: noteIds[0] }))
);
} else {
menu.append(
- new MenuItem({
- label: _('Stop external editing'),
- enabled: noteIds.length === 1,
- click: async () => {
- this.stopExternalEditing(noteIds[0]);
- },
- })
+ new MenuItem(cmdService.commandToMenuItem('stopExternalEditing', null, { noteId: noteIds[0] }))
);
}
@@ -149,17 +121,9 @@ class NoteListUtils {
);
menu.append(
- new MenuItem({
- label: _('Share note...'),
- click: async () => {
- console.info('NOTE IDS', noteIds);
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'commandShareNoteDialog',
- noteIds: noteIds.slice(),
- });
- },
- })
+ new MenuItem(
+ cmdService.commandToMenuItem('showShareNoteDialog', null, { noteIds: noteIds.slice() })
+ )
);
const exportMenu = new Menu();
@@ -182,16 +146,9 @@ class NoteListUtils {
}
exportMenu.append(
- new MenuItem({
- label: `PDF - ${_('PDF File')}`,
- click: () => {
- props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'exportPdf',
- noteIds: noteIds,
- });
- },
- })
+ new MenuItem(
+ cmdService.commandToMenuItem('exportPdf', null, { noteIds: noteIds })
+ )
);
const exportMenuItem = new MenuItem({ label: _('Export'), submenu: exportMenu });
@@ -232,19 +189,6 @@ class NoteListUtils {
await Note.batchDelete(noteIds);
}
- static async startExternalEditing(noteId) {
- try {
- const note = await Note.load(noteId);
- ExternalEditWatcher.instance().openAndWatch(note);
- } catch (error) {
- bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
- }
- }
-
- static async stopExternalEditing(noteId) {
- ExternalEditWatcher.instance().stopWatching(noteId);
- }
-
}
module.exports = NoteListUtils;
diff --git a/ElectronClient/main-html.js b/ElectronClient/main-html.js
index 9cf32a1d3..5fe0dab33 100644
--- a/ElectronClient/main-html.js
+++ b/ElectronClient/main-html.js
@@ -98,6 +98,8 @@ document.addEventListener('click', (event) => event.preventDefault());
app().start(bridge().processArgv()).then(() => {
require('./gui/Root.min.js');
}).catch((error) => {
+ const env = bridge().env();
+
if (error.code == 'flagError') {
bridge().showErrorMessageBox(error.message);
} else {
@@ -107,8 +109,14 @@ app().start(bridge().processArgv()).then(() => {
if (error.fileName) msg.push(error.fileName);
if (error.lineNumber) msg.push(error.lineNumber);
if (error.stack) msg.push(error.stack);
- bridge().showErrorMessageBox(msg.join('\n\n'));
+
+ if (env === 'dev') {
+ console.error(error);
+ } else {
+ bridge().showErrorMessageBox(msg.join('\n\n'));
+ }
}
- bridge().electronApp().exit(1);
+ // In dev, we leave the app open as debug statements in the console can be useful
+ if (env !== 'dev') bridge().electronApp().exit(1);
});
diff --git a/ElectronClient/plugins/GotoAnything.jsx b/ElectronClient/plugins/GotoAnything.jsx
index 55067c039..731802cbd 100644
--- a/ElectronClient/plugins/GotoAnything.jsx
+++ b/ElectronClient/plugins/GotoAnything.jsx
@@ -3,6 +3,7 @@ const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('lib/theme');
const SearchEngine = require('lib/services/SearchEngine');
+const CommandService = require('lib/services/CommandService').default;
const BaseModel = require('lib/BaseModel');
const Tag = require('lib/models/Tag');
const Folder = require('lib/models/Folder');
@@ -301,11 +302,7 @@ class Dialog extends React.PureComponent {
noteId: item.id,
});
- this.props.dispatch({
- type: 'WINDOW_COMMAND',
- name: 'focusElement',
- target: 'noteBody',
- });
+ CommandService.instance().scheduleExecute('focusElement', { target: 'noteBody' });
} else if (this.state.listType === BaseModel.TYPE_TAG) {
this.props.dispatch({
type: 'TAG_SELECT',
diff --git a/ElectronClient/tools/compileScripts.js b/ElectronClient/tools/compileScripts.js
index 12c03aa02..eb73fc41b 100644
--- a/ElectronClient/tools/compileScripts.js
+++ b/ElectronClient/tools/compileScripts.js
@@ -43,6 +43,10 @@ function convertJsx(path) {
module.exports = function() {
convertJsx(`${__dirname}/../gui`);
+ convertJsx(`${__dirname}/../gui/SideBar`);
+ convertJsx(`${__dirname}/../gui/MainScreen`);
+ convertJsx(`${__dirname}/../gui/Header`);
+ convertJsx(`${__dirname}/../gui/NoteList`);
convertJsx(`${__dirname}/../plugins`);
const libContent = [
diff --git a/ReactNativeClient/lib/commands/historyBackward.ts b/ReactNativeClient/lib/commands/historyBackward.ts
new file mode 100644
index 000000000..4140d5c8e
--- /dev/null
+++ b/ReactNativeClient/lib/commands/historyBackward.ts
@@ -0,0 +1,29 @@
+import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'historyBackward',
+ label: () => _('Back'),
+ iconName: 'fa-arrow-left',
+};
+
+interface Props {
+ backwardHistoryNotes: any[],
+}
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async (props:Props) => {
+ if (!props.backwardHistoryNotes.length) return;
+ utils.store.dispatch({
+ type: 'HISTORY_BACKWARD',
+ });
+ },
+ isEnabled: (props:Props) => {
+ return props.backwardHistoryNotes.length > 0;
+ },
+ mapStateToProps: (state:any) => {
+ return { backwardHistoryNotes: state.backwardHistoryNotes };
+ },
+ };
+};
diff --git a/ReactNativeClient/lib/commands/historyForward.ts b/ReactNativeClient/lib/commands/historyForward.ts
new file mode 100644
index 000000000..945120bbc
--- /dev/null
+++ b/ReactNativeClient/lib/commands/historyForward.ts
@@ -0,0 +1,29 @@
+import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService';
+const { _ } = require('lib/locale');
+
+export const declaration:CommandDeclaration = {
+ name: 'historyForward',
+ label: () => _('Forward'),
+ iconName: 'fa-arrow-right',
+};
+
+interface Props {
+ forwardHistoryNotes: any[],
+}
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async (props:Props) => {
+ if (!props.forwardHistoryNotes.length) return;
+ utils.store.dispatch({
+ type: 'HISTORY_FORWARD',
+ });
+ },
+ isEnabled: (props:Props) => {
+ return props.forwardHistoryNotes.length > 0;
+ },
+ mapStateToProps: (state:any) => {
+ return { forwardHistoryNotes: state.forwardHistoryNotes };
+ },
+ };
+};
diff --git a/ReactNativeClient/lib/commands/synchronize.ts b/ReactNativeClient/lib/commands/synchronize.ts
new file mode 100644
index 000000000..c2ebb150f
--- /dev/null
+++ b/ReactNativeClient/lib/commands/synchronize.ts
@@ -0,0 +1,55 @@
+import { utils, CommandRuntime, CommandDeclaration } from '../services/CommandService';
+const { _ } = require('lib/locale');
+const { reg } = require('lib/registry.js');
+
+export const declaration:CommandDeclaration = {
+ name: 'synchronize',
+ label: () => _('Synchronize'),
+ iconName: 'fa-sync-alt',
+};
+
+export const runtime = ():CommandRuntime => {
+ return {
+ execute: async ({ syncStarted }:any) => {
+ const action = syncStarted ? 'cancel' : 'start';
+
+ if (!(await reg.syncTarget().isAuthenticated())) {
+ if (reg.syncTarget().authRouteName()) {
+ utils.store.dispatch({
+ type: 'NAV_GO',
+ routeName: reg.syncTarget().authRouteName(),
+ });
+ return 'auth';
+ }
+
+ reg.logger().info('Not authentified with sync target - please check your credential.');
+ return 'error';
+ }
+
+ let sync = null;
+ try {
+ sync = await reg.syncTarget().synchronizer();
+ } catch (error) {
+ reg.logger().info('Could not acquire synchroniser:');
+ reg.logger().info(error);
+ return 'error';
+ }
+
+ if (action == 'cancel') {
+ sync.cancel();
+ return 'cancel';
+ } else {
+ reg.scheduleSync(0);
+ return 'sync';
+ }
+ },
+ isEnabled: (props:any) => {
+ return !props.syncStarted;
+ },
+ mapStateToProps: (state:any):any => {
+ return {
+ syncStarted: state.syncStarted,
+ };
+ },
+ };
+};
diff --git a/ElectronClient/eventManager.js b/ReactNativeClient/lib/eventManager.js
similarity index 100%
rename from ElectronClient/eventManager.js
rename to ReactNativeClient/lib/eventManager.js
diff --git a/ReactNativeClient/lib/hooks/useEffectDebugger.ts b/ReactNativeClient/lib/hooks/useEffectDebugger.ts
new file mode 100644
index 000000000..b490ba30d
--- /dev/null
+++ b/ReactNativeClient/lib/hooks/useEffectDebugger.ts
@@ -0,0 +1,27 @@
+import usePrevious from './usePrevious';
+import { useEffect } from 'react';
+
+export default function useEffectDebugger(effectHook:any, dependencies:any, dependencyNames:any[] = []) {
+ const previousDeps = usePrevious(dependencies, []);
+
+ const changedDeps = dependencies.reduce((accum:any, dependency:any, index:any) => {
+ if (dependency !== previousDeps[index]) {
+ const keyName = dependencyNames[index] || index;
+ return {
+ ...accum,
+ [keyName]: {
+ before: previousDeps[index],
+ after: dependency,
+ },
+ };
+ }
+
+ return accum;
+ }, {});
+
+ if (Object.keys(changedDeps).length) {
+ console.log('[use-effet-debugger] ', changedDeps);
+ }
+
+ useEffect(effectHook, dependencies);
+}
diff --git a/ReactNativeClient/lib/models/Note.js b/ReactNativeClient/lib/models/Note.js
index cc0fbfeeb..ede96c417 100644
--- a/ReactNativeClient/lib/models/Note.js
+++ b/ReactNativeClient/lib/models/Note.js
@@ -265,7 +265,7 @@ class Note extends BaseItem {
includeTimestamps: true,
}, options);
- const output = ['id', 'title', 'is_todo', 'todo_completed', 'parent_id', 'encryption_applied', 'order'];
+ const output = ['id', 'title', 'is_todo', 'todo_completed', 'todo_due', 'parent_id', 'encryption_applied', 'order', 'markup_language'];
if (options.includeTimestamps) {
output.push('updated_time');
diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js
index d141ed99c..f5f22fbb9 100644
--- a/ReactNativeClient/lib/reducer.js
+++ b/ReactNativeClient/lib/reducer.js
@@ -2,6 +2,7 @@ const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js');
const ArrayUtils = require('lib/ArrayUtils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids');
+const CommandService = require('lib/services/CommandService').default;
const defaultState = {
notes: [],
@@ -77,6 +78,10 @@ const cacheEnabledOutput = (key, output) => {
return derivedStateCache_[key];
};
+stateUtils.hasOneSelectedNote = function(state) {
+ return state.selectedNoteIds.length === 1;
+};
+
stateUtils.notesOrder = function(stateSettings) {
if (stateSettings['notes.sortOrder.field'] === 'order') {
return cacheEnabledOutput('notesOrder', [
@@ -1016,6 +1021,8 @@ const reducer = (state = defaultState, action) => {
newState = handleHistory(newState, action);
}
+ CommandService.instance().scheduleMapStateToProps(newState);
+
return newState;
};
diff --git a/ReactNativeClient/lib/services/CommandService.ts b/ReactNativeClient/lib/services/CommandService.ts
new file mode 100644
index 000000000..bfcdbda0d
--- /dev/null
+++ b/ReactNativeClient/lib/services/CommandService.ts
@@ -0,0 +1,289 @@
+const BaseService = require('lib/services/BaseService');
+const eventManager = require('lib/eventManager');
+
+export interface CommandRuntime {
+ execute(props:any):void
+ isEnabled?(props:any):boolean
+ mapStateToProps?(state:any):any
+ // Used for the (optional) toolbar button title
+ title?(props:any):string,
+ props?:any
+}
+
+export interface CommandDeclaration {
+ name: string
+ // Used for the menu item label, and toolbar button tooltip
+ label?():string,
+ iconName?: string,
+ // Same as `role` key in Electron MenuItem:
+ // https://www.electronjs.org/docs/api/menu-item#new-menuitemoptions
+ // Note that due to a bug in Electron, menu items with a role cannot
+ // be disabled.
+ role?: string,
+}
+
+export interface Command {
+ declaration: CommandDeclaration,
+ runtime?: CommandRuntime,
+}
+
+interface Commands {
+ [key:string]: Command;
+}
+
+interface ReduxStore {
+ dispatch(action:any):void;
+ getState():any;
+}
+
+interface Utils {
+ store: ReduxStore;
+}
+
+export const utils:Utils = {
+ store: {
+ dispatch: () => {},
+ getState: () => {},
+ },
+};
+
+interface CommandByNameOptions {
+ mustExist?:boolean,
+ runtimeMustBeRegistered?:boolean,
+}
+
+interface CommandState {
+ title: string,
+ enabled: boolean,
+}
+
+interface CommandStates {
+ [key:string]: CommandState
+}
+
+export default class CommandService extends BaseService {
+
+ private static instance_:CommandService;
+
+ static instance():CommandService {
+ if (this.instance_) return this.instance_;
+ this.instance_ = new CommandService();
+ return this.instance_;
+ }
+
+ private commands_:Commands = {};
+ private commandPreviousStates_:CommandStates = {};
+ private mapStateToPropsIID_:any = null;
+
+ initialize(store:any) {
+ utils.store = store;
+ }
+
+ public on(eventName:string, callback:Function) {
+ eventManager.on(eventName, callback);
+ }
+
+ public off(eventName:string, callback:Function) {
+ eventManager.off(eventName, callback);
+ }
+
+ private propsHaveChanged(previous:any, next:any) {
+ if (!previous && next) return true;
+
+ for (const n in previous) {
+ if (previous[n] !== next[n]) return true;
+ }
+
+ return false;
+ }
+
+ scheduleMapStateToProps(state:any) {
+ if (this.mapStateToPropsIID_) clearTimeout(this.mapStateToPropsIID_);
+
+ this.mapStateToPropsIID_ = setTimeout(() => {
+ this.mapStateToProps(state);
+ }, 50);
+ }
+
+ private mapStateToProps(state:any) {
+ const newState = state;
+
+ const changedCommands:any = {};
+
+ for (const name in this.commands_) {
+ const command = this.commands_[name];
+ if (!command.runtime || !command.runtime.mapStateToProps) continue;
+ const newProps = command.runtime.mapStateToProps(state);
+
+ const haveChanged = this.propsHaveChanged(command.runtime.props, newProps);
+
+ if (haveChanged) {
+ const previousState = this.commandPreviousStates_[name];
+
+ command.runtime.props = newProps;
+
+ const newState:CommandState = {
+ enabled: this.isEnabled(name),
+ title: this.title(name),
+ };
+
+ if (!previousState || previousState.title !== newState.title || previousState.enabled !== newState.enabled) {
+ changedCommands[name] = newState;
+ }
+
+ this.commandPreviousStates_[name] = newState;
+ }
+ }
+
+ if (Object.keys(changedCommands).length) {
+ eventManager.emit('commandsEnabledStateChange', { commands: changedCommands });
+ }
+
+ return newState;
+ }
+
+ private commandByName(name:string, options:CommandByNameOptions = null):Command {
+ options = {
+ mustExist: true,
+ runtimeMustBeRegistered: false,
+ };
+
+ const command = this.commands_[name];
+
+ if (!command) {
+ if (options.mustExist) throw new Error(`Command not found: ${name}. Make sure the declaration has been registered.`);
+ return null;
+ }
+
+ if (options.runtimeMustBeRegistered && !command.runtime) throw new Error(`Runtime is not registered for command ${name}`);
+ return command;
+ }
+
+ registerDeclaration(declaration:CommandDeclaration) {
+ // if (this.commands_[declaration.name]) throw new Error(`There is already a command with name ${declaration.name}`);
+
+ declaration = { ...declaration };
+ if (!declaration.label) declaration.label = () => '';
+ if (!declaration.iconName) declaration.iconName = '';
+
+ // In TypeScript it's not an issue, but in JavaScript it's easy to accidentally set the label
+ // to a string instead of a function, and it will cause strange errors that are hard to debug.
+ // So here check early that we have the right type.
+ if (typeof declaration.label !== 'function') throw new Error(`declaration.label must be a function: ${declaration.name}`);
+
+ this.commands_[declaration.name] = {
+ declaration: declaration,
+ };
+ }
+
+ registerRuntime(commandName:string, runtime:CommandRuntime) {
+ // console.info('CommandService::registerRuntime:', commandName);
+
+ if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`);
+
+ const command = this.commandByName(commandName);
+ // if (command.runtime) throw new Error(`Runtime is already registered for command: ${commandName}`);
+
+ runtime = Object.assign({}, runtime);
+ if (!runtime.isEnabled) runtime.isEnabled = () => true;
+ if (!runtime.title) runtime.title = () => null;
+ command.runtime = runtime;
+ }
+
+ componentRegisterCommands(component:any, commands:any[]) {
+ for (const command of commands) {
+ CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component));
+ }
+ }
+
+ componentUnregisterCommands(commands:any[]) {
+ for (const command of commands) {
+ CommandService.instance().unregisterRuntime(command.declaration.name);
+ }
+ }
+
+ unregisterRuntime(commandName:string) {
+ // console.info('CommandService::unregisterRuntime:', commandName);
+
+ const command = this.commandByName(commandName, { mustExist: false });
+ if (!command || !command.runtime) return;
+ delete command.runtime;
+ }
+
+ execute(commandName:string, args:any = null) {
+ console.info('CommandService::execute:', commandName, args);
+
+ const command = this.commandByName(commandName);
+ command.runtime.execute(args ? args : {});
+ }
+
+ scheduleExecute(commandName:string, args:any = null) {
+ setTimeout(() => {
+ this.execute(commandName, args);
+ }, 10);
+ }
+
+ isEnabled(commandName:string):boolean {
+ const command = this.commandByName(commandName);
+ if (!command || !command.runtime) return false;
+ return command.runtime.props ? command.runtime.isEnabled(command.runtime.props ? command.runtime.props : {}) : true;
+ }
+
+ title(commandName:string):string {
+ const command = this.commandByName(commandName);
+ if (!command || !command.runtime) return null;
+ return command.runtime.props ? command.runtime.title(command.runtime.props ? command.runtime.props : {}) : null;
+ }
+
+ private extractExecuteArgs(command:Command, executeArgs:any) {
+ if (executeArgs) return executeArgs;
+ if (!command.runtime) throw new Error(`Command: ${command.declaration.name}: Runtime is not defined - make sure it has been registered.`);
+ if (command.runtime.props) return command.runtime.props;
+ return {};
+ }
+
+ commandToToolbarButton(commandName:string, executeArgs:any = null) {
+ const command = this.commandByName(commandName, { runtimeMustBeRegistered: true });
+
+ return {
+ tooltip: command.declaration.label(),
+ iconName: command.declaration.iconName,
+ enabled: this.isEnabled(commandName),
+ onClick: () => {
+ this.execute(commandName, this.extractExecuteArgs(command, executeArgs));
+ },
+ title: this.title(commandName),
+ };
+ }
+
+ commandToMenuItem(commandName:string, accelerator:string = null, executeArgs:any = null) {
+ const command = this.commandByName(commandName);
+
+ const item:any = {
+ id: command.declaration.name,
+ label: command.declaration.label(),
+ click: () => {
+ this.execute(commandName, this.extractExecuteArgs(command, executeArgs));
+ },
+ };
+
+ if (accelerator) item.accelerator = accelerator;
+ if (command.declaration.role) item.role = command.declaration.role;
+
+ return item;
+ }
+
+ commandsEnabledState(previousState:any = null):any {
+ const output:any = {};
+
+ for (const name in this.commands_) {
+ const enabled = this.isEnabled(name);
+ if (!previousState || previousState[name] !== enabled) {
+ output[name] = enabled;
+ }
+ }
+
+ return output;
+ }
+
+}
diff --git a/joplin.code-workspace b/joplin.code-workspace
index 61b523be3..b482b4e04 100644
--- a/joplin.code-workspace
+++ b/joplin.code-workspace
@@ -313,7 +313,48 @@
"D:/Web/www/nextcloud/apps/joplin/Tools/**/github_oauth_token.txt": true,
"D:/Web/www/nextcloud/apps/joplin/Tools/**/node_modules/": true,
"D:/Web/www/nextcloud/apps/joplin/**/vendor/": true,
- "D:/Web/www/nextcloud/apps/joplin/**/dist/": true
+ "D:/Web/www/nextcloud/apps/joplin/**/dist/": true,
+ "ReactNativeClient/lib/commands/newNote.js": true,
+ "ReactNativeClient/lib/commands/newTodo.js": true,
+ "ReactNativeClient/lib/services/CommandService.js": true,
+ "ElectronClient/gui/ErrorBoundary.js": true,
+ "ElectronClient/gui/MainScreen/commands/editAlarm.js": true,
+ "ElectronClient/gui/MainScreen/commands/exportPdf.js": true,
+ "ElectronClient/gui/MainScreen/commands/hideModalMessage.js": true,
+ "ElectronClient/gui/MainScreen/commands/moveToFolder.js": true,
+ "ElectronClient/gui/MainScreen/commands/newNote.js": true,
+ "ElectronClient/gui/MainScreen/commands/newNotebook.js": true,
+ "ElectronClient/gui/MainScreen/commands/newTodo.js": true,
+ "ElectronClient/gui/MainScreen/commands/print.js": true,
+ "ElectronClient/gui/MainScreen/commands/renameFolder.js": true,
+ "ElectronClient/gui/MainScreen/commands/renameTag.js": true,
+ "ElectronClient/gui/MainScreen/commands/search.js": true,
+ "ElectronClient/gui/MainScreen/commands/selectTemplate.js": true,
+ "ElectronClient/gui/MainScreen/commands/setTags.js": true,
+ "ElectronClient/gui/MainScreen/commands/showModalMessage.js": true,
+ "ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js": true,
+ "ElectronClient/gui/MainScreen/commands/showNoteProperties.js": true,
+ "ElectronClient/gui/MainScreen/commands/showShareNoteDialog.js": true,
+ "ElectronClient/gui/MainScreen/commands/toggleNoteList.js": true,
+ "ElectronClient/gui/MainScreen/commands/toggleSidebar.js": true,
+ "ElectronClient/gui/MainScreen/commands/toggleVisiblePanes.js": true,
+ "./ElectronClient/**/*.min.js": true,
+ "ElectronClient/commands/focusElement.js": true,
+ "ElectronClient/gui/Header/commands/focusSearch.js": true,
+ "ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js": true,
+ "ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js": true,
+ "ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js": true,
+ "ElectronClient/gui/NoteEditor/commands/showLocalSearch.js": true,
+ "ElectronClient/gui/NoteEditor/commands/startExternalEditing.js": true,
+ "ElectronClient/gui/NoteEditor/commands/stopExternalEditing.js": true,
+ "ElectronClient/gui/NoteList/commands/focusElementNoteList.js": true,
+ "ElectronClient/gui/SideBar/commands/focusElementSideBar.js": true,
+ "ReactNativeClient/lib/commands/synchronize.js": true,
+ "ElectronClient/commands/startExternalEditing.js": true,
+ "ElectronClient/commands/stopExternalEditing.js": true,
+ "ElectronClient/gui/NoteEditor/commands/showRevisions.js": true,
+ "ReactNativeClient/lib/commands/historyBackward.js": true,
+ "ReactNativeClient/lib/commands/historyForward.js": true
},
"spellright.language": [
"en"