diff --git a/db/migrations/0203__set_notes_as_doc_type.sql b/db/migrations/0203__set_notes_as_doc_type.sql new file mode 100644 index 000000000..140e945ad --- /dev/null +++ b/db/migrations/0203__set_notes_as_doc_type.sql @@ -0,0 +1 @@ +UPDATE notes SET type = 'doc' WHERE noteId IN ('share, hidden', 'sqlconsole', 'search'); \ No newline at end of file diff --git a/src/public/app/doc_notes/hidden.html b/src/public/app/doc_notes/hidden.html new file mode 100644 index 000000000..98e79fd93 --- /dev/null +++ b/src/public/app/doc_notes/hidden.html @@ -0,0 +1 @@ +

Hidden tree is used to record various application-level data which can stay most of the time hidden from the user view.

\ No newline at end of file diff --git a/src/public/app/doc_notes/share.html b/src/public/app/doc_notes/share.html new file mode 100644 index 000000000..499cf35e8 --- /dev/null +++ b/src/public/app/doc_notes/share.html @@ -0,0 +1 @@ +

Here you can find all shared notes.

\ No newline at end of file diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js index 15dad902e..b918d2470 100644 --- a/src/public/app/entities/note_short.js +++ b/src/public/app/entities/note_short.js @@ -21,7 +21,8 @@ const NOTE_TYPE_ICONS = { "mermaid": "bx bx-selection", "canvas": "bx bx-pen", "web-view": "bx bx-globe-alt", - "shortcut": "bx bx-up-arrow-circle" + "shortcut": "bx bx-up-arrow-circle", + "doc": "bx bx-file-doc" }; /** diff --git a/src/public/app/menus/shortcut_context_menu.js b/src/public/app/menus/shortcut_context_menu.js index 74b05f072..ba78003ee 100644 --- a/src/public/app/menus/shortcut_context_menu.js +++ b/src/public/app/menus/shortcut_context_menu.js @@ -1,6 +1,8 @@ import treeService from '../services/tree.js'; import froca from "../services/froca.js"; import contextMenu from "./context_menu.js"; +import dialogService from "../services/dialog.js"; +import server from "../services/server.js"; export default class ShortcutContextMenu { /** @@ -25,12 +27,13 @@ export default class ShortcutContextMenu { const note = await froca.getNote(this.node.data.noteId); const parentNoteId = this.node.getParent().data.noteId; - const isLbRoot = note.noteId === 'lb_root'; const isVisibleRoot = note.noteId === 'lb_visibleshortcuts'; const isAvailableRoot = note.noteId === 'lb_availableshortcuts'; const isVisibleItem = parentNoteId === 'lb_visibleshortcuts'; const isAvailableItem = parentNoteId === 'lb_availableshortcuts'; const isItem = isVisibleItem || isAvailableItem; + const canBeDeleted = !note.noteId.startsWith("lb_"); + const canBeReset = note.noteId.startsWith("lb_"); return [ (isVisibleRoot || isAvailableRoot) ? { title: 'Add note shortcut', command: 'addNoteShortcut', uiIcon: "bx bx-plus" } : null, @@ -38,8 +41,8 @@ export default class ShortcutContextMenu { (isVisibleRoot || isAvailableRoot) ? { title: 'Add widget shortcut', command: 'addWidgetShortcut', uiIcon: "bx bx-plus" } : null, (isVisibleRoot || isAvailableRoot) ? { title: 'Add spacer', command: 'addSpacerShortcut', uiIcon: "bx bx-plus" } : null, (isVisibleRoot || isAvailableRoot) ? { title: "----" } : null, - { title: 'Delete ', command: "deleteNotes", uiIcon: "bx bx-trash", - enabled: !isLbRoot}, // allow everything to be deleted as a form of a reset. Root can't be deleted because it's a hoisted note + { title: 'Delete ', command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted }, + { title: 'Reset', command: "resetShortcut", uiIcon: "bx bx-empty", enabled: canBeReset}, { title: "----" }, isAvailableItem ? { title: 'Move to visible shortcuts', command: "moveShortcutToVisible", uiIcon: "bx bx-show", enabled: true } : null, isVisibleItem ? { title: 'Move to available shortcuts', command: "moveShortcutToAvailable", uiIcon: "bx bx-hide", enabled: true } : null, @@ -49,6 +52,18 @@ export default class ShortcutContextMenu { } async selectMenuItemHandler({command}) { + if (command === 'resetShortcut') { + const confirmed = await dialogService.confirm(`Do you really want to reset "${this.node.title}"? + All data / settings in this shortcut (and its children) will be lost + and the shortcut will be returned to its original location.`); + + if (confirmed) { + await server.post(`special-notes/shortcuts/${this.node.data.noteId}/reset`); + } + + return; + } + this.treeWidget.triggerCommand(command, { node: this.node, notePath: treeService.getNotePath(this.node), diff --git a/src/public/app/services/branches.js b/src/public/app/services/branches.js index 0b885bb02..a1a9bb4d1 100644 --- a/src/public/app/services/branches.js +++ b/src/public/app/services/branches.js @@ -10,8 +10,8 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) { branchIdsToMove = filterRootNote(branchIdsToMove); branchIdsToMove = filterSearchBranches(branchIdsToMove); - if (beforeBranchId === 'root') { - toastService.showError('Cannot move notes before root note.'); + if (['root', 'lb_root', 'lb_availableshortcuts', 'lb_visibleshortcuts'].includes(beforeBranchId)) { + toastService.showError('Cannot move notes here.'); return; } @@ -31,8 +31,16 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) { const afterNote = await froca.getBranch(afterBranchId).getNote(); - if (afterNote.noteId === 'root' || afterNote.noteId === hoistedNoteService.getHoistedNoteId()) { - toastService.showError('Cannot move notes after root note.'); + const forbiddenNoteIds = [ + 'root', + hoistedNoteService.getHoistedNoteId(), + 'lb_root', + 'lb_availableshortcuts', + 'lb_visibleshortcuts' + ]; + + if (forbiddenNoteIds.includes(afterNote.noteId)) { + toastService.showError('Cannot move notes here.'); return; } @@ -49,6 +57,11 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) { } async function moveToParentNote(branchIdsToMove, newParentBranchId) { + if (newParentBranchId === 'lb_root') { + toastService.showError('Cannot move notes here.'); + return; + } + branchIdsToMove = filterRootNote(branchIdsToMove); for (const branchIdToMove of branchIdsToMove) { diff --git a/src/public/app/widgets/dialog.js b/src/public/app/services/dialog.js similarity index 93% rename from src/public/app/widgets/dialog.js rename to src/public/app/services/dialog.js index 7c7b935a9..9a401e52f 100644 --- a/src/public/app/widgets/dialog.js +++ b/src/public/app/services/dialog.js @@ -1,4 +1,4 @@ -import appContext from "../services/app_context.js"; +import appContext from "./app_context.js"; async function info(message) { return new Promise(res => diff --git a/src/public/app/services/hoisted_note.js b/src/public/app/services/hoisted_note.js index b188d27a0..361276c86 100644 --- a/src/public/app/services/hoisted_note.js +++ b/src/public/app/services/hoisted_note.js @@ -1,6 +1,6 @@ import appContext from "./app_context.js"; import treeService from "./tree.js"; -import dialogService from "../widgets/dialog.js"; +import dialogService from "./dialog.js"; import froca from "./froca.js"; function getHoistedNoteId() { diff --git a/src/public/app/services/root_command_executor.js b/src/public/app/services/root_command_executor.js index e56242007..764e34e8d 100644 --- a/src/public/app/services/root_command_executor.js +++ b/src/public/app/services/root_command_executor.js @@ -74,7 +74,13 @@ export default class RootCommandExecutor extends Component { async showLaunchBarShortcutsCommand() { await appContext.tabManager.openContextWithNote('lb_root', true, null, 'lb_root'); + } + async showShareSubtreeCommand() { + await appContext.tabManager.openContextWithNote('share', true, null, 'share'); + } + async showHiddenSubtreeCommand() { + await appContext.tabManager.openContextWithNote('hidden', true, null, 'hidden'); } } diff --git a/src/public/app/widgets/buttons/button_from_note.js b/src/public/app/widgets/buttons/button_from_note.js new file mode 100644 index 000000000..bafc809b4 --- /dev/null +++ b/src/public/app/widgets/buttons/button_from_note.js @@ -0,0 +1,40 @@ +import ButtonWidget from "./button_widget.js"; +import froca from "../../services/froca.js"; +import attributeService from "../../services/attributes.js"; + +export default class ButtonFromNoteWidget extends ButtonWidget { + constructor() { + super(); + + this.settings.buttonNoteId = null; + } + + buttonNoteId(noteId) { + this.settings.buttonNoteId = noteId; + return this; + } + + doRender() { + super.doRender(); + + this.updateIcon(); + } + + updateIcon() { + froca.getNote(this.settings.buttonNoteId).then(note => { + this.settings.icon = note.getLabelValue("iconClass"); + + this.refreshIcon(); + }); + } + + entitiesReloadedEvent({loadResults}) { + if (loadResults.getAttributes(this.componentId).find(attr => + attr.type === 'label' + && attr.name === 'iconClass' + && attributeService.isAffecting(attr, this.note))) { + + this.updateIcon(); + } + } +} \ No newline at end of file diff --git a/src/public/app/widgets/buttons/button_widget.js b/src/public/app/widgets/buttons/button_widget.js index 92f4d701d..623facfe6 100644 --- a/src/public/app/widgets/buttons/button_widget.js +++ b/src/public/app/widgets/buttons/button_widget.js @@ -18,7 +18,12 @@ export default class ButtonWidget extends NoteContextAwareWidget { super(); this.settings = { - titlePlacement: 'right' + titlePlacement: 'right', + title: null, + icon: null, + command: null, + onClick: null, + onContextMenu: null }; } @@ -39,6 +44,14 @@ export default class ButtonWidget extends NoteContextAwareWidget { }); } + if (this.settings.onContextMenu) { + this.$widget.on("contextmenu", e => { + this.$widget.tooltip("hide"); + + this.settings.onContextMenu(e); + }); + } + this.$widget.attr("data-placement", this.settings.titlePlacement); this.$widget.tooltip({ @@ -70,8 +83,7 @@ export default class ButtonWidget extends NoteContextAwareWidget { } } - this.$widget - .addClass(this.settings.icon); + this.$widget.addClass(this.settings.icon); } initialRenderCompleteEvent() { @@ -102,4 +114,8 @@ export default class ButtonWidget extends NoteContextAwareWidget { this.settings.onClick = handler; return this; } + + onContextMenu(handler) { + this.settings.onContextMenu = handler; + } } diff --git a/src/public/app/widgets/buttons/global_menu.js b/src/public/app/widgets/buttons/global_menu.js index 45fa44133..0ae7bf1db 100644 --- a/src/public/app/widgets/buttons/global_menu.js +++ b/src/public/app/widgets/buttons/global_menu.js @@ -124,11 +124,16 @@ const TPL = ` - + + + + diff --git a/src/public/app/widgets/history_navigation.js b/src/public/app/widgets/buttons/history/abstract_history.js similarity index 58% rename from src/public/app/widgets/history_navigation.js rename to src/public/app/widgets/buttons/history/abstract_history.js index 182cad0c1..c356961e1 100644 --- a/src/public/app/widgets/history_navigation.js +++ b/src/public/app/widgets/buttons/history/abstract_history.js @@ -1,48 +1,17 @@ -import BasicWidget from "./basic_widget.js"; -import utils from "../services/utils.js"; -import contextMenu from "../menus/context_menu.js"; -import treeService from "../services/tree.js"; +import utils from "../../../services/utils.js"; +import contextMenu from "../../../menus/context_menu.js"; +import treeService from "../../../services/tree.js"; +import ButtonFromNoteWidget from "../button_from_note.js"; -const TPL = ` -
- - - - -
-`; - -export default class HistoryNavigationWidget extends BasicWidget { doRender() { - if (!utils.isElectron()) { - this.$widget = $("
"); - return; - } + super.doRender(); - this.$widget = $(TPL); - - const contextMenuHandler = e => { - e.preventDefault(); - - if (this.webContents.history.length < 2) { - return; - } - - this.showContextMenu(e); - }; - - this.$backInHistory = this.$widget.find("[data-trigger-command='backInNoteHistory']"); - this.$backInHistory.on('contextmenu', contextMenuHandler); - - this.$forwardInHistory = this.$widget.find("[data-trigger-command='forwardInNoteHistory']"); - this.$forwardInHistory.on('contextmenu', contextMenuHandler); - - this.webContents = utils.dynamicRequire('@electron/remote').webContents; + this.webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents(); // without this the history is preserved across frontend reloads this.webContents.clearHistory(); @@ -51,6 +20,16 @@ export default class HistoryNavigationWidget extends BasicWidget { } async showContextMenu(e) { + e.preventDefault(); + + // API is broken and will be replaced: https://github.com/electron/electron/issues/33899 + // until then no context menu + return; + + if (this.webContents.history.length < 2) { + return; + } + let items = []; const activeIndex = this.webContents.getActiveIndex(); diff --git a/src/public/app/widgets/buttons/history/history_back.js b/src/public/app/widgets/buttons/history/history_back.js new file mode 100644 index 000000000..63fc51f58 --- /dev/null +++ b/src/public/app/widgets/buttons/history/history_back.js @@ -0,0 +1,14 @@ +import AbstractHistoryNavigationWidget from "./abstract_history.js"; + +export default class BackInHistoryButtonWidget extends AbstractHistoryNavigationWidget { + constructor() { + super(); + + this.icon('bx-left-arrow-circle') + .title("Go to previous note.") + .command("backInNoteHistory") + .titlePlacement("right") + .buttonNoteId('lb_backinhistory') + .onContextMenu(e => this.showContextMenu(e)); + } +} diff --git a/src/public/app/widgets/buttons/history/history_forward.js b/src/public/app/widgets/buttons/history/history_forward.js new file mode 100644 index 000000000..9b5fde004 --- /dev/null +++ b/src/public/app/widgets/buttons/history/history_forward.js @@ -0,0 +1,14 @@ +import AbstractHistoryNavigationWidget from "./abstract_history.js"; + +export default class ForwardInHistoryButtonWidget extends AbstractHistoryNavigationWidget { + constructor() { + super(); + + this.icon('bx-left-arrow-circle') + .title("Go to next note.") + .command("forwardInNoteHistory") + .titlePlacement("right") + .buttonNoteId('lb_forwardinhistory') + .onContextMenu(e => this.showContextMenu(e)); + } +} diff --git a/src/public/app/widgets/buttons/note_revisions_button.js b/src/public/app/widgets/buttons/note_revisions_button.js index a8bb1945f..dace67dcb 100644 --- a/src/public/app/widgets/buttons/note_revisions_button.js +++ b/src/public/app/widgets/buttons/note_revisions_button.js @@ -11,6 +11,6 @@ export default class NoteRevisionsButton extends ButtonWidget { } isEnabled() { - return super.isEnabled() && this.note?.type !== 'shortcut'; + return super.isEnabled() && !['shortcut', 'doc'].includes(this.note?.type); } } \ No newline at end of file diff --git a/src/public/app/widgets/buttons/show_note_source.js b/src/public/app/widgets/buttons/show_note_source.js deleted file mode 100644 index c3d8c7d1c..000000000 --- a/src/public/app/widgets/buttons/show_note_source.js +++ /dev/null @@ -1,16 +0,0 @@ -import ButtonWidget from "./button_widget.js"; - -export default class ShowNoteSourceButton extends ButtonWidget { - isEnabled() { - return super.isEnabled() && this.note && ['text', 'relation-map'].includes(this.note.type); - } - - constructor() { - super(); - - this.icon('bx bx-code') - .title("Show Note Source") - .command("openNoteSourceDialog") - .titlePlacement("bottom"); - } -} diff --git a/src/public/app/widgets/containers/shortcut_container.js b/src/public/app/widgets/containers/shortcut_container.js index ee8f30fe5..9422c5812 100644 --- a/src/public/app/widgets/containers/shortcut_container.js +++ b/src/public/app/widgets/containers/shortcut_container.js @@ -7,6 +7,8 @@ import SpacerWidget from "../spacer.js"; import BookmarkButtons from "../bookmark_buttons.js"; import ProtectedSessionStatusWidget from "../buttons/protected_session_status.js"; import SyncStatusWidget from "../sync_status.js"; +import BackInHistoryButtonWidget from "../buttons/history/history_back.js"; +import ForwardInHistoryButtonWidget from "../buttons/history/history_forward.js"; export default class ShortcutContainer extends FlexContainer { constructor() { @@ -63,6 +65,10 @@ export default class ShortcutContainer extends FlexContainer { this.child(new ProtectedSessionStatusWidget()); } else if (builtinWidget === 'syncStatus') { this.child(new SyncStatusWidget()); + } else if (builtinWidget === 'backInHistoryButton') { + this.child(new BackInHistoryButtonWidget()); + } else if (builtinWidget === 'forwardInHistoryButton') { + this.child(new ForwardInHistoryButtonWidget()); } else { console.log(`Unrecognized builtin widget ${builtinWidget} for shortcut ${shortcut.noteId} "${shortcut.title}"`); } diff --git a/src/public/app/widgets/dialogs/note_revisions.js b/src/public/app/widgets/dialogs/note_revisions.js index 7df8ea592..9560ad575 100644 --- a/src/public/app/widgets/dialogs/note_revisions.js +++ b/src/public/app/widgets/dialogs/note_revisions.js @@ -6,7 +6,7 @@ import libraryLoader from "../../services/library_loader.js"; import openService from "../../services/open.js"; import protectedSessionHolder from "../../services/protected_session_holder.js"; import BasicWidget from "../basic_widget.js"; -import dialogService from "../dialog.js"; +import dialogService from "../../services/dialog.js"; const TPL = `