diff --git a/src/public/app/widgets/buttons/button_widget.js b/src/public/app/widgets/buttons/abstract_button.js similarity index 51% rename from src/public/app/widgets/buttons/button_widget.js rename to src/public/app/widgets/buttons/abstract_button.js index 623facfe6..69b1f22b3 100644 --- a/src/public/app/widgets/buttons/button_widget.js +++ b/src/public/app/widgets/buttons/abstract_button.js @@ -1,15 +1,10 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import keyboardActionsService from "../../services/keyboard_actions.js"; const TPL = ``; -let actions; - -keyboardActionsService.getActions().then(as => actions = as); - -export default class ButtonWidget extends NoteContextAwareWidget { +export default class AbstractButtonWidget extends NoteContextAwareWidget { isEnabled() { return true; } @@ -21,8 +16,6 @@ export default class ButtonWidget extends NoteContextAwareWidget { titlePlacement: 'right', title: null, icon: null, - command: null, - onClick: null, onContextMenu: null }; } @@ -30,20 +23,6 @@ export default class ButtonWidget extends NoteContextAwareWidget { doRender() { this.$widget = $(TPL); - if (this.settings.onClick) { - this.$widget.on("click", e => { - this.$widget.tooltip("hide"); - - this.settings.onClick(this, e); - }); - } else { - this.$widget.on("click", () => { - this.$widget.tooltip("hide"); - - this.triggerCommand(this.settings.command); - }); - } - if (this.settings.onContextMenu) { this.$widget.on("contextmenu", e => { this.$widget.tooltip("hide"); @@ -56,26 +35,19 @@ export default class ButtonWidget extends NoteContextAwareWidget { this.$widget.tooltip({ html: true, - title: () => { - const title = typeof this.settings.title === "function" - ? this.settings.title() - : this.settings.title; - - const action = actions.find(act => act.actionName === this.settings.command); - - if (action && action.effectiveShortcuts.length > 0) { - return `${title} (${action.effectiveShortcuts.join(", ")})`; - } - else { - return title; - } - }, + title: () => this.getTitle(), trigger: "hover" }); super.doRender(); } + getTitle() { + return typeof this.settings.title === "function" + ? this.settings.title() + : this.settings.title; + } + refreshIcon() { for (const className of this.$widget[0].classList) { if (className.startsWith("bx-")) { @@ -83,39 +55,36 @@ export default class ButtonWidget extends NoteContextAwareWidget { } } - this.$widget.addClass(this.settings.icon); + const icon = typeof this.settings.icon === "function" + ? this.settings.icon() + : this.settings.icon; + + this.$widget.addClass(icon); } initialRenderCompleteEvent() { this.refreshIcon(); } + /** @param {string|function} icon */ icon(icon) { this.settings.icon = icon; return this; } + /** @param {string|function} title */ title(title) { this.settings.title = title; return this; } + /** @param {string} placement - "top", "bottom", "left", "right" */ titlePlacement(placement) { this.settings.titlePlacement = placement; return this; } - command(command) { - this.settings.command = command; - return this; - } - - onClick(handler) { - this.settings.onClick = handler; - return this; - } - onContextMenu(handler) { this.settings.onContextMenu = handler; } -} +} \ No newline at end of file diff --git a/src/public/app/widgets/buttons/button_from_note.js b/src/public/app/widgets/buttons/button_from_note.js index e6c021160..d8d08791e 100644 --- a/src/public/app/widgets/buttons/button_from_note.js +++ b/src/public/app/widgets/buttons/button_from_note.js @@ -1,8 +1,8 @@ -import ButtonWidget from "./button_widget.js"; import froca from "../../services/froca.js"; import attributeService from "../../services/attributes.js"; +import CommandButtonWidget from "./command_button.js"; -export default class ButtonFromNoteWidget extends ButtonWidget { +export default class ButtonFromNoteWidget extends CommandButtonWidget { constructor() { super(); diff --git a/src/public/app/widgets/buttons/close_pane_button.js b/src/public/app/widgets/buttons/close_pane_button.js index 6c2621afd..b3466d264 100644 --- a/src/public/app/widgets/buttons/close_pane_button.js +++ b/src/public/app/widgets/buttons/close_pane_button.js @@ -1,6 +1,6 @@ -import ButtonWidget from "./button_widget.js"; +import OnClickButtonWidget from "./onclick_button.js"; -export default class ClosePaneButton extends ButtonWidget { +export default class ClosePaneButton extends OnClickButtonWidget { isEnabled() { return super.isEnabled() // main note context should not be closeable diff --git a/src/public/app/widgets/buttons/command_button.js b/src/public/app/widgets/buttons/command_button.js new file mode 100644 index 000000000..376c5a822 --- /dev/null +++ b/src/public/app/widgets/buttons/command_button.js @@ -0,0 +1,54 @@ +import keyboardActionsService from "../../services/keyboard_actions.js"; +import AbstractButtonWidget from "./abstract_button.js"; + +let actions; + +keyboardActionsService.getActions().then(as => actions = as); + +export default class CommandButtonWidget extends AbstractButtonWidget { + doRender() { + super.doRender(); + + if (this.settings.command) { + this.$widget.on("click", () => { + this.$widget.tooltip("hide"); + + this.triggerCommand(this._command); + }); + } else { + console.warn(`Button widget '${this.componentId}' has no defined command`, this.settings); + } + } + + getTitle() { + const title = super.getTitle(); + + const action = actions.find(act => act.actionName === this._command); + + if (action && action.effectiveShortcuts.length > 0) { + return `${title} (${action.effectiveShortcuts.join(", ")})`; + } else { + return title; + } + } + + onClick(handler) { + this.settings.onClick = handler; + return this; + } + + /** + * @param {function|string} command + * @returns {CommandButtonWidget} + */ + command(command) { + this.settings.command = command; + return this; + } + + get _command() { + return typeof this.settings.command === "function" + ? this.settings.command() + : this.settings.command; + } +} diff --git a/src/public/app/widgets/buttons/create_pane_button.js b/src/public/app/widgets/buttons/create_pane_button.js index 9b48e0c46..8d38abdb9 100644 --- a/src/public/app/widgets/buttons/create_pane_button.js +++ b/src/public/app/widgets/buttons/create_pane_button.js @@ -1,6 +1,6 @@ -import ButtonWidget from "./button_widget.js"; +import OnClickButtonWidget from "./onclick_button.js"; -export default class CreatePaneButton extends ButtonWidget { +export default class CreatePaneButton extends OnClickButtonWidget { constructor() { super(); diff --git a/src/public/app/widgets/buttons/edit_button.js b/src/public/app/widgets/buttons/edit_button.js index 0b22c31c6..db4ddff54 100644 --- a/src/public/app/widgets/buttons/edit_button.js +++ b/src/public/app/widgets/buttons/edit_button.js @@ -1,9 +1,9 @@ -import ButtonWidget from "./button_widget.js"; +import OnClickButtonWidget from "./onclick_button.js"; import appContext from "../../components/app_context.js"; import attributeService from "../../services/attributes.js"; import protectedSessionHolder from "../../services/protected_session_holder.js"; -export default class EditButton extends ButtonWidget { +export default class EditButton extends OnClickButtonWidget { isEnabled() { return super.isEnabled() && this.noteContext; } diff --git a/src/public/app/widgets/buttons/launcher/abstract_launcher.js b/src/public/app/widgets/buttons/launcher/abstract_launcher.js new file mode 100644 index 000000000..f970710b8 --- /dev/null +++ b/src/public/app/widgets/buttons/launcher/abstract_launcher.js @@ -0,0 +1,33 @@ +import shortcutService from "../../../services/shortcuts.js"; +import OnClickButtonWidget from "../onclick_button.js"; + +export default class AbstractLauncher extends OnClickButtonWidget { + constructor(launcherNote) { + super(); + + /** @type {NoteShort} */ + this.launcherNote = launcherNote; + } + + launch() { + throw new Error("Abstract implementation"); + } + + bindNoteShortcutHandler(label) { + const namespace = label.attributeId; + + if (label.isDeleted) { + shortcutService.removeGlobalShortcut(namespace); + } else { + shortcutService.bindGlobalShortcut(label.value, () => this.launch(), namespace); + } + } + + entitiesReloadedEvent({loadResults}) { + for (const attr of loadResults.getAttributes()) { + if (attr.noteId === this.launcherNote.noteId && attr.type === 'label' && attr.name === 'keyboardShortcut') { + this.bindNoteShortcutHandler(attr); + } + } + } +} \ No newline at end of file diff --git a/src/public/app/widgets/buttons/launcher/note_launcher.js b/src/public/app/widgets/buttons/launcher/note_launcher.js new file mode 100644 index 000000000..91e483e80 --- /dev/null +++ b/src/public/app/widgets/buttons/launcher/note_launcher.js @@ -0,0 +1,44 @@ +import AbstractLauncher from "./abstract_launcher.js"; +import dialogService from "../../../services/dialog.js"; +import appContext from "../../../components/app_context.js"; + +export default class NoteLauncher extends AbstractLauncher { + constructor(launcherNote) { + super(launcherNote); + + this.title(this.launcherNote.title) + .icon(this.launcherNote.getIcon()) + .onClick(() => this.launch()); + } + + launch() { + // we're intentionally displaying the launcher title and icon instead of the target + // e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok), + // but on the launchpad you want them distinguishable. + // for titles, the note titles may follow a different scheme than maybe desirable on the launchpad + // another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons). + // The only (but major) downside is more work in setting up the typical case where you actually want to have both title and icon in sync. + const targetNoteId = this.launcherNote.getRelationValue('targetNote'); + + if (!targetNoteId) { + dialogService.info("This launcher doesn't define target note."); + return; + } + + appContext.tabManager.openTabWithNoteWithHoisting(targetNoteId, true); + } + + getTitle() { + const shortcuts = this.launcherNote.getLabels("keyboardShortcut") + .map(l => l.value) + .filter(v => !!v) + .join(", "); + + let title = super.getTitle(); + if (shortcuts) { + title += ` (${shortcuts})`; + } + + return title; + } +} \ No newline at end of file diff --git a/src/public/app/widgets/buttons/launcher/script_launcher.js b/src/public/app/widgets/buttons/launcher/script_launcher.js new file mode 100644 index 000000000..de3d3c0fb --- /dev/null +++ b/src/public/app/widgets/buttons/launcher/script_launcher.js @@ -0,0 +1,17 @@ +import AbstractLauncher from "./abstract_launcher.js"; + +export default class ScriptLauncher extends AbstractLauncher { + constructor(launcherNote) { + super(launcherNote); + + this.title(this.launcherNote.title) + .icon(this.launcherNote.getIcon()) + .onClick(this.handler); + } + + async launch() { + const script = await this.launcherNote.getRelationTarget('script'); + + await script.executeScript(); + } +} \ No newline at end of file diff --git a/src/public/app/widgets/buttons/left_pane_toggle.js b/src/public/app/widgets/buttons/left_pane_toggle.js index 451aa25fa..7cb1f626a 100644 --- a/src/public/app/widgets/buttons/left_pane_toggle.js +++ b/src/public/app/widgets/buttons/left_pane_toggle.js @@ -1,26 +1,28 @@ -import ButtonWidget from "./button_widget.js"; import options from "../../services/options.js"; import splitService from "../../services/resizer.js"; +import CommandButtonWidget from "./command_button.js"; -export default class LeftPaneToggleWidget extends ButtonWidget { - refreshIcon() { - const isLeftPaneVisible = options.is('leftPaneVisible'); +export default class LeftPaneToggleWidget extends CommandButtonWidget { + constructor() { + super(); - this.settings.icon = isLeftPaneVisible + this.settings.icon = () => options.is('leftPaneVisible') ? "bx-chevrons-left" : "bx-chevrons-right"; - this.settings.title = isLeftPaneVisible + this.settings.title = () => options.is('leftPaneVisible') ? "Hide panel" : "Open panel"; - this.settings.command = isLeftPaneVisible + this.settings.command = () => options.is('leftPaneVisible') ? "hideLeftPane" : "showLeftPane"; + } + refreshIcon() { super.refreshIcon(); - splitService.setupLeftPaneResizer(isLeftPaneVisible); + splitService.setupLeftPaneResizer(options.is('leftPaneVisible')); } entitiesReloadedEvent({loadResults}) { diff --git a/src/public/app/widgets/buttons/note_revisions_button.js b/src/public/app/widgets/buttons/note_revisions_button.js index 25f9eae3f..e40e8687d 100644 --- a/src/public/app/widgets/buttons/note_revisions_button.js +++ b/src/public/app/widgets/buttons/note_revisions_button.js @@ -1,6 +1,6 @@ -import ButtonWidget from "./button_widget.js"; +import CommandButtonWidget from "./command_button.js"; -export default class NoteRevisionsButton extends ButtonWidget { +export default class NoteRevisionsButton extends CommandButtonWidget { constructor() { super(); diff --git a/src/public/app/widgets/buttons/onclick_button.js b/src/public/app/widgets/buttons/onclick_button.js new file mode 100644 index 000000000..00e724054 --- /dev/null +++ b/src/public/app/widgets/buttons/onclick_button.js @@ -0,0 +1,22 @@ +import AbstractButtonWidget from "./abstract_button.js"; + +export default class OnClickButtonWidget extends AbstractButtonWidget { + doRender() { + super.doRender(); + + if (this.settings.onClick) { + this.$widget.on("click", e => { + this.$widget.tooltip("hide"); + + this.settings.onClick(this, e); + }); + } else { + console.warn(`Button widget '${this.componentId}' has no defined click handler`, this.settings); + } + } + + onClick(handler) { + this.settings.onClick = handler; + return this; + } +} diff --git a/src/public/app/widgets/buttons/open_note_button_widget.js b/src/public/app/widgets/buttons/open_note_button_widget.js index 6bef9b6cf..b38b81f23 100644 --- a/src/public/app/widgets/buttons/open_note_button_widget.js +++ b/src/public/app/widgets/buttons/open_note_button_widget.js @@ -1,10 +1,10 @@ -import ButtonWidget from "./button_widget.js"; +import OnClickButtonWidget from "./onclick_button.js"; import appContext from "../../components/app_context.js"; import froca from "../../services/froca.js"; // FIXME: this widget might not be useful anymore -export default class OpenNoteButtonWidget extends ButtonWidget { +export default class OpenNoteButtonWidget extends OnClickButtonWidget { targetNote(noteId) { froca.getNote(noteId).then(note => { if (!note) { diff --git a/src/public/app/widgets/buttons/protected_session_status.js b/src/public/app/widgets/buttons/protected_session_status.js index 18f9aa819..32fe14c7c 100644 --- a/src/public/app/widgets/buttons/protected_session_status.js +++ b/src/public/app/widgets/buttons/protected_session_status.js @@ -1,29 +1,24 @@ -import ButtonWidget from "./button_widget.js"; import protectedSessionHolder from "../../services/protected_session_holder.js"; +import CommandButtonWidget from "./command_button.js"; -export default class ProtectedSessionStatusWidget extends ButtonWidget { - doRender() { - this.updateSettings(); +export default class ProtectedSessionStatusWidget extends CommandButtonWidget { + constructor() { + super(); - super.doRender(); - } - - updateSettings() { - this.settings.icon = protectedSessionHolder.isProtectedSessionAvailable() + this.settings.icon = () => protectedSessionHolder.isProtectedSessionAvailable() ? "bx-check-shield" : "bx-shield-quarter"; - this.settings.title = protectedSessionHolder.isProtectedSessionAvailable() + this.settings.title = () => protectedSessionHolder.isProtectedSessionAvailable() ? "Protected session is active. Click to leave protected session." : "Click to enter protected session"; - this.settings.command = protectedSessionHolder.isProtectedSessionAvailable() + this.settings.command = () => protectedSessionHolder.isProtectedSessionAvailable() ? "leaveProtectedSession" : "enterProtectedSession"; } protectedSessionStartedEvent() { - this.updateSettings(); this.refreshIcon(); } } diff --git a/src/public/app/widgets/containers/launcher.js b/src/public/app/widgets/containers/launcher.js index daec1e69a..67977437d 100644 --- a/src/public/app/widgets/containers/launcher.js +++ b/src/public/app/widgets/containers/launcher.js @@ -1,6 +1,4 @@ -import ButtonWidget from "../buttons/button_widget.js"; -import dialogService from "../../services/dialog.js"; -import appContext from "../../components/app_context.js"; +import OnClickButtonWidget from "../buttons/onclick_button.js"; import CalendarWidget from "../buttons/calendar.js"; import SpacerWidget from "../spacer.js"; import BookmarkButtons from "../bookmark_buttons.js"; @@ -9,19 +7,15 @@ import SyncStatusWidget from "../sync_status.js"; import BackInHistoryButtonWidget from "../buttons/history/history_back.js"; import ForwardInHistoryButtonWidget from "../buttons/history/history_forward.js"; import BasicWidget from "../basic_widget.js"; -import shortcutService from "../../services/shortcuts.js"; +import NoteLauncher from "../buttons/launcher/note_launcher.js"; +import ScriptLauncher from "../buttons/launcher/script_launcher.js"; +import CommandButtonWidget from "../buttons/command_button.js"; export default class LauncherWidget extends BasicWidget { - constructor(launcherNote) { + constructor() { super(); - - if (launcherNote.type !== 'launcher') { - throw new Error(`Note '${this.note.noteId}' '${this.note.title}' is not a launcher even though it's in the launcher subtree`); - } - - this.note = launcherNote; + this.innerWidget = null; - this.handler = null; } isEnabled() { @@ -32,118 +26,74 @@ export default class LauncherWidget extends BasicWidget { this.$widget = this.innerWidget.render(); } - async initLauncher() { - const launcherType = this.note.getLabelValue("launcherType"); + async initLauncher(note) { + if (note.type !== 'launcher') { + throw new Error(`Note '${note.noteId}' '${note.title}' is not a launcher even though it's in the launcher subtree`); + } + + const launcherType = note.getLabelValue("launcherType"); if (launcherType === 'command') { - this.handler = () => this.triggerCommand(this.note.getLabelValue("command")); - - this.innerWidget = new ButtonWidget() - .title(this.note.title) - .icon(this.note.getIcon()) - .onClick(this.handler); + this.innerWidget = this.initCommandWidget(note); } else if (launcherType === 'note') { - // we're intentionally displaying the launcher title and icon instead of the target - // e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok), - // but on the launchpad you want them distinguishable. - // for titles, the note titles may follow a different scheme than maybe desirable on the launchpad - // another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons). - // The only (but major) downside is more work in setting up the typical case where you actually want to have both title and icon in sync. - - this.handler = () => { - const targetNoteId = this.note.getRelationValue('targetNote'); - - if (!targetNoteId) { - dialogService.info("This launcher doesn't define target note."); - return; - } - - appContext.tabManager.openTabWithNoteWithHoisting(targetNoteId, true) - }; - - this.innerWidget = new ButtonWidget() - .title(this.note.title) - .icon(this.note.getIcon()) - .onClick(this.handler); + this.innerWidget = new NoteLauncher(note); } else if (launcherType === 'script') { - this.handler = async () => { - const script = await this.note.getRelationTarget('script'); - - await script.executeScript(); - }; - - this.innerWidget = new ButtonWidget() - .title(this.note.title) - .icon(this.note.getIcon()) - .onClick(this.handler); + this.innerWidget = new ScriptLauncher(note); } else if (launcherType === 'customWidget') { - const widget = await this.note.getRelationTarget('widget'); - - if (widget) { - this.innerWidget = await widget.executeScript(); - } else { - throw new Error(`Could not initiate custom widget of launcher '${this.note.noteId}' '${this.note.title}`); - } + this.innerWidget = await this.initCustomWidget(note); } else if (launcherType === 'builtinWidget') { - const builtinWidget = this.note.getLabelValue("builtinWidget"); - - if (builtinWidget) { - if (builtinWidget === 'calendar') { - this.innerWidget = new CalendarWidget(this.note.title, this.note.getIcon()); - } else if (builtinWidget === 'spacer') { - // || has to be inside since 0 is a valid value - const baseSize = parseInt(this.note.getLabelValue("baseSize") || "40"); - const growthFactor = parseInt(this.note.getLabelValue("growthFactor") || "100"); - - this.innerWidget = new SpacerWidget(baseSize, growthFactor); - } else if (builtinWidget === 'bookmarks') { - this.innerWidget = new BookmarkButtons(); - } else if (builtinWidget === 'protectedSession') { - this.innerWidget = new ProtectedSessionStatusWidget(); - } else if (builtinWidget === 'syncStatus') { - this.innerWidget = new SyncStatusWidget(); - } else if (builtinWidget === 'backInHistoryButton') { - this.innerWidget = new BackInHistoryButtonWidget(); - } else if (builtinWidget === 'forwardInHistoryButton') { - this.innerWidget = new ForwardInHistoryButtonWidget(); - } else { - throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${this.note.noteId} "${this.note.title}"`); - } - } + this.innerWidget = this.initBuiltinWidget(note); } else { - throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${this.note.noteId}' title ${this.note.title}`); + throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title ${note.title}`); } if (!this.innerWidget) { - throw new Error(`Unknown initialization error for note '${this.note.noteId}', title '${this.note.title}'`); - } - - for (const label of this.note.getLabels('keyboardShortcut')) { - this.bindNoteShortcutHandler(label); + throw new Error(`Unknown initialization error for note '${note.noteId}', title '${note.title}'`); } this.child(this.innerWidget); } - bindNoteShortcutHandler(label) { - if (!this.handler) { - return; - } + initCommandWidget(note) { + return new CommandButtonWidget() + .title(note.title) + .icon(note.getIcon()) + .command(() => note.getLabelValue("command")); + } - const namespace = label.attributeId; + async initCustomWidget(note) { + const widget = await note.getRelationTarget('widget'); - if (label.isDeleted) { - shortcutService.removeGlobalShortcut(namespace); + if (widget) { + return await widget.executeScript(); } else { - shortcutService.bindGlobalShortcut(label.value, this.handler, namespace); + throw new Error(`Custom widget of launcher '${note.noteId}' '${note.title}' is not defined.`); } } - entitiesReloadedEvent({loadResults}) { - for (const attr of loadResults.getAttributes()) { - if (attr.noteId === this.note.noteId && attr.type === 'label' && attr.name === 'keyboardShortcut') { - this.bindNoteShortcutHandler(attr); - } + initBuiltinWidget(note) { + const builtinWidget = note.getLabelValue("builtinWidget"); + + if (builtinWidget === 'calendar') { + return new CalendarWidget(note.title, note.getIcon()); + } else if (builtinWidget === 'spacer') { + // || has to be inside since 0 is a valid value + const baseSize = parseInt(note.getLabelValue("baseSize") || "40"); + const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100"); + + return new SpacerWidget(baseSize, growthFactor); + } else if (builtinWidget === 'bookmarks') { + return new BookmarkButtons(); + } else if (builtinWidget === 'protectedSession') { + return new ProtectedSessionStatusWidget(); + } else if (builtinWidget === 'syncStatus') { + return new SyncStatusWidget(); + } else if (builtinWidget === 'backInHistoryButton') { + return new BackInHistoryButtonWidget(); + } else if (builtinWidget === 'forwardInHistoryButton') { + return new ForwardInHistoryButtonWidget(); + } else { + throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); } } } diff --git a/src/public/app/widgets/containers/launcher_container.js b/src/public/app/widgets/containers/launcher_container.js index 8e661adda..3ff2755e0 100644 --- a/src/public/app/widgets/containers/launcher_container.js +++ b/src/public/app/widgets/containers/launcher_container.js @@ -29,12 +29,12 @@ export default class LauncherContainer extends FlexContainer { (await visibleLaunchersRoot.getChildNotes()) .map(async launcherNote => { try { - const launcherWidget = new LauncherWidget(launcherNote); - await launcherWidget.initLauncher(); + const launcherWidget = new LauncherWidget(); + await launcherWidget.initLauncher(launcherNote); this.child(launcherWidget); } catch (e) { - console.error(e.message); + console.error(e); } }) ); diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 4043aa1fa..f59e93a85 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -19,6 +19,7 @@ import options from "../services/options.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import dialogService from "../services/dialog.js"; import shortcutService from "../services/shortcuts.js"; +import LauncherContextMenu from "../menus/launcher_context_menu.js"; const TPL = `