mirror of https://github.com/laurent22/joplin.git
Desktop: Refactored command system
The goal is to make the command system more modular, so each command can be defined as a single object that includes a declaration (name, label, etc.) and a runtime (to execute the command, test if it should be enabled, etc.) Utility methods are provided to convert a command to a menu item or a toolbar button, thus reducing duplicated and boiler plate code across the codebase (often the menu item logic was duplicated in the toolbar button logic and vice versa). The goal is to make it easier to add new commands (and associated menu item and toolbar buttons) and to call them from anywhere. This is also useful for plugins, which can also easily define new commands. Could also allow creating a command palette.pull/3458/head
parent
6046f40e45
commit
c63c6370b5
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}`);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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(<h2>Message</h2>);
|
||||
output.push(<p>{this.state.error.message}</p>);
|
||||
|
||||
if (this.state.error.stack) {
|
||||
output.push(<h2>Stack trace</h2>);
|
||||
output.push(<pre>{this.state.error.stack}</pre>);
|
||||
}
|
||||
|
||||
if (this.state.errorInfo) {
|
||||
if (this.state.errorInfo.componentStack) {
|
||||
output.push(<h2>Component stack</h2>);
|
||||
output.push(<pre>{this.state.errorInfo.componentStack}</pre>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'auto', fontFamily: 'sans-serif', padding: '5px 20px' }}>
|
||||
<h1>Error</h1>
|
||||
<p>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.</p>
|
||||
{output}
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
return (
|
||||
<div>
|
||||
{JSON.stringify(this.state)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
|
@ -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,
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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:
|
||||
<div className="modal-message">
|
||||
<div id="loading-animation" />
|
||||
<div className="text">{command.message}</div>
|
||||
</div>,
|
||||
},
|
||||
});
|
||||
} 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,
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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: '' } });
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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:
|
||||
<div className="modal-message">
|
||||
<div id="loading-animation" />
|
||||
<div className="text">{message}</div>
|
||||
</div>,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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'],
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 <ToolbarBase disabled={props.disabled} style={styles.root} items={createToolbarItems()} />;
|
||||
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;
|
||||
}
|
||||
|
|
|
@ -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 <ToolbarBase disabled={props.disabled} style={styles.root} items={createToolbarItems()} />;
|
||||
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
// };
|
||||
// },
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -13,7 +13,6 @@ export interface NoteEditorProps {
|
|||
editorNoteStatuses: any;
|
||||
syncStarted: boolean;
|
||||
bodyEditor: string;
|
||||
windowCommand: any;
|
||||
folders: any[];
|
||||
notesParentType: string;
|
||||
selectedNoteTags: any[];
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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'],
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 <Toolbar style={styles.root} items={toolbarItems} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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(
|
||||
<Provider store={store}>
|
||||
<Root />
|
||||
<ErrorBoundary>
|
||||
<Root />
|
||||
</ErrorBoundary>
|
||||
</Provider>,
|
||||
document.getElementById('react-root')
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue