@@ -58,13 +58,13 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
this.deleteNoteOnEscape = false;
});
- utils.bindElShortcut(this.$noteTitle, 'esc', () => {
+ shortcutService.bindElShortcut(this.$noteTitle, 'esc', () => {
if (this.deleteNoteOnEscape && this.noteContext.isActive()) {
branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
}
});
- utils.bindElShortcut(this.$noteTitle, 'return', () => {
+ shortcutService.bindElShortcut(this.$noteTitle, 'return', () => {
this.triggerCommand('focusOnDetail', {ntxId: this.noteContext.ntxId});
});
}
@@ -72,7 +72,8 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
async refreshWithNote(note) {
this.$noteTitle.val(note.title);
- this.$noteTitle.prop("readonly", note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable());
+ this.$noteTitle.prop("readonly", (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable())
+ || ["lbRoot", "lbAvailableLaunchers", "lbVisibleLaunchers"].includes(note.noteId));
this.setProtectedStatus(note);
}
diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js
index 1de9aa5a5..6a277c2ee 100644
--- a/src/public/app/widgets/note_tree.js
+++ b/src/public/app/widgets/note_tree.js
@@ -1,7 +1,7 @@
import hoistedNoteService from "../services/hoisted_note.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
-import contextMenu from "../services/context_menu.js";
+import contextMenu from "../menus/context_menu.js";
import froca from "../services/froca.js";
import branchService from "../services/branches.js";
import ws from "../services/ws.js";
@@ -9,7 +9,7 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js";
import noteCreateService from "../services/note_create.js";
import toastService from "../services/toast.js";
-import appContext from "../services/app_context.js";
+import appContext from "../components/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js";
@@ -17,7 +17,9 @@ import linkService from "../services/link.js";
import syncService from "../services/sync.js";
import options from "../services/options.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
-import dialogService from "./dialog.js";
+import dialogService from "../services/dialog.js";
+import shortcutService from "../services/shortcuts.js";
+import LauncherContextMenu from "../menus/launcher_context_menu.js";
const TPL = `
@@ -88,6 +90,10 @@ const TPL = `
width: 340px;
border-radius: 10px;
}
+
+ .tree .hidden-node-is-hidden {
+ display: none;
+ }
@@ -143,10 +149,10 @@ const TPL = `
const MAX_SEARCH_RESULTS_IN_TREE = 100;
export default class NoteTreeWidget extends NoteContextAwareWidget {
- constructor(treeName) {
+ constructor() {
super();
- this.treeName = treeName;
+ this.treeName = "main"; // legacy value
}
doRender() {
@@ -232,7 +238,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.reloadTreeFromCache();
});
- this.initFancyTree();
+ // note tree starts initializing already during render which is atypical
+ Promise.all([options.initializedPromise, froca.initializedPromise]).then(() => this.initFancyTree());
this.setupNoteTitleTooltip();
}
@@ -362,6 +369,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return false;
}
},
+ beforeActivate: (event, data) => {
+ // hidden subtree is hidden hackily, prevent activating it e.g. by keyboard
+ return hoistedNoteService.getHoistedNoteId() === 'hidden' || data.node.data.noteId !== 'hidden';
+ },
activate: async (event, data) => {
// click event won't propagate so let's close context menu manually
contextMenu.hide();
@@ -372,10 +383,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const activeNoteContext = appContext.tabManager.getActiveContext();
await activeNoteContext.setNote(notePath);
-
- if (utils.isMobile()) {
- this.triggerCommand('setActiveScreen', {screen: 'detail'});
- }
},
expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
@@ -388,6 +395,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
autoExpandMS: 600,
preventLazyParents: false,
dragStart: (node, data) => {
+ if (['root', 'hidden', 'lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(node.data.noteId)
+ || node.data.noteId.startsWith("options")) {
+ return false;
+ }
+
const notes = this.getSelectedOrActiveNodes(node).map(node => ({
noteId: node.data.noteId,
branchId: node.data.branchId,
@@ -409,7 +421,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
data.dataTransfer.setData("text", JSON.stringify(notes));
return true; // allow dragging to start
},
- dragEnter: (node, data) => node.data.noteType !== 'search',
+ dragEnter: (node, data) => {
+ if (node.data.noteType === 'search') {
+ return false;
+ } else if (node.data.noteId === 'lbRoot') {
+ return false;
+ } else if (node.data.noteId.startsWith('options')) {
+ return false;
+ } else if (node.data.noteType === 'launcher') {
+ return ['before', 'after'];
+ } else {
+ return true;
+ }
+ },
dragDrop: async (node, data) => {
if ((data.hitMode === 'over' && node.data.noteType === 'search') ||
(['after', 'before'].includes(data.hitMode)
@@ -425,7 +449,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (dataTransfer && dataTransfer.files && dataTransfer.files.length > 0) {
const files = [...dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
- const importService = await import('../services/import.js');
+ const importService = await import('../services/import');
importService.uploadFiles(node.data.noteId, files, {
safeImport: true,
@@ -526,7 +550,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (isHoistedNote) {
const $unhoistButton = $('
');
- $span.append($unhoistButton);
+ // unhoist button is prepended since compared to other buttons this is not just convenience
+ // on the mobile interface - it's the only way to unhoist
+ $span.prepend($unhoistButton);
}
if (note.hasLabel('workspace') && !isHoistedNote) {
@@ -541,7 +567,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
$span.append($refreshSearchButton);
}
- if (note.type !== 'search') {
+ if (!['search', 'launcher'].includes(note.type) && !note.isOptions() && !note.isLaunchBarConfig()) {
const $createChildNoteButton = $('
');
$span.append($createChildNoteButton);
@@ -580,10 +606,17 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$tree.on('contextmenu', '.fancytree-node', e => {
const node = $.ui.fancytree.getNode(e);
- import("../services/tree_context_menu.js").then(({default: TreeContextMenu}) => {
- const treeContextMenu = new TreeContextMenu(this, node);
- treeContextMenu.show(e);
- });
+ if (hoistedNoteService.getHoistedNoteId() === 'lbRoot') {
+ import("../menus/launcher_context_menu.js").then(({LauncherContextMenu: ShortcutContextMenu}) => {
+ const shortcutContextMenu = new LauncherContextMenu(this, node);
+ shortcutContextMenu.show(e);
+ });
+ } else {
+ import("../menus/tree_context_menu.js").then(({default: TreeContextMenu}) => {
+ const treeContextMenu = new TreeContextMenu(this, node);
+ treeContextMenu.show(e);
+ });
+ }
return false; // blocks default browser right click menu
});
@@ -612,10 +645,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
for (const branch of childBranches) {
- if (branch.noteId === 'hidden') {
- continue;
- }
-
if (hideArchivedNotes) {
const note = branch.getNoteFromCache();
@@ -713,12 +742,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
extraClasses.push("shared");
}
else if (note.getParentNoteIds().length > 1) {
- const notSearchParents = note.getParentNoteIds()
+ const realClones = note.getParentNoteIds()
.map(noteId => froca.notes[noteId])
.filter(note => !!note)
- .filter(note => note.type !== 'search');
+ .filter(note =>
+ !['share', 'lbBookmarks'].includes(note.noteId)
+ && note.type !== 'search');
- if (notSearchParents.length > 1) {
+ if (realClones.length > 1) {
extraClasses.push("multiple-parents");
}
}
@@ -788,6 +819,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await node.setExpanded(isExpanded, {noEvents: true, noAnimation: true});
}
});
+
+ const activeNode = await this.getNodeFromPath(appContext.tabManager.getActiveContextNotePath());
+
+ if (activeNode) {
+ activeNode.setActive({noEvents: true, noFocus: false});
+ }
}
async expandTree(node = null) {
@@ -953,7 +990,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (this.noteContext
&& this.noteContext.notePath
&& !this.noteContext.note?.isDeleted
- && !this.noteContext.notePath.includes("root/hidden")
+ && (!treeService.isNotePathInHiddenSubtree(this.noteContext.notePath) || await hoistedNoteService.isHoistedInHiddenSubtree())
) {
const newActiveNode = await this.getNodeFromPath(this.noteContext.notePath);
@@ -1047,7 +1084,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const noteIdsToReload = new Set();
for (const ecAttr of loadResults.getAttributes()) {
- if (ecAttr.type === 'label' && ['iconClass', 'cssClass', 'workspace', 'workspaceIconClass', 'color'].includes(ecAttr.name)) {
+ const dirtyingLabels = ['iconClass', 'cssClass', 'workspace', 'workspaceIconClass', 'color'];
+
+ if (ecAttr.type === 'label' && dirtyingLabels.includes(ecAttr.name)) {
if (ecAttr.isInheritable) {
noteIdsToReload.add(ecAttr.noteId);
}
@@ -1111,7 +1150,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
}
- if (!ecBranch.isDeleted && ecBranch.noteId !== 'hidden') {
+ if (!ecBranch.isDeleted) {
for (const parentNode of this.getNodesByNoteId(ecBranch.parentNoteId)) {
if (parentNode.isFolder() && !parentNode.isLoaded()) {
continue;
@@ -1169,7 +1208,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
let node = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== activeNoteId) {
- // if the active note has been moved elsewhere then it won't be found by the path
+ // if the active note has been moved elsewhere then it won't be found by the path,
// so we switch to the alternative of trying to find it by noteId
const notesById = this.getNodesByNoteId(activeNoteId);
@@ -1186,7 +1225,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
}
else {
- // this is used when original note has been deleted and we want to move the focus to the note above/below
+ // this is used when original note has been deleted, and we want to move the focus to the note above/below
node = await this.expandToNote(nextNotePath, false);
if (node) {
@@ -1283,14 +1322,22 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
if (this.noteContext.hoistedNoteId === 'root') {
this.tree.clearFilter();
+ this.toggleHiddenNode(false); // show everything but the hidden subtree
} else {
// hack when hoisted note is cloned then it could be filtered multiple times while we want only 1
this.tree.filterBranches(node =>
node.data.noteId === this.noteContext.hoistedNoteId // optimization to not having always resolve the node path
&& treeService.getNotePath(node) === hoistedNotePath);
+
+ this.toggleHiddenNode(true); // hoisting will handle hidden note visibility
}
}
+ toggleHiddenNode(show) {
+ const hiddenNode = this.getNodesByNoteId('hidden')[0];
+ $(hiddenNode.li).toggleClass("hidden-node-is-hidden", !show);
+ }
+
frocaReloadedEvent() {
this.reloadTreeFromCache();
}
@@ -1301,7 +1348,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts) {
- hotKeyMap[utils.normalizeShortcut(shortcut)] = node => {
+ hotKeyMap[shortcutService.normalizeShortcut(shortcut)] = node => {
const notePath = treeService.getNotePath(node);
this.triggerCommand(action.actionName, {node, notePath});
@@ -1518,4 +1565,40 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
noteCreateService.duplicateSubtree(nodeToDuplicate.data.noteId, branch.parentNoteId);
}
}
+
+ moveShortcutToVisibleCommand({node, selectedOrActiveBranchIds}) {
+ branchService.moveToParentNote(selectedOrActiveBranchIds, 'lbVisibleLaunchers');
+ }
+
+ moveShortcutToAvailableCommand({node, selectedOrActiveBranchIds}) {
+ branchService.moveToParentNote(selectedOrActiveBranchIds, 'lbAvailableLaunchers');
+ }
+
+ addNoteLauncherCommand({node}) {
+ this.createLauncherNote(node, 'note');
+ }
+
+ addScriptLauncherCommand({node}) {
+ this.createLauncherNote(node, 'script');
+ }
+
+ addWidgetLauncherCommand({node}) {
+ this.createLauncherNote(node, 'customWidget');
+ }
+
+ addSpacerLauncherCommand({node}) {
+ this.createLauncherNote(node, 'spacer');
+ }
+
+ async createLauncherNote(node, launcherType) {
+ const resp = await server.post(`special-notes/launchers/${node.data.noteId}/${launcherType}`);
+
+ if (!resp.success) {
+ alert(resp.message);
+ }
+
+ await ws.waitForMaxKnownEntityChangeId();
+
+ appContext.tabManager.getActiveContext().setNote(resp.note.noteId);
+ }
}
diff --git a/src/public/app/widgets/note_type.js b/src/public/app/widgets/note_type.js
index 76fc1be3b..779ada63b 100644
--- a/src/public/app/widgets/note_type.js
+++ b/src/public/app/widgets/note_type.js
@@ -1,21 +1,24 @@
import server from '../services/server.js';
import mimeTypesService from '../services/mime_types.js';
import NoteContextAwareWidget from "./note_context_aware_widget.js";
-import dialogService from "./dialog.js";
+import dialogService from "../services/dialog.js";
const NOTE_TYPES = [
{ type: "file", title: "File", selectable: false },
{ type: "image", title: "Image", selectable: false },
{ type: "search", title: "Saved Search", selectable: false },
- { type: "note-map", mime: '', title: "Note Map", selectable: false },
+ { type: "noteMap", mime: '', title: "Note Map", selectable: false },
+ { type: "launcher", mime: '', title: "Launcher", selectable: false },
+ { type: "doc", mime: '', title: "Doc", selectable: false },
+ { type: "contentWidget", mime: '', title: "Widget", selectable: false },
{ type: "text", mime: "text/html", title: "Text", selectable: true },
- { type: "relation-map", mime: "application/json", title: "Relation Map", selectable: true },
+ { type: "relationMap", mime: "application/json", title: "Relation Map", selectable: true },
{ type: "render", mime: '', title: "Render Note", selectable: true },
{ type: "canvas", mime: 'application/json', title: "Canvas", selectable: true },
{ type: "mermaid", mime: 'text/mermaid', title: "Mermaid Diagram", selectable: true },
{ type: "book", mime: '', title: "Book", selectable: true },
- { type: "web-view", mime: '', title: "Web View", selectable: true },
+ { type: "webView", mime: '', title: "Web View", selectable: true },
{ type: "code", mime: 'text/plain', title: "Code", selectable: true }
];
diff --git a/src/public/app/widgets/note_wrapper.js b/src/public/app/widgets/note_wrapper.js
index 43979ada4..c91e42e80 100644
--- a/src/public/app/widgets/note_wrapper.js
+++ b/src/public/app/widgets/note_wrapper.js
@@ -43,7 +43,7 @@ export default class NoteWrapperWidget extends FlexContainer {
}
this.$widget.toggleClass("full-content-width",
- ['image', 'mermaid', 'book', 'render', 'canvas', 'web-view'].includes(note.type)
+ ['image', 'mermaid', 'book', 'render', 'canvas', 'webView'].includes(note.type)
|| !!note?.hasLabel('fullContentWidth')
);
diff --git a/src/public/app/widgets/quick_search.js b/src/public/app/widgets/quick_search.js
index 6337f4dd0..e275756b1 100644
--- a/src/public/app/widgets/quick_search.js
+++ b/src/public/app/widgets/quick_search.js
@@ -1,10 +1,10 @@
import BasicWidget from "./basic_widget.js";
import server from "../services/server.js";
import linkService from "../services/link.js";
-import dateNotesService from "../services/date_notes.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
-import appContext from "../services/app_context.js";
+import appContext from "../components/app_context.js";
+import shortcutService from "../services/shortcuts.js";
const TPL = `
@@ -66,7 +66,7 @@ export default class QuickSearchWidget extends BasicWidget {
})
}
- utils.bindElShortcut(this.$searchString, 'return', () => {
+ shortcutService.bindElShortcut(this.$searchString, 'return', () => {
if (this.$dropdownMenu.is(":visible")) {
this.search(); // just update already visible dropdown
} else {
@@ -76,11 +76,11 @@ export default class QuickSearchWidget extends BasicWidget {
this.$searchString.focus();
});
- utils.bindElShortcut(this.$searchString, 'down', () => {
+ shortcutService.bindElShortcut(this.$searchString, 'down', () => {
this.$dropdownMenu.find('.dropdown-item:first').focus();
});
- utils.bindElShortcut(this.$searchString, 'esc', () => {
+ shortcutService.bindElShortcut(this.$searchString, 'esc', () => {
this.$dropdownToggle.dropdown('hide');
});
@@ -120,7 +120,7 @@ export default class QuickSearchWidget extends BasicWidget {
appContext.tabManager.getActiveContext().setNote(note.noteId);
}
});
- utils.bindElShortcut($link, 'return', () => {
+ shortcutService.bindElShortcut($link, 'return', () => {
this.$dropdownToggle.dropdown("hide");
appContext.tabManager.getActiveContext().setNote(note.noteId);
@@ -140,9 +140,9 @@ export default class QuickSearchWidget extends BasicWidget {
$showInFullButton.on('click', () => this.showInFullSearch());
- utils.bindElShortcut($showInFullButton, 'return', () => this.showInFullSearch());
+ shortcutService.bindElShortcut($showInFullButton, 'return', () => this.showInFullSearch());
- utils.bindElShortcut(this.$dropdownMenu.find('.dropdown-item:first'), 'up', () => this.$searchString.focus());
+ shortcutService.bindElShortcut(this.$dropdownMenu.find('.dropdown-item:first'), 'up', () => this.$searchString.focus());
this.$dropdownToggle.dropdown('update');
}
diff --git a/src/public/app/widgets/ribbon_widgets/basic_properties.js b/src/public/app/widgets/ribbon_widgets/basic_properties.js
index ef2625b7f..00dec1015 100644
--- a/src/public/app/widgets/ribbon_widgets/basic_properties.js
+++ b/src/public/app/widgets/ribbon_widgets/basic_properties.js
@@ -68,13 +68,9 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
return "toggleRibbonBasicProperties";
}
- isEnabled() {
- return this.note;
- }
-
getTitle() {
return {
- show: this.isEnabled(),
+ show: !this.note.isLaunchBarConfig(),
title: 'Basic Properties',
icon: 'bx bx-slider'
};
diff --git a/src/public/app/widgets/ribbon_widgets/inherited_attribute_list.js b/src/public/app/widgets/ribbon_widgets/inherited_attribute_list.js
index 7e367a473..19263fdf4 100644
--- a/src/public/app/widgets/ribbon_widgets/inherited_attribute_list.js
+++ b/src/public/app/widgets/ribbon_widgets/inherited_attribute_list.js
@@ -43,7 +43,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
getTitle() {
return {
- show: true,
+ show: !this.note.isLaunchBarConfig(),
title: "Inherited attributes",
icon: "bx bx-list-plus"
};
diff --git a/src/public/app/widgets/ribbon_widgets/note_map.js b/src/public/app/widgets/ribbon_widgets/note_map.js
index f88484291..26405b281 100644
--- a/src/public/app/widgets/ribbon_widgets/note_map.js
+++ b/src/public/app/widgets/ribbon_widgets/note_map.js
@@ -47,10 +47,6 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
return "toggleRibbonTabNoteMap";
}
- isEnabled() {
- return this.note;
- }
-
getTitle() {
return {
show: this.isEnabled(),
diff --git a/src/public/app/widgets/ribbon_widgets/note_paths.js b/src/public/app/widgets/ribbon_widgets/note_paths.js
index 225cbef69..3270e0eb4 100644
--- a/src/public/app/widgets/ribbon_widgets/note_paths.js
+++ b/src/public/app/widgets/ribbon_widgets/note_paths.js
@@ -44,10 +44,6 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
return "toggleRibbonTabNotePaths";
}
- isEnabled() {
- return this.note;
- }
-
getTitle() {
return {
show: true,
diff --git a/src/public/app/widgets/ribbon_widgets/owned_attribute_list.js b/src/public/app/widgets/ribbon_widgets/owned_attribute_list.js
index da8ed35ba..836d39a98 100644
--- a/src/public/app/widgets/ribbon_widgets/owned_attribute_list.js
+++ b/src/public/app/widgets/ribbon_widgets/owned_attribute_list.js
@@ -47,7 +47,7 @@ export default class OwnedAttributeListWidget extends NoteContextAwareWidget {
getTitle() {
return {
- show: true,
+ show: !this.note.isLaunchBarConfig(),
title: "Owned attributes",
icon: "bx bx-list-check"
};
diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.js b/src/public/app/widgets/ribbon_widgets/search_definition.js
index 3c1451313..4b6c1d785 100644
--- a/src/public/app/widgets/ribbon_widgets/search_definition.js
+++ b/src/public/app/widgets/ribbon_widgets/search_definition.js
@@ -13,7 +13,7 @@ import OrderBy from "../search_options/order_by.js";
import SearchScript from "../search_options/search_script.js";
import Limit from "../search_options/limit.js";
import Debug from "../search_options/debug.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
import bulkActionService from "../../services/bulk_action.js";
const TPL = `
diff --git a/src/public/app/widgets/search_options/abstract_search_option.js b/src/public/app/widgets/search_options/abstract_search_option.js
index 6103f9dde..daf76fb73 100644
--- a/src/public/app/widgets/search_options/abstract_search_option.js
+++ b/src/public/app/widgets/search_options/abstract_search_option.js
@@ -1,6 +1,6 @@
import server from "../../services/server.js";
import ws from "../../services/ws.js";
-import Component from "../component.js";
+import Component from "../../components/component.js";
import utils from "../../services/utils.js";
export default class AbstractSearchOption extends Component {
diff --git a/src/public/app/widgets/search_options/search_string.js b/src/public/app/widgets/search_options/search_string.js
index beb4deeed..ebfd03a95 100644
--- a/src/public/app/widgets/search_options/search_string.js
+++ b/src/public/app/widgets/search_options/search_string.js
@@ -1,7 +1,7 @@
import AbstractSearchOption from "./abstract_search_option.js";
-import utils from "../../services/utils.js";
import SpacedUpdate from "../../services/spaced_update.js";
import server from "../../services/server.js";
+import shortcutService from "../../services/shortcuts.js";
const TPL = `
@@ -45,7 +45,7 @@ export default class SearchString extends AbstractSearchOption {
this.$searchString = $option.find('.search-string');
this.$searchString.on('input', () => this.spacedUpdate.scheduleUpdate());
- utils.bindElShortcut(this.$searchString, 'return', async () => {
+ shortcutService.bindElShortcut(this.$searchString, 'return', async () => {
// this also in effect disallows new lines in query string.
// on one hand this makes sense since search string is a label
// on the other hand it could be nice for structuring long search string. It's probably a niche case though.
diff --git a/src/public/app/widgets/shared_switch.js b/src/public/app/widgets/shared_switch.js
index e7f10d590..f159d7023 100644
--- a/src/public/app/widgets/shared_switch.js
+++ b/src/public/app/widgets/shared_switch.js
@@ -3,11 +3,12 @@ import branchService from "../services/branches.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import syncService from "../services/sync.js";
-import dialogService from "./dialog.js";
+import dialogService from "../services/dialog.js";
export default class SharedSwitchWidget extends SwitchWidget {
isEnabled() {
- return super.isEnabled() && this.noteId !== 'root' && this.noteId !== 'share';
+ return super.isEnabled()
+ && !['root', 'share', 'hidden'].includes(this.noteId);
}
doRender() {
diff --git a/src/public/app/widgets/spacer.js b/src/public/app/widgets/spacer.js
index 3e1468d09..9281853bb 100644
--- a/src/public/app/widgets/spacer.js
+++ b/src/public/app/widgets/spacer.js
@@ -3,18 +3,17 @@ import BasicWidget from "./basic_widget.js";
const TPL = `
`;
export default class SpacerWidget extends BasicWidget {
- constructor(baseSize = 0, growIndex = 1000, shrinkIndex = 1000) {
+ constructor(baseSize = 0, growthFactor = 1000) {
super();
this.baseSize = baseSize;
- this.growIndex = growIndex;
- this.shrinkIndex = shrinkIndex;
+ this.growthFactor = growthFactor;
}
doRender() {
this.$widget = $(TPL);
this.$widget.css("flex-basis", this.baseSize);
- this.$widget.css("flex-grow", this.growIndex);
- this.$widget.css("flex-shrink", this.shrinkIndex);
+ this.$widget.css("flex-grow", this.growthFactor);
+ this.$widget.css("flex-shrink", 1000);
}
}
diff --git a/src/public/app/widgets/switch.js b/src/public/app/widgets/switch.js
index 94402749f..3fab865bc 100644
--- a/src/public/app/widgets/switch.js
+++ b/src/public/app/widgets/switch.js
@@ -101,18 +101,26 @@ export default class SwitchWidget extends NoteContextAwareWidget {
this.$switchOnName = this.$widget.find(".switch-on-name");
this.$switchOnButton = this.$widget.find(".switch-on-button");
- this.$switchOnButton.on('click', () => this.switchOn());
+ this.$switchOnButton.on('click', () => this.toggle(true));
this.$switchOff = this.$widget.find(".switch-off");
this.$switchOffName = this.$widget.find(".switch-off-name");
this.$switchOffButton = this.$widget.find(".switch-off-button");
- this.$switchOffButton.on('click', () => this.switchOff());
+ this.$switchOffButton.on('click', () => this.toggle(false));
this.$helpButton = this.$widget.find(".switch-help-button");
}
+ toggle(state) {
+ if (state) {
+ this.switchOn();
+ } else {
+ this.switchOff();
+ }
+ }
+
switchOff() {}
switchOn() {}
}
diff --git a/src/public/app/widgets/tab_row.js b/src/public/app/widgets/tab_row.js
index 44eeb7c19..17fdb7305 100644
--- a/src/public/app/widgets/tab_row.js
+++ b/src/public/app/widgets/tab_row.js
@@ -1,8 +1,8 @@
import BasicWidget from "./basic_widget.js";
-import contextMenu from "../services/context_menu.js";
+import contextMenu from "../menus/context_menu.js";
import utils from "../services/utils.js";
import keyboardActionService from "../services/keyboard_actions.js";
-import appContext from "../services/app_context.js";
+import appContext from "../components/app_context.js";
import froca from "../services/froca.js";
import attributeService from "../services/attributes.js";
diff --git a/src/public/app/widgets/type_widgets/abstract_text_type_widget.js b/src/public/app/widgets/type_widgets/abstract_text_type_widget.js
index e742ec2ef..3477e3790 100644
--- a/src/public/app/widgets/type_widgets/abstract_text_type_widget.js
+++ b/src/public/app/widgets/type_widgets/abstract_text_type_widget.js
@@ -1,5 +1,5 @@
import TypeWidget from "./type_widget.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
import froca from "../../services/froca.js";
import linkService from "../../services/link.js";
import noteContentRenderer from "../../services/note_content_renderer.js";
@@ -41,7 +41,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
}
getNoteIdFromImage(imgSrc) {
- const match = imgSrc.match(/\/api\/images\/([A-Za-z0-9]+)\//);
+ const match = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);
return match ? match[1] : null;
}
diff --git a/src/public/app/widgets/type_widgets/content_widget.js b/src/public/app/widgets/type_widgets/content_widget.js
new file mode 100644
index 000000000..5055ca910
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/content_widget.js
@@ -0,0 +1,121 @@
+import TypeWidget from "./type_widget.js";
+import ZoomFactorOptions from "./options/appearance/zoom_factor.js";
+import NativeTitleBarOptions from "./options/appearance/native_title_bar.js";
+import ThemeOptions from "./options/appearance/theme.js";
+import FontsOptions from "./options/appearance/fonts.js";
+import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
+import KeyboardShortcutsOptions from "./options/shortcuts.js";
+import HeadingStyleOptions from "./options/text_notes/heading_style.js";
+import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
+import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
+import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js";
+import WrapLinesOptions from "./options/code_notes/wrap_lines.js";
+import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
+import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
+import ImageOptions from "./options/images.js";
+import SpellcheckOptions from "./options/spellcheck.js";
+import PasswordOptions from "./options/password.js";
+import EtapiOptions from "./options/etapi.js";
+import BackupOptions from "./options/backup.js";
+import SyncOptions from "./options/sync.js";
+import TrayOptions from "./options/other/tray.js";
+import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js";
+import NoteRevisionsSnapshotIntervalOptions from "./options/other/note_revisions_snapshot_interval.js";
+import NetworkConnectionsOptions from "./options/other/network_connections.js";
+import AdvancedSyncOptions from "./options/advanced/sync.js";
+import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js";
+import ConsistencyChecksOptions from "./options/advanced/consistency_checks.js";
+import VacuumDatabaseOptions from "./options/advanced/vacuum_database.js";
+import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js";
+
+const TPL = ``;
+
+const CONTENT_WIDGETS = {
+ optionsAppearance: [
+ ZoomFactorOptions,
+ NativeTitleBarOptions,
+ ThemeOptions,
+ FontsOptions,
+ MaxContentWidthOptions
+ ],
+ optionsShortcuts: [ KeyboardShortcutsOptions ],
+ optionsTextNotes: [
+ HeadingStyleOptions,
+ TableOfContentsOptions,
+ TextAutoReadOnlySizeOptions
+ ],
+ optionsCodeNotes: [
+ VimKeyBindingsOptions,
+ WrapLinesOptions,
+ CodeAutoReadOnlySizeOptions,
+ CodeMimeTypesOptions
+ ],
+ optionsImages: [ ImageOptions ],
+ optionsSpellcheck: [ SpellcheckOptions ],
+ optionsPassword: [ PasswordOptions ],
+ optionsEtapi: [ EtapiOptions ],
+ optionsBackup: [ BackupOptions ],
+ optionsSync: [ SyncOptions ],
+ optionsOther: [
+ TrayOptions,
+ NoteErasureTimeoutOptions,
+ NoteRevisionsSnapshotIntervalOptions,
+ NetworkConnectionsOptions
+ ],
+ optionsAdvanced: [
+ DatabaseIntegrityCheckOptions,
+ ConsistencyChecksOptions,
+ DatabaseAnonymizationOptions,
+ AdvancedSyncOptions,
+ VacuumDatabaseOptions
+ ]
+};
+
+export default class ContentWidgetTypeWidget extends TypeWidget {
+ static getType() { return "contentWidget"; }
+
+ doRender() {
+ this.$widget = $(TPL);
+ this.$content = this.$widget.find(".note-detail-content-widget-content");
+
+ super.doRender();
+ }
+
+ async doRefresh(note) {
+ this.$content.empty();
+ this.children = [];
+
+ const contentWidgets = CONTENT_WIDGETS[note.noteId];
+
+ if (contentWidgets) {
+ for (const clazz of contentWidgets) {
+ const widget = new clazz();
+
+ await widget.handleEvent('setNoteContext', { noteContext: this.noteContext });
+ this.child(widget);
+
+ this.$content.append(widget.render());
+ await widget.refresh();
+ }
+ } else {
+ this.$content.append(`Unknown widget for "${note.noteId}"`);
+ }
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/doc.js b/src/public/app/widgets/type_widgets/doc.js
new file mode 100644
index 000000000..fc775a62f
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/doc.js
@@ -0,0 +1,39 @@
+import TypeWidget from "./type_widget.js";
+
+const TPL = ``;
+
+export default class DocTypeWidget extends TypeWidget {
+ static getType() { return "doc"; }
+
+ doRender() {
+ this.$widget = $(TPL);
+ this.$content = this.$widget.find('.note-detail-doc-content');
+
+ super.doRender();
+ }
+
+ async doRefresh(note) {
+ const docName = note.getLabelValue('docName');
+
+ if (docName) {
+ this.$content.load(`${window.glob.assetPath}/app/doc_notes/${docName}.html`);
+ } else {
+ this.$content.empty();
+ }
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/editable_code.js b/src/public/app/widgets/type_widgets/editable_code.js
index 143c65458..6abca184e 100644
--- a/src/public/app/widgets/type_widgets/editable_code.js
+++ b/src/public/app/widgets/type_widgets/editable_code.js
@@ -21,7 +21,7 @@ const TPL = `
`;
export default class EditableCodeTypeWidget extends TypeWidget {
- static getType() { return "editable-code"; }
+ static getType() { return "editableCode"; }
doRender() {
this.$widget = $(TPL);
diff --git a/src/public/app/widgets/type_widgets/editable_code_buttons.js b/src/public/app/widgets/type_widgets/editable_code_buttons.js
index 006ace51f..0c8fafd43 100644
--- a/src/public/app/widgets/type_widgets/editable_code_buttons.js
+++ b/src/public/app/widgets/type_widgets/editable_code_buttons.js
@@ -1,6 +1,6 @@
import server from "../../services/server.js";
import ws from "../../services/ws.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
import toastService from "../../services/toast.js";
import treeService from "../../services/tree.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js
index 181311e3b..261719859 100644
--- a/src/public/app/widgets/type_widgets/editable_text.js
+++ b/src/public/app/widgets/type_widgets/editable_text.js
@@ -8,7 +8,7 @@ import treeService from "../../services/tree.js";
import noteCreateService from "../../services/note_create.js";
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import link from "../../services/link.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
const ENABLE_INSPECTOR = false;
@@ -39,6 +39,10 @@ const TPL = `
height: 100%;
}
+ body.mobile .note-detail-editable-text {
+ padding-left: 4px;
+ }
+
.note-detail-editable-text a:hover {
cursor: pointer;
}
@@ -83,7 +87,7 @@ const TPL = `
`;
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
- static getType() { return "editable-text"; }
+ static getType() { return "editableText"; }
doRender() {
this.$widget = $(TPL);
@@ -132,7 +136,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.textEditor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate());
if (glob.isDev && ENABLE_INSPECTOR) {
- await import(/* webpackIgnore: true */'../../../libraries/ckeditor/inspector.js');
+ await import(/* webpackIgnore: true */'../../../libraries/ckeditor/inspector');
CKEditorInspector.attach(this.textEditor);
}
}
diff --git a/src/public/app/widgets/type_widgets/empty.js b/src/public/app/widgets/type_widgets/empty.js
index 7df7a9d08..0eed0373f 100644
--- a/src/public/app/widgets/type_widgets/empty.js
+++ b/src/public/app/widgets/type_widgets/empty.js
@@ -1,6 +1,6 @@
import noteAutocompleteService from '../../services/note_autocomplete.js';
import TypeWidget from "./type_widget.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
import searchService from "../../services/search.js";
const TPL = `
diff --git a/src/public/app/widgets/type_widgets/image.js b/src/public/app/widgets/type_widgets/image.js
index db78f8ef1..4ef6628d8 100644
--- a/src/public/app/widgets/type_widgets/image.js
+++ b/src/public/app/widgets/type_widgets/image.js
@@ -2,7 +2,7 @@ import utils from "../../services/utils.js";
import toastService from "../../services/toast.js";
import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js";
-import contextMenu from "../../services/context_menu.js";
+import contextMenu from "../../menus/context_menu.js";
const TPL = `
diff --git a/src/public/app/widgets/type_widgets/note_map.js b/src/public/app/widgets/type_widgets/note_map.js
index 5e0680b9c..895d2ea7b 100644
--- a/src/public/app/widgets/type_widgets/note_map.js
+++ b/src/public/app/widgets/type_widgets/note_map.js
@@ -4,7 +4,7 @@ import NoteMapWidget from "../note_map.js";
const TPL = `
`;
export default class NoteMapTypeWidget extends TypeWidget {
- static getType() { return "note-map"; }
+ static getType() { return "noteMap"; }
constructor() {
super();
diff --git a/src/public/app/widgets/type_widgets/options/advanced/consistency_checks.js b/src/public/app/widgets/type_widgets/options/advanced/consistency_checks.js
new file mode 100644
index 000000000..e48746ab1
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/advanced/consistency_checks.js
@@ -0,0 +1,24 @@
+import OptionsWidget from "../options_widget.js";
+import toastService from "../../../../services/toast.js";
+import server from "../../../../services/server.js";
+
+const TPL = `
+
+
Consistency checks
+
+ Find and fix consistency issues
+`;
+
+export default class ConsistencyChecksOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$findAndFixConsistencyIssuesButton = this.$widget.find(".find-and-fix-consistency-issues-button");
+ this.$findAndFixConsistencyIssuesButton.on('click', async () => {
+ toastService.showMessage("Finding and fixing consistency issues...");
+
+ await server.post('database/find-and-fix-consistency-issues');
+
+ toastService.showMessage("Consistency issues should be fixed.");
+ });
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/advanced/database_anonymization.js b/src/public/app/widgets/type_widgets/options/advanced/database_anonymization.js
new file mode 100644
index 000000000..3a0c534e2
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/advanced/database_anonymization.js
@@ -0,0 +1,56 @@
+import OptionsWidget from "../options_widget.js";
+import toastService from "../../../../services/toast.js";
+import server from "../../../../services/server.js";
+
+const TPL = `
+
+
Database anonymization
+
+
Full anonymization
+
+
This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
+ for sharing online for debugging purposes without fear of leaking your personal data.
+
+
Save fully anonymized database
+
+
Light anonymization
+
+
This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.
+
+
You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.
+
+
Save lightly anonymized database
+
`;
+
+export default class DatabaseAnonymizationOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$anonymizeFullButton = this.$widget.find(".anonymize-full-button");
+ this.$anonymizeLightButton = this.$widget.find(".anonymize-light-button");
+ this.$anonymizeFullButton.on('click', async () => {
+ toastService.showMessage("Creating fully anonymized database...");
+
+ const resp = await server.post('database/anonymize/full');
+
+ if (!resp.success) {
+ toastService.showError("Could not create anonymized database, check backend logs for details");
+ }
+ else {
+ toastService.showMessage(`Created fully anonymized database in ${resp.anonymizedFilePath}`, 10000);
+ }
+ });
+
+ this.$anonymizeLightButton.on('click', async () => {
+ toastService.showMessage("Creating lightly anonymized database...");
+
+ const resp = await server.post('database/anonymize/light');
+
+ if (!resp.success) {
+ toastService.showError("Could not create anonymized database, check backend logs for details");
+ }
+ else {
+ toastService.showMessage(`Created lightly anonymized database in ${resp.anonymizedFilePath}`, 10000);
+ }
+ });
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/advanced/database_integrity_check.js b/src/public/app/widgets/type_widgets/options/advanced/database_integrity_check.js
new file mode 100644
index 000000000..5fc1264b9
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/advanced/database_integrity_check.js
@@ -0,0 +1,31 @@
+import OptionsWidget from "../options_widget.js";
+import toastService from "../../../../services/toast.js";
+import server from "../../../../services/server.js";
+
+const TPL = `
+
+
Database integrity check
+
+
This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.
+
+
Check database integrity
+
`;
+
+export default class DatabaseIntegrityCheckOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$checkIntegrityButton = this.$widget.find(".check-integrity-button");
+ this.$checkIntegrityButton.on('click', async () => {
+ toastService.showMessage("Checking database integrity...");
+
+ const {results} = await server.get('database/check-integrity');
+
+ if (results.length === 1 && results[0].integrity_check === "ok") {
+ toastService.showMessage("Integrity check succeeded - no problems found.");
+ }
+ else {
+ toastService.showMessage("Integrity check failed: " + JSON.stringify(results, null, 2), 15000);
+ }
+ });
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/advanced/sync.js b/src/public/app/widgets/type_widgets/options/advanced/sync.js
new file mode 100644
index 000000000..457d7e34e
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/advanced/sync.js
@@ -0,0 +1,36 @@
+import OptionsWidget from "../options_widget.js";
+import server from "../../../../services/server.js";
+import toastService from "../../../../services/toast.js";
+
+const TPL = `
+
+
Sync
+ Force full sync
+
+ Fill entity changes records
+`;
+
+export default class AdvancedSyncOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$forceFullSyncButton = this.$widget.find(".force-full-sync-button");
+ this.$fillEntityChangesButton = this.$widget.find(".fill-entity-changes-button");
+ this.$forceFullSyncButton.on('click', async () => {
+ await server.post('sync/force-full-sync');
+
+ toastService.showMessage("Full sync triggered");
+ });
+
+ this.$fillEntityChangesButton.on('click', async () => {
+ toastService.showMessage("Filling entity changes rows...");
+
+ await server.post('sync/fill-entity-changes');
+
+ toastService.showMessage("Sync rows filled successfully");
+ });
+ }
+
+ async optionsLoaded(options) {
+
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/advanced/vacuum_database.js b/src/public/app/widgets/type_widgets/options/advanced/vacuum_database.js
new file mode 100644
index 000000000..4ac6adda0
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/advanced/vacuum_database.js
@@ -0,0 +1,26 @@
+import OptionsWidget from "../options_widget.js";
+import toastService from "../../../../services/toast.js";
+import server from "../../../../services/server.js";
+
+const TPL = `
+
+
Vacuum database
+
+
This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.
+
+
Vacuum database
+
`;
+
+export default class VacuumDatabaseOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$vacuumDatabaseButton = this.$widget.find(".vacuum-database-button");
+ this.$vacuumDatabaseButton.on('click', async () => {
+ toastService.showMessage("Vacuuming database...");
+
+ await server.post('database/vacuum-database');
+
+ toastService.showMessage("Database has been vacuumed");
+ });
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/appearance/fonts.js b/src/public/app/widgets/type_widgets/options/appearance/fonts.js
new file mode 100644
index 000000000..964ff09fb
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/appearance/fonts.js
@@ -0,0 +1,185 @@
+import OptionsWidget from "../options_widget.js";
+import utils from "../../../../services/utils.js";
+
+const FONT_FAMILIES = [
+ { value: "theme", label: "Theme defined" },
+ { value: "serif", label: "Serif" },
+ { value: "sans-serif", label: "Sans Serif" },
+ { value: "monospace", label: "Monospace" },
+ { value: "Arial", label: "Arial" },
+ { value: "Verdana", label: "Verdana" },
+ { value: "Helvetica", label: "Helvetica" },
+ { value: "Tahoma", label: "Tahoma" },
+ { value: "Trebuchet MS", label: "Trebuchet MS" },
+ { value: "Times New Roman", label: "Times New Roman" },
+ { value: "Georgia", label: "Georgia" },
+ { value: "Garamond", label: "Garamond" },
+ { value: "Courier New", label: "Courier New" },
+ { value: "Brush Script MT", label: "Brush Script MT" },
+ { value: "Impact", label: "Impact" },
+ { value: "American Typewriter", label: "American Typewriter" },
+ { value: "Andalé Mono", label: "Andalé Mono" },
+ { value: "Lucida Console", label: "Lucida Console" },
+ { value: "Monaco", label: "Monaco" },
+ { value: "Bradley Hand", label: "Bradley Hand" },
+ { value: "Luminari", label: "Luminari" },
+ { value: "Comic Sans MS", label: "Comic Sans MS" },
+];
+
+const TPL = `
+
+
Fonts
+
+
Main font
+
+
+
+
Note tree font
+
+
+
+
Note detail font
+
+
+
+
Monospace (code) font
+
+
+
+
Note that tree and detail font sizing is relative to the main font size setting.
+
+
Not all listed fonts may be available on your system.
+
+
+ To apply font changes, click on
+ reload frontend
+
+
`;
+
+export default class FontsOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+
+ this.$mainFontSize = this.$widget.find(".main-font-size");
+ this.$mainFontFamily = this.$widget.find(".main-font-family");
+
+ this.$treeFontSize = this.$widget.find(".tree-font-size");
+ this.$treeFontFamily = this.$widget.find(".tree-font-family");
+
+ this.$detailFontSize = this.$widget.find(".detail-font-size");
+ this.$detailFontFamily = this.$widget.find(".detail-font-family");
+
+ this.$monospaceFontSize = this.$widget.find(".monospace-font-size");
+ this.$monospaceFontFamily = this.$widget.find(".monospace-font-family");
+
+ this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
+ }
+
+ async optionsLoaded(options) {
+ if (options.overrideThemeFonts !== 'true') {
+ this.toggleInt(false);
+ return;
+ }
+
+ this.toggleInt(true);
+
+ this.$mainFontSize.val(options.mainFontSize);
+ this.fillFontFamilyOptions(this.$mainFontFamily, options.mainFontFamily);
+
+ this.$treeFontSize.val(options.treeFontSize);
+ this.fillFontFamilyOptions(this.$treeFontFamily, options.treeFontFamily);
+
+ this.$detailFontSize.val(options.detailFontSize);
+ this.fillFontFamilyOptions(this.$detailFontFamily, options.detailFontFamily);
+
+ this.$monospaceFontSize.val(options.monospaceFontSize);
+ this.fillFontFamilyOptions(this.$monospaceFontFamily, options.monospaceFontFamily);
+
+ const optionsToSave = [
+ 'mainFontFamily', 'mainFontSize',
+ 'treeFontFamily', 'treeFontSize',
+ 'detailFontFamily', 'detailFontSize',
+ 'monospaceFontFamily', 'monospaceFontSize'
+ ];
+
+ for (const optionName of optionsToSave) {
+ this['$' + optionName].on('change', () =>
+ this.updateOption(optionName, this['$' + optionName].val()));
+ }
+ }
+
+ fillFontFamilyOptions($select, currentValue) {
+ $select.empty();
+
+ for (const {value, label} of FONT_FAMILIES) {
+ $select.append($("
")
+ .attr("value", value)
+ .prop("selected", value === currentValue)
+ .text(label));
+ }
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/appearance/max_content_width.js b/src/public/app/widgets/type_widgets/options/appearance/max_content_width.js
new file mode 100644
index 000000000..6b27b50d6
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/appearance/max_content_width.js
@@ -0,0 +1,38 @@
+import OptionsWidget from "../options_widget.js";
+import utils from "../../../../services/utils.js";
+
+const TPL = `
+
+
Content width
+
+
Trilium by default limits max content width to improve readability for maximized screens on wide screens.
+
+
+
+
+ To apply content width changes, click on
+ reload frontend
+
+
`;
+
+export default class MaxContentWidthOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+
+ this.$maxContentWidth = this.$widget.find(".max-content-width");
+
+ this.$maxContentWidth.on('change', async () =>
+ this.updateOption('maxContentWidth', this.$maxContentWidth.val()))
+
+ this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
+ }
+
+ async optionsLoaded(options) {
+ this.$maxContentWidth.val(options.maxContentWidth);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/appearance/native_title_bar.js b/src/public/app/widgets/type_widgets/options/appearance/native_title_bar.js
new file mode 100644
index 000000000..b91e6ea41
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/appearance/native_title_bar.js
@@ -0,0 +1,27 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Native title bar (requires app restart)
+
+
+ enabled
+ disabled
+
+`;
+
+export default class NativeTitleBarOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$nativeTitleBarSelect = this.$widget.find(".native-title-bar-select");
+ this.$nativeTitleBarSelect.on('change', () => {
+ const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false';
+
+ this.updateOption('nativeTitleBarVisible', nativeTitleBarVisible);
+ });
+ }
+
+ async optionsLoaded(options) {
+ this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/appearance/theme.js b/src/public/app/widgets/type_widgets/options/appearance/theme.js
new file mode 100644
index 000000000..15c850858
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/appearance/theme.js
@@ -0,0 +1,58 @@
+import OptionsWidget from "../options_widget.js";
+import server from "../../../../services/server.js";
+import utils from "../../../../services/utils.js";
+
+const TPL = `
+`;
+
+export default class ThemeOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$themeSelect = this.$widget.find(".theme-select");
+ this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
+
+ this.$themeSelect.on('change', async () => {
+ const newTheme = this.$themeSelect.val();
+
+ await server.put('options/theme/' + newTheme);
+
+ utils.reloadFrontendApp("theme change");
+ });
+
+ this.$overrideThemeFonts.on('change', () => this.updateCheckboxOption('overrideThemeFonts', this.$overrideThemeFonts));
+ }
+
+ async optionsLoaded(options) {
+ const themes = [
+ { val: 'light', title: 'Light' },
+ { val: 'dark', title: 'Dark' }
+ ].concat(await server.get('options/user-themes'));
+
+ this.$themeSelect.empty();
+
+ for (const theme of themes) {
+ this.$themeSelect.append($(" ")
+ .attr("value", theme.val)
+ .attr("data-note-id", theme.noteId)
+ .text(theme.title));
+ }
+
+ this.$themeSelect.val(options.theme);
+
+ this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/appearance/zoom_factor.js b/src/public/app/widgets/type_widgets/options/appearance/zoom_factor.js
new file mode 100644
index 000000000..4546919d3
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/appearance/zoom_factor.js
@@ -0,0 +1,27 @@
+import appContext from "../../../../components/app_context.js";
+import OptionsWidget from "../options_widget.js";
+import utils from "../../../../services/utils.js";
+
+const TPL = `
+
+
Zoom factor (desktop build only)
+
+
+
Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.
+
`;
+
+export default class ZoomFactorOptions extends OptionsWidget {
+ isEnabled() {
+ return super.isEnabled() && utils.isElectron();
+ }
+
+ doRender() {
+ this.$widget = $(TPL);
+ this.$zoomFactorSelect = this.$widget.find(".zoom-factor-select");
+ this.$zoomFactorSelect.on('change', () => { appContext.triggerCommand('setZoomFactorAndSave', {zoomFactor: this.$zoomFactorSelect.val()}); });
+ }
+
+ async optionsLoaded(options) {
+ this.$zoomFactorSelect.val(options.zoomFactor);
+ }
+}
diff --git a/src/public/app/widgets/dialogs/options/backup.js b/src/public/app/widgets/type_widgets/options/backup.js
similarity index 55%
rename from src/public/app/widgets/dialogs/options/backup.js
rename to src/public/app/widgets/type_widgets/options/backup.js
index d8c955ea6..674801055 100644
--- a/src/public/app/widgets/dialogs/options/backup.js
+++ b/src/public/app/widgets/type_widgets/options/backup.js
@@ -1,6 +1,6 @@
import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
-import OptionsTab from "./options_tab.js";
+import OptionsWidget from "./options_widget.js";
const TPL = `
@@ -8,22 +8,26 @@ const TPL = `
Trilium can back up the database automatically:
-
-
- Enable daily backup
-
-
-
-
- Enable weekly backup
-
-
-
-
- Enable monthly backup
-
-
-
+
It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.
@@ -31,17 +35,15 @@ const TPL = `
Backup now
- Backup database now
+ Backup database now
`;
-export default class BackupOptions extends OptionsTab {
- get tabTitle() { return "Backup" }
-
- lazyRender() {
+export default class BackupOptions extends OptionsWidget {
+ doRender() {
this.$widget = $(TPL);
- this.$backupDatabaseButton = this.$widget.find("#backup-database-button");
+ this.$backupDatabaseButton = this.$widget.find(".backup-database-button");
this.$backupDatabaseButton.on('click', async () => {
const {backupFile} = await server.post('database/backup-database');
@@ -49,9 +51,9 @@ export default class BackupOptions extends OptionsTab {
toastService.showMessage("Database has been backed up to " + backupFile, 10000);
});
- this.$dailyBackupEnabled = this.$widget.find("#daily-backup-enabled");
- this.$weeklyBackupEnabled = this.$widget.find("#weekly-backup-enabled");
- this.$monthlyBackupEnabled = this.$widget.find("#monthly-backup-enabled");
+ this.$dailyBackupEnabled = this.$widget.find(".daily-backup-enabled");
+ this.$weeklyBackupEnabled = this.$widget.find(".weekly-backup-enabled");
+ this.$monthlyBackupEnabled = this.$widget.find(".monthly-backup-enabled");
this.$dailyBackupEnabled.on('change', () =>
this.updateCheckboxOption('dailyBackupEnabled', this.$dailyBackupEnabled));
diff --git a/src/public/app/widgets/type_widgets/options/code_notes/code_auto_read_only_size.js b/src/public/app/widgets/type_widgets/options/code_notes/code_auto_read_only_size.js
new file mode 100644
index 000000000..5722d4c20
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/code_notes/code_auto_read_only_size.js
@@ -0,0 +1,26 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Automatic read-only size
+
+
Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).
+
+
+ Automatic read-only size (code notes)
+
+
+
`;
+
+export default class CodeAutoReadOnlySizeOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$autoReadonlySizeCode = this.$widget.find(".auto-readonly-size-code");
+ this.$autoReadonlySizeCode.on('change', () =>
+ this.updateOption('autoReadonlySizeCode', this.$autoReadonlySizeCode.val()));
+ }
+
+ async optionsLoaded(options) {
+ this.$autoReadonlySizeCode.val(options.autoReadonlySizeCode);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/code_notes/code_mime_types.js b/src/public/app/widgets/type_widgets/options/code_notes/code_mime_types.js
new file mode 100644
index 000000000..1034737f0
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/code_notes/code_mime_types.js
@@ -0,0 +1,49 @@
+import OptionsWidget from "../options_widget.js";
+import mimeTypesService from "../../../../services/mime_types.js";
+
+const TPL = `
+
+
Available MIME types in the dropdown
+
+
+
`;
+
+let idCtr = 1; // global, since this can be shown in multiple dialogs
+
+export default class CodeMimeTypesOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$mimeTypes = this.$widget.find(".options-mime-types");
+ }
+
+ async optionsLoaded(options) {
+ this.$mimeTypes.empty();
+
+ for (const mimeType of await mimeTypesService.getMimeTypes()) {
+ const id = "code-mime-type-" + (idCtr++);
+
+ this.$mimeTypes.append($("")
+ .append($(' ')
+ .attr("id", id)
+ .attr("data-mime-type", mimeType.mime)
+ .prop("checked", mimeType.enabled))
+ .on('change', () => this.save())
+ .append(" ")
+ .append($('')
+ .attr("for", id)
+ .text(mimeType.title))
+ );
+ }
+ }
+
+ async save() {
+ const enabledMimeTypes = [];
+
+ this.$mimeTypes.find("input:checked").each(
+ (i, el) => enabledMimeTypes.push(this.$widget.find(el).attr("data-mime-type")));
+
+ await this.updateOption('codeNotesMimeTypes', JSON.stringify(enabledMimeTypes));
+
+ mimeTypesService.loadMimeTypes();
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/code_notes/vim_key_bindings.js b/src/public/app/widgets/type_widgets/options/code_notes/vim_key_bindings.js
new file mode 100644
index 000000000..f772f1cba
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/code_notes/vim_key_bindings.js
@@ -0,0 +1,23 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Use vim keybindings in code notes (no ex mode)
+
+
+ Enable Vim Keybindings
+
+`;
+
+export default class VimKeyBindingsOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$vimKeymapEnabled = this.$widget.find(".vim-keymap-enabled");
+ this.$vimKeymapEnabled.on('change', () =>
+ this.updateCheckboxOption('vimKeymapEnabled', this.$vimKeymapEnabled));
+ }
+
+ async optionsLoaded(options) {
+ this.setCheckboxState(this.$vimKeymapEnabled, options.vimKeymapEnabled);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/code_notes/wrap_lines.js b/src/public/app/widgets/type_widgets/options/code_notes/wrap_lines.js
new file mode 100644
index 000000000..9449d28b1
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/code_notes/wrap_lines.js
@@ -0,0 +1,23 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Wrap lines in code notes
+
+
+ Enable Line Wrap (change might need a frontend reload to take effect)
+
+`;
+
+export default class WrapLinesOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$codeLineWrapEnabled = this.$widget.find(".line-wrap-enabled");
+ this.$codeLineWrapEnabled.on('change', () =>
+ this.updateCheckboxOption('codeLineWrapEnabled', this.$codeLineWrapEnabled));
+ }
+
+ async optionsLoaded(options) {
+ this.setCheckboxState(this.$codeLineWrapEnabled, options.codeLineWrapEnabled);
+ }
+}
diff --git a/src/public/app/widgets/dialogs/options/etapi.js b/src/public/app/widgets/type_widgets/options/etapi.js
similarity index 82%
rename from src/public/app/widgets/dialogs/options/etapi.js
rename to src/public/app/widgets/type_widgets/options/etapi.js
index 7bbaf4dc4..d6356dff5 100644
--- a/src/public/app/widgets/dialogs/options/etapi.js
+++ b/src/public/app/widgets/type_widgets/options/etapi.js
@@ -1,7 +1,7 @@
import server from "../../../services/server.js";
-import dialogService from "../../dialog.js";
+import dialogService from "../../../services/dialog.js";
import toastService from "../../../services/toast.js";
-import OptionsTab from "./options_tab.js";
+import OptionsWidget from "./options_widget.js";
const TPL = `
@@ -10,14 +10,14 @@ const TPL = `
ETAPI is a REST API used to access Trilium instance programmatically, without UI.
See more details on wiki and ETAPI OpenAPI spec .
-
Create new ETAPI token
+
Create new ETAPI token
Existing tokens
-
There are no tokens yet. Click on the button above to create one.
+
There are no tokens yet. Click on the button above to create one.
-
+
Token name
@@ -46,13 +46,11 @@ const TPL = `
}
`;
-export default class EtapiOptions extends OptionsTab {
- get tabTitle() { return "ETAPI" }
-
- lazyRender() {
+export default class EtapiOptions extends OptionsWidget {
+ doRender() {
this.$widget = $(TPL);
- this.$widget.find("#create-etapi-token").on("click", async () => {
+ this.$widget.find(".create-etapi-token").on("click", async () => {
const tokenName = await dialogService.prompt({
title: "New ETAPI token",
message: "Please enter new token's name",
@@ -79,8 +77,8 @@ export default class EtapiOptions extends OptionsTab {
}
async refreshTokens() {
- const $noTokensYet = this.$widget.find("#no-tokens-yet");
- const $tokensTable = this.$widget.find("#tokens-table");
+ const $noTokensYet = this.$widget.find(".no-tokens-yet");
+ const $tokensTable = this.$widget.find(".tokens-table");
const tokens = await server.get('etapi-tokens');
@@ -118,7 +116,7 @@ export default class EtapiOptions extends OptionsTab {
}
async deleteToken(etapiTokenId, name) {
- if (!confirm(`Are you sure you want to delete ETAPI token "${name}"?`)) {
+ if (!await dialogService.confirm(`Are you sure you want to delete ETAPI token "${name}"?`)) {
return;
}
diff --git a/src/public/app/widgets/type_widgets/options/images.js b/src/public/app/widgets/type_widgets/options/images.js
new file mode 100644
index 000000000..fdc69df1f
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/images.js
@@ -0,0 +1,84 @@
+import OptionsWidget from "./options_widget.js";
+
+const TPL = `
+
+
+
+
Images
+
+
+
+ Download images automatically for offline use.
+
+
+
(pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline)
+
+
+
+ Enable image compression
+
+
+
+
+`;
+
+export default class ImageOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+
+ this.$imageMaxWidthHeight = this.$widget.find(".image-max-width-height");
+ this.$imageJpegQuality = this.$widget.find(".image-jpeg-quality");
+
+ this.$imageMaxWidthHeight.on('change', () =>
+ this.updateOption('imageMaxWidthHeight', this.$imageMaxWidthHeight.val()));
+
+ this.$imageJpegQuality.on('change', () =>
+ this.updateOption('imageJpegQuality', this.$imageJpegQuality.val()));
+
+ this.$downloadImagesAutomatically = this.$widget.find(".download-images-automatically");
+
+ this.$downloadImagesAutomatically.on("change", () =>
+ this.updateCheckboxOption('downloadImagesAutomatically', this.$downloadImagesAutomatically));
+
+ this.$enableImageCompression = this.$widget.find(".image-compresion-enabled");
+ this.$imageCompressionWrapper = this.$widget.find(".image-compression-enabled-wraper");
+
+ this.$enableImageCompression.on("change", () => {
+ this.updateCheckboxOption('compressImages', this.$enableImageCompression);
+ this.setImageCompression();
+ });
+ }
+
+ optionsLoaded(options) {
+ this.$imageMaxWidthHeight.val(options.imageMaxWidthHeight);
+ this.$imageJpegQuality.val(options.imageJpegQuality);
+
+ this.setCheckboxState(this.$downloadImagesAutomatically, options.downloadImagesAutomatically);
+ this.setCheckboxState(this.$enableImageCompression, options.compressImages);
+
+ this.setImageCompression();
+ }
+
+ setImageCompression() {
+ if (this.$enableImageCompression.prop("checked")) {
+ this.$imageCompressionWrapper.removeClass("disabled-field");
+ } else {
+ this.$imageCompressionWrapper.addClass("disabled-field");
+ }
+ }
+}
diff --git a/src/public/app/widgets/dialogs/options/options_tab.js b/src/public/app/widgets/type_widgets/options/options_widget.js
similarity index 61%
rename from src/public/app/widgets/dialogs/options/options_tab.js
rename to src/public/app/widgets/type_widgets/options/options_widget.js
index 3e74dcbc7..ad0bf97a2 100644
--- a/src/public/app/widgets/dialogs/options/options_tab.js
+++ b/src/public/app/widgets/type_widgets/options/options_widget.js
@@ -1,8 +1,15 @@
-import BasicWidget from "../../basic_widget.js";
import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
+import NoteContextAwareWidget from "../../note_context_aware_widget.js";
+import attributeService from "../../../services/attributes.js";
+
+export default class OptionsWidget extends NoteContextAwareWidget {
+ constructor() {
+ super();
+
+ this.contentSized();
+ }
-export default class OptionsTab extends BasicWidget {
async updateOption(name, value) {
const opts = { [name]: value };
@@ -34,4 +41,18 @@ export default class OptionsTab extends BasicWidget {
setCheckboxState($checkbox, optionValue) {
$checkbox.prop('checked', optionValue === 'true');
}
+
+ optionsLoaded(options) {}
+
+ async refreshWithNote(note) {
+ const options = await server.get('options');
+
+ this.optionsLoaded(options);
+ }
+
+ async entitiesReloadedEvent({loadResults}) {
+ if (loadResults.options.length > 0) {
+ this.refresh();
+ }
+ }
}
diff --git a/src/public/app/widgets/type_widgets/options/other/network_connections.js b/src/public/app/widgets/type_widgets/options/other/network_connections.js
new file mode 100644
index 000000000..6dea5a332
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/other/network_connections.js
@@ -0,0 +1,24 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Network connections
+
+
+
+ Check for updates automatically
+
+`;
+
+export default class NetworkConnectionsOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$checkForUpdates = this.$widget.find(".check-for-updates");
+ this.$checkForUpdates.on("change", () =>
+ this.updateCheckboxOption('checkForUpdates', this.$checkForUpdates));
+ }
+
+ async optionsLoaded(options) {
+ this.setCheckboxState(this.$checkForUpdates, options.checkForUpdates);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/other/note_erasure_timeout.js b/src/public/app/widgets/type_widgets/options/other/note_erasure_timeout.js
new file mode 100644
index 000000000..1b455788a
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/other/note_erasure_timeout.js
@@ -0,0 +1,41 @@
+import OptionsWidget from "../options_widget.js";
+import server from "../../../../services/server.js";
+import toastService from "../../../../services/toast.js";
+
+const TPL = `
+
+
Note erasure timeout
+
+
Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them
+ from Recent Notes dialog. After a period of time, deleted notes are "erased" which means
+ their content is not recoverable anymore. This setting allows you to configure the length
+ of the period between deleting and erasing the note.
+
+
+ Erase notes after X seconds
+
+
+
+
You can also trigger erasing manually:
+
+
Erase deleted notes now
+
`;
+
+export default class NoteErasureTimeoutOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$eraseEntitiesAfterTimeInSeconds = this.$widget.find(".erase-entities-after-time-in-seconds");
+ this.$eraseEntitiesAfterTimeInSeconds.on('change', () => this.updateOption('eraseEntitiesAfterTimeInSeconds', this.$eraseEntitiesAfterTimeInSeconds.val()));
+
+ this.$eraseDeletedNotesButton = this.$widget.find(".erase-deleted-notes-now-button");
+ this.$eraseDeletedNotesButton.on('click', () => {
+ server.post('notes/erase-deleted-notes-now').then(() => {
+ toastService.showMessage("Deleted notes have been erased.");
+ });
+ });
+ }
+
+ async optionsLoaded(options) {
+ this.$eraseEntitiesAfterTimeInSeconds.val(options.eraseEntitiesAfterTimeInSeconds);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/other/note_revisions_snapshot_interval.js b/src/public/app/widgets/type_widgets/options/other/note_revisions_snapshot_interval.js
new file mode 100644
index 000000000..5e02991f8
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/other/note_revisions_snapshot_interval.js
@@ -0,0 +1,26 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Note revisions snapshot interval
+
+
Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See wiki for more info.
+
+
+ Note revision snapshot time interval (in seconds)
+
+
+
`;
+
+export default class NoteRevisionsSnapshotIntervalOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$noteRevisionsTimeInterval = this.$widget.find(".note-revision-snapshot-time-interval-in-seconds");
+ this.$noteRevisionsTimeInterval.on('change', () =>
+ this.updateOption('noteRevisionSnapshotTimeInterval', this.$noteRevisionsTimeInterval.val()));
+ }
+
+ async optionsLoaded(options) {
+ this.$noteRevisionsTimeInterval.val(options.noteRevisionSnapshotTimeInterval);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/other/tray.js b/src/public/app/widgets/type_widgets/options/other/tray.js
new file mode 100644
index 000000000..e0cfdac62
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/other/tray.js
@@ -0,0 +1,24 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Tray
+
+
+
+ Enable tray (Trilium needs to be restarted for this change to take effect)
+
+`;
+
+export default class TrayOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$trayEnabled = this.$widget.find(".tray-enabled");
+ this.$trayEnabled.on('change', () =>
+ this.updateOption('disableTray', !this.$trayEnabled.is(":checked") ? "true" : "false"));
+ }
+
+ async optionsLoaded(options) {
+ this.$trayEnabled.prop("checked", options.disableTray !== 'true');
+ }
+}
diff --git a/src/public/app/widgets/dialogs/options/password.js b/src/public/app/widgets/type_widgets/options/password.js
similarity index 67%
rename from src/public/app/widgets/dialogs/options/password.js
rename to src/public/app/widgets/type_widgets/options/password.js
index 21eb75306..98838a264 100644
--- a/src/public/app/widgets/dialogs/options/password.js
+++ b/src/public/app/widgets/type_widgets/options/password.js
@@ -1,35 +1,35 @@
import server from "../../../services/server.js";
import protectedSessionHolder from "../../../services/protected_session_holder.js";
import toastService from "../../../services/toast.js";
-import OptionsTab from "./options_tab.js";
+import OptionsWidget from "./options_widget.js";
const TPL = `
-
+
Please take care to remember your new password. Password is used for logging into the web interface and
to encrypt protected notes. If you forget your password, then all your protected notes are forever lost.
- In case you did forget your password,
click here to reset it .
+ In case you did forget your password,
click here to reset it .
-
`;
-export default class PasswordOptions extends OptionsTab {
- get tabTitle() { return "Password" }
-
- lazyRender() {
+export default class PasswordOptions extends OptionsWidget {
+ doRender() {
this.$widget = $(TPL);
- this.$passwordHeading = this.$widget.find("#password-heading");
- this.$changePasswordForm = this.$widget.find("#change-password-form");
- this.$oldPassword = this.$widget.find("#old-password");
- this.$newPassword1 = this.$widget.find("#new-password1");
- this.$newPassword2 = this.$widget.find("#new-password2");
- this.$savePasswordButton = this.$widget.find("#save-password-button");
- this.$resetPasswordButton = this.$widget.find("#reset-password-button");
+ this.$passwordHeading = this.$widget.find(".password-heading");
+ this.$changePasswordForm = this.$widget.find(".change-password-form");
+ this.$oldPassword = this.$widget.find(".old-password");
+ this.$newPassword1 = this.$widget.find(".new-password1");
+ this.$newPassword2 = this.$widget.find(".new-password2");
+ this.$savePasswordButton = this.$widget.find(".save-password-button");
+ this.$resetPasswordButton = this.$widget.find(".reset-password-button");
this.$resetPasswordButton.on("click", async () => {
if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) {
@@ -72,7 +70,7 @@ export default class PasswordOptions extends OptionsTab {
this.$changePasswordForm.on('submit', () => this.save());
- this.$protectedSessionTimeout = this.$widget.find("#protected-session-timeout-in-seconds");
+ this.$protectedSessionTimeout = this.$widget.find(".protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on('change', () =>
this.updateOption('protectedSessionTimeout', this.$protectedSessionTimeout.val()));
}
@@ -80,7 +78,7 @@ export default class PasswordOptions extends OptionsTab {
optionsLoaded(options) {
const isPasswordSet = options.isPasswordSet === 'true';
- this.$widget.find("#old-password-form-group").toggle(isPasswordSet);
+ this.$widget.find(".old-password-form-group").toggle(isPasswordSet);
this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password');
this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password');
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
diff --git a/src/public/app/widgets/dialogs/options/shortcuts.js b/src/public/app/widgets/type_widgets/options/shortcuts.js
similarity index 76%
rename from src/public/app/widgets/dialogs/options/shortcuts.js
rename to src/public/app/widgets/type_widgets/options/shortcuts.js
index 224431850..f3393fdca 100644
--- a/src/public/app/widgets/dialogs/options/shortcuts.js
+++ b/src/public/app/widgets/type_widgets/options/shortcuts.js
@@ -1,10 +1,30 @@
import server from "../../../services/server.js";
import utils from "../../../services/utils.js";
-import dialogService from "../../dialog.js";
-import OptionsTab from "./options_tab.js";
+import dialogService from "../../../services/dialog.js";
+import OptionsWidget from "./options_widget.js";
const TPL = `
-
+
+
+
Keyboard shortcuts
@@ -13,11 +33,11 @@ const TPL = `
-
+
-
-
+
+
Action name
@@ -30,24 +50,22 @@ const TPL = `
-
-
Reload app to apply changes
+
+ Reload app to apply changes
- Set all shortcuts to the default
+ Set all shortcuts to the default
`;
let globActions;
-export default class KeyboardShortcutsOptions extends OptionsTab {
- get tabTitle() { return "Shortcuts" }
-
- lazyRender() {
+export default class KeyboardShortcutsOptions extends OptionsWidget {
+ doRender() {
this.$widget = $(TPL);
- this.$widget.find("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadFrontendApp());
+ this.$widget.find(".options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadFrontendApp());
- const $table = this.$widget.find("#keyboard-shortcut-table tbody");
+ const $table = this.$widget.find(".keyboard-shortcut-table tbody");
server.get('keyboard-actions').then(actions => {
globActions = actions;
@@ -93,7 +111,7 @@ export default class KeyboardShortcutsOptions extends OptionsTab {
this.updateOption(optionName, JSON.stringify(shortcuts));
});
- this.$widget.find("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
+ this.$widget.find(".options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
if (!await dialogService.confirm("Do you really want to reset all keyboard shortcuts to the default?")) {
return;
}
@@ -109,7 +127,7 @@ export default class KeyboardShortcutsOptions extends OptionsTab {
});
});
- const $filter = this.$widget.find("#keyboard-shortcut-filter");
+ const $filter = this.$widget.find(".keyboard-shortcut-filter");
$filter.on('keyup', () => {
const filter = $filter.val().trim().toLowerCase();
diff --git a/src/public/app/widgets/dialogs/options/spellcheck.js b/src/public/app/widgets/type_widgets/options/spellcheck.js
similarity index 59%
rename from src/public/app/widgets/dialogs/options/spellcheck.js
rename to src/public/app/widgets/type_widgets/options/spellcheck.js
index 1ef2a58c1..36d144434 100644
--- a/src/public/app/widgets/dialogs/options/spellcheck.js
+++ b/src/public/app/widgets/type_widgets/options/spellcheck.js
@@ -1,5 +1,5 @@
import utils from "../../../services/utils.js";
-import OptionsTab from "./options_tab.js";
+import OptionsWidget from "./options_widget.js";
const TPL = `
`;
-export default class SpellcheckOptions extends OptionsTab {
- get tabTitle() { return "Spellcheck" }
-
- lazyRender() {
+export default class SpellcheckOptions extends OptionsWidget {
+ doRender() {
this.$widget = $(TPL);
- this.$spellCheckEnabled = this.$widget.find("#spell-check-enabled");
- this.$spellCheckLanguageCode = this.$widget.find("#spell-check-language-code");
+ this.$spellCheckEnabled = this.$widget.find(".spell-check-enabled");
+ this.$spellCheckLanguageCode = this.$widget.find(".spell-check-language-code");
this.$spellCheckEnabled.on('change', () =>
this.updateCheckboxOption('spellCheckEnabled', this.$spellCheckEnabled));
@@ -39,7 +37,7 @@ export default class SpellcheckOptions extends OptionsTab {
this.$spellCheckLanguageCode.on('change', () =>
this.updateOption('spellCheckLanguageCode', this.$spellCheckLanguageCode.val()));
- this.$availableLanguageCodes = this.$widget.find("#available-language-codes");
+ this.$availableLanguageCodes = this.$widget.find(".available-language-codes");
if (utils.isElectron()) {
const { webContents } = utils.dynamicRequire('@electron/remote').getCurrentWindow();
diff --git a/src/public/app/widgets/dialogs/options/sync.js b/src/public/app/widgets/type_widgets/options/sync.js
similarity index 64%
rename from src/public/app/widgets/dialogs/options/sync.js
rename to src/public/app/widgets/type_widgets/options/sync.js
index 0f788571e..e7ac9b8cc 100644
--- a/src/public/app/widgets/dialogs/options/sync.js
+++ b/src/public/app/widgets/type_widgets/options/sync.js
@@ -1,25 +1,25 @@
import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
-import OptionsTab from "./options_tab.js";
+import OptionsWidget from "./options_widget.js";
const TPL = `
`;
-export default class SyncOptions extends OptionsTab {
- get tabTitle() { return "Sync" }
-
- lazyRender() {
+export default class SyncOptions extends OptionsWidget {
+ doRender() {
this.$widget = $(TPL);
- this.$form = this.$widget.find("#sync-setup-form");
- this.$syncServerHost = this.$widget.find("#sync-server-host");
- this.$syncServerTimeout = this.$widget.find("#sync-server-timeout");
- this.$syncProxy = this.$widget.find("#sync-proxy");
- this.$testSyncButton = this.$widget.find("#test-sync-button");
+ this.$form = this.$widget.find(".sync-setup-form");
+ this.$syncServerHost = this.$widget.find(".sync-server-host");
+ this.$syncServerTimeout = this.$widget.find(".sync-server-timeout");
+ this.$syncProxy = this.$widget.find(".sync-proxy");
+ this.$testSyncButton = this.$widget.find(".test-sync-button");
this.$form.on('submit', () => this.save());
diff --git a/src/public/app/widgets/type_widgets/options/text_notes/heading_style.js b/src/public/app/widgets/type_widgets/options/text_notes/heading_style.js
new file mode 100644
index 000000000..a50ae7b4c
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/text_notes/heading_style.js
@@ -0,0 +1,40 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Heading style
+
+ Plain
+ Underline
+ Markdown-style
+
+`;
+
+export default class HeadingStyleOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$body = $("body");
+ this.$headingStyle = this.$widget.find(".heading-style");
+ this.$headingStyle.on('change', () => {
+ const newHeadingStyle = this.$headingStyle.val();
+
+ this.toggleBodyClass("heading-style-", newHeadingStyle);
+
+ this.updateOption('headingStyle', newHeadingStyle);
+ });
+ }
+
+ async optionsLoaded(options) {
+ this.$headingStyle.val(options.headingStyle);
+ }
+
+ toggleBodyClass(prefix, value) {
+ for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes
+ if (clazz.startsWith(prefix)) {
+ this.$body.removeClass(clazz);
+ }
+ }
+
+ this.$body.addClass(prefix + value);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/text_notes/table_of_contents.js b/src/public/app/widgets/type_widgets/options/text_notes/table_of_contents.js
new file mode 100644
index 000000000..a5fa1083f
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/text_notes/table_of_contents.js
@@ -0,0 +1,27 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Table of contents
+
+ Table of contents will appear in text notes when the note has more than a defined number of headings. You can customize this number:
+
+
+
+
+
+
You can also use this option to effectively disable TOC by setting a very high number.
+
`;
+
+export default class TableOfContentsOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$minTocHeadings = this.$widget.find(".min-toc-headings");
+ this.$minTocHeadings.on('change', () =>
+ this.updateOption('minTocHeadings', this.$minTocHeadings.val()));
+ }
+
+ async optionsLoaded(options) {
+ this.$minTocHeadings.val(options.minTocHeadings);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/options/text_notes/text_auto_read_only_size.js b/src/public/app/widgets/type_widgets/options/text_notes/text_auto_read_only_size.js
new file mode 100644
index 000000000..c72640e73
--- /dev/null
+++ b/src/public/app/widgets/type_widgets/options/text_notes/text_auto_read_only_size.js
@@ -0,0 +1,26 @@
+import OptionsWidget from "../options_widget.js";
+
+const TPL = `
+
+
Automatic read-only size
+
+
Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).
+
+
+ Automatic read-only size (text notes)
+
+
+
`;
+
+export default class TextAutoReadOnlySizeOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$autoReadonlySizeText = this.$widget.find(".auto-readonly-size-text");
+ this.$autoReadonlySizeText.on('change', () =>
+ this.updateOption('autoReadonlySizeText', this.$autoReadonlySizeText.val()));
+ }
+
+ async optionsLoaded(options) {
+ this.$autoReadonlySizeText.val(options.autoReadonlySizeText);
+ }
+}
diff --git a/src/public/app/widgets/type_widgets/protected_session.js b/src/public/app/widgets/type_widgets/protected_session.js
index 8a7508894..994313fc2 100644
--- a/src/public/app/widgets/type_widgets/protected_session.js
+++ b/src/public/app/widgets/type_widgets/protected_session.js
@@ -21,7 +21,7 @@ const TPL = `
`;
export default class ProtectedSessionTypeWidget extends TypeWidget {
- static getType() { return "protected-session"; }
+ static getType() { return "protectedSession"; }
doRender() {
this.$widget = $(TPL);
diff --git a/src/public/app/widgets/type_widgets/read_only_code.js b/src/public/app/widgets/type_widgets/read_only_code.js
index 8a54d4bc0..bd817fa65 100644
--- a/src/public/app/widgets/type_widgets/read_only_code.js
+++ b/src/public/app/widgets/type_widgets/read_only_code.js
@@ -17,7 +17,7 @@ const TPL = `
`;
export default class ReadOnlyCodeTypeWidget extends TypeWidget {
- static getType() { return "read-only-code"; }
+ static getType() { return "readOnlyCode"; }
doRender() {
this.$widget = $(TPL);
diff --git a/src/public/app/widgets/type_widgets/read_only_text.js b/src/public/app/widgets/type_widgets/read_only_text.js
index eabc32cfc..ff02138db 100644
--- a/src/public/app/widgets/type_widgets/read_only_text.js
+++ b/src/public/app/widgets/type_widgets/read_only_text.js
@@ -35,6 +35,10 @@ const TPL = `
min-height: 50px;
position: relative;
}
+
+ body.mobile .note-detail-readonly-text {
+ padding-left: 10px;
+ }
.note-detail-readonly-text p:first-child, .note-detail-readonly-text::before {
margin-top: 0;
@@ -67,7 +71,7 @@ const TPL = `
`;
export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
- static getType() { return "read-only-text"; }
+ static getType() { return "readOnlyText"; }
doRender() {
this.$widget = $(TPL);
diff --git a/src/public/app/widgets/type_widgets/relation_map.js b/src/public/app/widgets/type_widgets/relation_map.js
index 615b9d069..c3e1a1402 100644
--- a/src/public/app/widgets/type_widgets/relation_map.js
+++ b/src/public/app/widgets/type_widgets/relation_map.js
@@ -1,14 +1,14 @@
import server from "../../services/server.js";
import linkService from "../../services/link.js";
import libraryLoader from "../../services/library_loader.js";
-import contextMenu from "../../services/context_menu.js";
+import contextMenu from "../../menus/context_menu.js";
import toastService from "../../services/toast.js";
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
import TypeWidget from "./type_widget.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
import utils from "../../services/utils.js";
import froca from "../../services/froca.js";
-import dialogService from "../../widgets/dialog.js";
+import dialogService from "../../services/dialog.js";
const uniDirectionalOverlays = [
[ "Arrow", {
@@ -74,7 +74,7 @@ const TPL = `
let containerCounter = 1;
export default class RelationMapTypeWidget extends TypeWidget {
- static getType() { return "relation-map"; }
+ static getType() { return "relationMap"; }
doRender() {
this.$widget = $(TPL);
diff --git a/src/public/app/widgets/type_widgets/type_widget.js b/src/public/app/widgets/type_widgets/type_widget.js
index f3dd88317..434ba2176 100644
--- a/src/public/app/widgets/type_widgets/type_widget.js
+++ b/src/public/app/widgets/type_widgets/type_widget.js
@@ -1,5 +1,5 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
-import appContext from "../../services/app_context.js";
+import appContext from "../../components/app_context.js";
export default class TypeWidget extends NoteContextAwareWidget {
// for overriding
@@ -40,7 +40,7 @@ export default class TypeWidget extends NoteContextAwareWidget {
/**
* @returns {Promise|*} promise resolving content or directly the content
- */
+ */
getContent() {}
focus() {}
diff --git a/src/public/app/widgets/type_widgets/web_view.js b/src/public/app/widgets/type_widgets/web_view.js
index e35bd79b3..766bb608b 100644
--- a/src/public/app/widgets/type_widgets/web_view.js
+++ b/src/public/app/widgets/type_widgets/web_view.js
@@ -19,7 +19,7 @@ const TPL = `
`;
export default class WebViewTypeWidget extends TypeWidget {
- static getType() { return "web-view"; }
+ static getType() { return "webView"; }
doRender() {
this.$widget = $(TPL);
diff --git a/src/public/stylesheets/relation_map.css b/src/public/stylesheets/relation_map.css
index 1882a6d48..c78767de4 100644
--- a/src/public/stylesheets/relation_map.css
+++ b/src/public/stylesheets/relation_map.css
@@ -1,4 +1,4 @@
-.type-relation-map .note-detail {
+.type-relationMap .note-detail {
height: 100%;
}
diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css
index 755fd61f3..fc6361bc3 100644
--- a/src/public/stylesheets/style.css
+++ b/src/public/stylesheets/style.css
@@ -109,7 +109,6 @@ button.close:hover {
.icon-action {
border: 1px solid transparent;
- padding: 5px;
width: 35px;
height: 35px;
cursor: pointer;
@@ -863,13 +862,14 @@ body {
margin: 10px;
}
-#launcher-pane .icon-action {
+#launcher-pane .launcher-button {
font-size: 150%;
display: inline-block;
padding: 15px 15px;
cursor: pointer;
border: none;
color: var(--launcher-pane-text-color);
+ background-color: var(--launcher-pane-background-color);
width: 53px;
height: 53px;
}
@@ -967,3 +967,17 @@ button.close:hover {
top: 1px;
margin-right: 3px;
}
+
+.options-section:first-of-type h4 {
+ margin-top: 0;
+}
+
+.options-section h4 {
+ margin-top: 15px;
+ margin-bottom: 15px;
+}
+
+.options-section h5 {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
diff --git a/src/public/stylesheets/tree.css b/src/public/stylesheets/tree.css
index c0cfa08dd..8962e812d 100644
--- a/src/public/stylesheets/tree.css
+++ b/src/public/stylesheets/tree.css
@@ -72,11 +72,11 @@ span.fancytree-node.fancytree-hide {
color: inherit !important;
display: block;
border-radius: 50%;
- border-color: #000 transparent #000 transparent;
+ border-color: var(--main-text-color) transparent var(--main-text-color) transparent;
animation: lds-dual-ring 1.2s linear infinite;
width: 12px;
height: 12px;
- margin-top: 4px;
+ margin-top: 2px;
margin-left: 1px;
border-width: 1px;
border-style: solid;
@@ -211,6 +211,11 @@ span.fancytree-node.archived {
border-radius: 5px;
}
+.unhoist-button.bx.tree-item-button {
+ margin-left: 0; /* unhoist button is on the left and doesn't need more margin */
+ display: block; /* keep always visible */
+}
+
.tree-item-button:hover {
border: 1px dotted var(--main-text-color);
}
diff --git a/src/routes/api/attributes.js b/src/routes/api/attributes.js
index 31f544c1c..ec4ab2c3f 100644
--- a/src/routes/api/attributes.js
+++ b/src/routes/api/attributes.js
@@ -5,6 +5,8 @@ const log = require('../../services/log');
const attributeService = require('../../services/attributes');
const Attribute = require('../../becca/entities/attribute');
const becca = require("../../becca/becca");
+const ValidationError = require("../../errors/validation_error");
+const NotFoundError = require("../../errors/not_found_error");
function getEffectiveNoteAttributes(req) {
const note = becca.getNote(req.params.noteId);
@@ -20,8 +22,12 @@ function updateNoteAttribute(req) {
if (body.attributeId) {
attribute = becca.getAttribute(body.attributeId);
+ if (!attribute) {
+ throw new NotFoundError(`Attribute '${body.attributeId}' does not exist.`);
+ }
+
if (attribute.noteId !== noteId) {
- return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`];
+ throw new ValidationError(`Attribute '${body.attributeId}' is not owned by ${noteId}`);
}
if (body.type !== attribute.type
@@ -102,7 +108,7 @@ function deleteNoteAttribute(req) {
if (attribute) {
if (attribute.noteId !== noteId) {
- return [400, `Attribute ${attributeId} is not owned by ${noteId}`];
+ throw new ValidationError(`Attribute ${attributeId} is not owned by ${noteId}`);
}
attribute.markAsDeleted();
diff --git a/src/routes/api/branches.js b/src/routes/api/branches.js
index a44a93f1e..cb3f31b71 100644
--- a/src/routes/api/branches.js
+++ b/src/routes/api/branches.js
@@ -8,7 +8,9 @@ const noteService = require('../../services/notes');
const becca = require('../../becca/becca');
const TaskContext = require('../../services/task_context');
const branchService = require("../../services/branches");
-const log = require("../../services/log.js");
+const log = require("../../services/log");
+const ValidationError = require("../../errors/validation_error");
+const NotFoundError = require("../../errors/not_found_error");
/**
* Code in this file deals with moving and cloning branches. Relationship between note and parent note is unique
@@ -22,7 +24,7 @@ function moveBranchToParent(req) {
const branchToMove = becca.getBranch(branchId);
if (!parentBranch || !branchToMove) {
- return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
+ throw new ValidationError(`One or both branches ${branchId}, ${parentBranchId} have not been found`);
}
return branchService.moveBranchToBranch(branchToMove, parentBranch, branchId);
@@ -35,11 +37,11 @@ function moveBranchBeforeNote(req) {
const beforeBranch = becca.getBranch(beforeBranchId);
if (!branchToMove) {
- return [404, `Can't find branch ${branchId}`];
+ throw new NotFoundError(`Can't find branch '${branchId}'`);
}
if (!beforeBranch) {
- return [404, `Can't find branch ${beforeBranchId}`];
+ throw new NotFoundError(`Can't find branch '${beforeBranchId}'`);
}
const validationResult = treeService.validateParentChild(beforeBranch.parentNoteId, branchToMove.noteId, branchId);
@@ -193,7 +195,7 @@ function deleteBranch(req) {
const branch = becca.getBranch(req.params.branchId);
if (!branch) {
- return [404, `Branch ${req.params.branchId} not found`];
+ throw new NotFoundError(`Branch '${req.params.branchId}' not found`);
}
const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes');
diff --git a/src/routes/api/bulk_action.js b/src/routes/api/bulk_action.js
index 902e9a675..ebf431040 100644
--- a/src/routes/api/bulk_action.js
+++ b/src/routes/api/bulk_action.js
@@ -6,7 +6,7 @@ function execute(req) {
const affectedNoteIds = getAffectedNoteIds(noteIds, includeDescendants);
- const bulkActionNote = becca.getNote('bulkaction');
+ const bulkActionNote = becca.getNote('bulkAction');
bulkActionService.executeActions(bulkActionNote, affectedNoteIds);
}
diff --git a/src/routes/api/cloning.js b/src/routes/api/cloning.js
index f00343afd..da557715c 100644
--- a/src/routes/api/cloning.js
+++ b/src/routes/api/cloning.js
@@ -22,8 +22,15 @@ function cloneNoteAfter(req) {
return cloningService.cloneNoteAfter(noteId, afterBranchId);
}
+function toggleNoteInParent(req) {
+ const {noteId, parentNoteId, present} = req.params;
+
+ return cloningService.toggleNoteInParent(present === 'true', noteId, parentNoteId);
+}
+
module.exports = {
cloneNoteToBranch,
cloneNoteToNote,
- cloneNoteAfter
+ cloneNoteAfter,
+ toggleNoteInParent
};
diff --git a/src/routes/api/export.js b/src/routes/api/export.js
index 6acb19952..365c7e185 100644
--- a/src/routes/api/export.js
+++ b/src/routes/api/export.js
@@ -6,6 +6,7 @@ const opmlExportService = require('../../services/export/opml');
const becca = require('../../becca/becca');
const TaskContext = require("../../services/task_context");
const log = require("../../services/log");
+const NotFoundError = require("../../errors/not_found_error");
function exportBranch(req, res) {
const {branchId, type, format, version, taskId} = req.params;
@@ -34,11 +35,11 @@ function exportBranch(req, res) {
opmlExportService.exportToOpml(taskContext, branch, version, res);
}
else {
- return [404, "Unrecognized export format " + format];
+ throw new NotFoundError(`Unrecognized export format '${format}'`);
}
}
catch (e) {
- const message = "Export failed with following error: '" + e.message + "'. More details might be in the logs.";
+ const message = `Export failed with following error: '${e.message}'. More details might be in the logs.`;
taskContext.reportError(message);
log.error(message + e.stack);
diff --git a/src/routes/api/files.js b/src/routes/api/files.js
index fc7ae6591..7e41af4ae 100644
--- a/src/routes/api/files.js
+++ b/src/routes/api/files.js
@@ -10,6 +10,7 @@ const { Readable } = require('stream');
const chokidar = require('chokidar');
const ws = require('../../services/ws');
const becca = require("../../becca/becca");
+const NotFoundError = require("../../errors/not_found_error");
function updateFile(req) {
const {noteId} = req.params;
@@ -18,7 +19,7 @@ function updateFile(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} doesn't exist.`];
+ throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
}
note.saveNoteRevision();
@@ -116,7 +117,7 @@ function saveToTmpDir(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404,`Note ${noteId} doesn't exist.`];
+ throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
}
const tmpObj = tmp.fileSync({postfix: getFilename(note)});
diff --git a/src/routes/api/image.js b/src/routes/api/image.js
index f54395697..aa6151ce6 100644
--- a/src/routes/api/image.js
+++ b/src/routes/api/image.js
@@ -4,6 +4,8 @@ const imageService = require('../../services/image');
const becca = require('../../becca/becca');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs');
+const ValidationError = require("../../errors/validation_error");
+const NotFoundError = require("../../errors/not_found_error");
function returnImage(req, res) {
const image = becca.getNote(req.params.noteId);
@@ -51,11 +53,11 @@ function uploadImage(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} doesn't exist.`];
+ throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
}
if (!["image/png", "image/jpg", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
- return [400, "Unknown image type: " + file.mimetype];
+ throw new ValidationError(`Unknown image type: ${file.mimetype}`);
}
const {url} = imageService.saveImage(noteId, file.buffer, file.originalname, true, true);
@@ -73,7 +75,7 @@ function updateImage(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} doesn't exist.`];
+ throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
}
if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
diff --git a/src/routes/api/import.js b/src/routes/api/import.js
index 80ecbb255..f62f37b7c 100644
--- a/src/routes/api/import.js
+++ b/src/routes/api/import.js
@@ -10,6 +10,8 @@ const becca = require('../../becca/becca');
const beccaLoader = require('../../becca/becca_loader');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
+const ValidationError = require("../../errors/validation_error");
+const NotFoundError = require("../../errors/not_found_error");
async function importToBranch(req) {
const {parentNoteId} = req.params;
@@ -27,13 +29,13 @@ async function importToBranch(req) {
const file = req.file;
if (!file) {
- return [400, "No file has been uploaded"];
+ throw new ValidationError("No file has been uploaded");
}
const parentNote = becca.getNote(parentNoteId);
if (!parentNote) {
- return [404, `Note ${parentNoteId} doesn't exist.`];
+ throw new NotFoundError(`Note '${parentNoteId}' doesn't exist.`);
}
const extension = path.extname(file.originalname).toLowerCase();
diff --git a/src/routes/api/keys.js b/src/routes/api/keys.js
index 1089544ca..bc1b97d4a 100644
--- a/src/routes/api/keys.js
+++ b/src/routes/api/keys.js
@@ -8,15 +8,10 @@ function getKeyboardActions() {
}
function getShortcutsForNotes() {
- const attrs = becca.findAttributes('label', 'keyboardShortcut');
+ const labels = becca.findAttributes('label', 'keyboardShortcut');
- const map = {};
-
- for (const attr of attrs) {
- map[attr.value] = attr.noteId;
- }
-
- return map;
+ // launchers have different handling
+ return labels.filter(attr => becca.getNote(attr.noteId)?.type !== 'launcher');
}
module.exports = {
diff --git a/src/routes/api/note_map.js b/src/routes/api/note_map.js
index 09b593c7f..4485130a0 100644
--- a/src/routes/api/note_map.js
+++ b/src/routes/api/note_map.js
@@ -2,6 +2,7 @@
const becca = require("../../becca/becca");
const { JSDOM } = require("jsdom");
+const NotFoundError = require("../../errors/not_found_error");
function buildDescendantCountMap() {
const noteIdToCountMap = {};
@@ -326,7 +327,7 @@ function getBacklinkCount(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, "Not found"];
+ throw new NotFoundError(`Note '${noteId}' not found`);
}
else {
return {
@@ -340,7 +341,7 @@ function getBacklinks(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} was not found`];
+ throw new NotFoundError(`Note '${noteId}' was not found`);
}
let backlinksWithExcerptCount = 0;
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index ebb345623..ba112b034 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -6,16 +6,17 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils');
const log = require('../../services/log');
const TaskContext = require('../../services/task_context');
-const protectedSessionService = require('../../services/protected_session');
const fs = require('fs');
const becca = require("../../becca/becca");
+const ValidationError = require("../../errors/validation_error");
+const NotFoundError = require("../../errors/not_found_error");
function getNote(req) {
const noteId = req.params.noteId;
const note = becca.getNote(noteId);
if (!note) {
- return [404, "Note " + noteId + " has not been found."];
+ throw new NotFoundError(`Note '${noteId}' has not been found.`);
}
const pojo = note.getPojo();
@@ -197,11 +198,11 @@ function changeTitle(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note '${noteId}' has not been found`];
+ throw new NotFoundError(`Note '${noteId}' has not been found`);
}
if (!note.isContentAvailable()) {
- return [400, `Note '${noteId}' is not available for change`];
+ throw new ValidationError(`Note '${noteId}' is not available for change`);
}
const noteTitleChanged = note.title !== title;
@@ -290,7 +291,7 @@ function uploadModifiedFile(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note '${noteId}' has not been found`];
+ throw new NotFoundError(`Note '${noteId}' has not been found`);
}
log.info(`Updating note '${noteId}' with content from ${filePath}`);
@@ -300,7 +301,7 @@ function uploadModifiedFile(req) {
const fileContent = fs.readFileSync(filePath);
if (!fileContent) {
- return [400, `File ${fileContent} is empty`];
+ throw new ValidationError(`File '${fileContent}' is empty`);
}
note.setContent(fileContent);
@@ -311,11 +312,11 @@ function forceSaveNoteRevision(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} not found.`];
+ throw new NotFoundError(`Note '${noteId}' not found.`);
}
if (!note.isContentAvailable()) {
- return [400, `Note revision of a protected note cannot be created outside of a protected session.`];
+ throw new ValidationError(`Note revision of a protected note cannot be created outside of a protected session.`);
}
note.saveNoteRevision();
diff --git a/src/routes/api/options.js b/src/routes/api/options.js
index e8e7bfe08..a176bb45f 100644
--- a/src/routes/api/options.js
+++ b/src/routes/api/options.js
@@ -3,6 +3,7 @@
const optionService = require('../../services/options');
const log = require('../../services/log');
const searchService = require('../../services/search/services/search');
+const ValidationError = require("../../errors/validation_error");
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = new Set([
@@ -82,7 +83,7 @@ function updateOption(req) {
const {name, value} = req.params;
if (!update(name, value)) {
- return [400, "not allowed option to change"];
+ throw new ValidationError("not allowed option to change");
}
}
diff --git a/src/routes/api/password.js b/src/routes/api/password.js
index 47466bd7e..289a18716 100644
--- a/src/routes/api/password.js
+++ b/src/routes/api/password.js
@@ -1,6 +1,7 @@
"use strict";
const passwordService = require('../../services/password');
+const ValidationError = require("../../errors/validation_error");
function changePassword(req) {
if (passwordService.isPasswordSet()) {
@@ -14,7 +15,7 @@ function changePassword(req) {
function resetPassword(req) {
// protection against accidental call (not a security measure)
if (req.query.really !== "yesIReallyWantToResetPasswordAndLoseAccessToMyProtectedNotes") {
- return [400, "Incorrect password reset confirmation"];
+ throw new ValidationError("Incorrect password reset confirmation");
}
return passwordService.resetPassword();
diff --git a/src/routes/api/search.js b/src/routes/api/search.js
index 929cc7c05..b9c1f5412 100644
--- a/src/routes/api/search.js
+++ b/src/routes/api/search.js
@@ -6,12 +6,14 @@ const searchService = require('../../services/search/services/search');
const bulkActionService = require("../../services/bulk_actions");
const cls = require("../../services/cls");
const {formatAttrForSearch} = require("../../services/attribute_formatter");
+const ValidationError = require("../../errors/validation_error");
+const NotFoundError = require("../../errors/not_found_error");
function searchFromNote(req) {
const note = becca.getNote(req.params.noteId);
if (!note) {
- return [404, `Note ${req.params.noteId} has not been found.`];
+ throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`);
}
if (note.isDeleted) {
@@ -20,7 +22,7 @@ function searchFromNote(req) {
}
if (note.type !== 'search') {
- return [400, `Note ${req.params.noteId} is not a search note.`]
+ throw new ValidationError(`Note '${req.params.noteId}' is not a search note.`);
}
return searchService.searchFromNote(note);
@@ -30,16 +32,16 @@ function searchAndExecute(req) {
const note = becca.getNote(req.params.noteId);
if (!note) {
- return [404, `Note ${req.params.noteId} has not been found.`];
+ throw new NotFoundError(`Note '${req.params.noteId}' has not been found.`);
}
if (note.isDeleted) {
- // this can be triggered from recent changes and it's harmless to return empty list rather than fail
+ // this can be triggered from recent changes, and it's harmless to return empty list rather than fail
return [];
}
if (note.type !== 'search') {
- return [400, `Note ${req.params.noteId} is not a search note.`]
+ throw new ValidationError(`Note '${req.params.noteId}' is not a search note.`);
}
const {searchResultNoteIds} = searchService.searchFromNote(note);
diff --git a/src/routes/api/similar_notes.js b/src/routes/api/similar_notes.js
index 348a3ddde..e27968ebe 100644
--- a/src/routes/api/similar_notes.js
+++ b/src/routes/api/similar_notes.js
@@ -2,6 +2,7 @@
const similarityService = require('../../becca/similarity');
const becca = require("../../becca/becca");
+const NotFoundError = require("../../errors/not_found_error");
async function getSimilarNotes(req) {
const noteId = req.params.noteId;
@@ -9,7 +10,7 @@ async function getSimilarNotes(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} not found.`];
+ throw new NotFoundError(`Note '${noteId}' not found.`);
}
return await similarityService.findSimilarNotes(noteId);
diff --git a/src/routes/api/special_notes.js b/src/routes/api/special_notes.js
index 04ae3e55a..fb9979e54 100644
--- a/src/routes/api/special_notes.js
+++ b/src/routes/api/special_notes.js
@@ -66,6 +66,14 @@ function getHoistedNote() {
return becca.getNote(cls.getHoistedNoteId());
}
+function createLauncher(req) {
+ return specialNotesService.createLauncher(req.params.parentNoteId, req.params.launcherType);
+}
+
+function resetLauncher(req) {
+ return specialNotesService.resetLauncher(req.params.noteId);
+}
+
module.exports = {
getInboxNote,
getDayNote,
@@ -76,5 +84,7 @@ module.exports = {
createSqlConsole,
saveSqlConsole,
createSearchNote,
- saveSearchNote
+ saveSearchNote,
+ createLauncher,
+ resetLauncher
};
diff --git a/src/routes/api/sql.js b/src/routes/api/sql.js
index b20f566e7..09e14cc86 100644
--- a/src/routes/api/sql.js
+++ b/src/routes/api/sql.js
@@ -2,6 +2,7 @@
const sql = require('../../services/sql');
const becca = require("../../becca/becca");
+const NotFoundError = require("../../errors/not_found_error");
function getSchema() {
const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
@@ -21,7 +22,7 @@ function execute(req) {
const note = becca.getNote(req.params.noteId);
if (!note) {
- return [404, `Note ${req.params.noteId} was not found.`];
+ throw new NotFoundError(`Note '${req.params.noteId}' was not found.`);
}
const queries = note.getContent().split("\n---");
diff --git a/src/routes/api/stats.js b/src/routes/api/stats.js
index b747a7e28..43aec3014 100644
--- a/src/routes/api/stats.js
+++ b/src/routes/api/stats.js
@@ -1,5 +1,6 @@
const sql = require('../../services/sql');
const becca = require('../../becca/becca');
+const NotFoundError = require("../../errors/not_found_error");
function getNoteSize(req) {
const {noteId} = req.params;
@@ -26,7 +27,7 @@ function getSubtreeSize(req) {
const note = becca.notes[noteId];
if (!note) {
- return [404, `Note ${noteId} was not found.`];
+ throw new NotFoundError(`Note '${noteId}' was not found.`);
}
const subTreeNoteIds = note.getSubtreeNoteIds();
diff --git a/src/routes/api/tree.js b/src/routes/api/tree.js
index 7874814c2..82b3ace04 100644
--- a/src/routes/api/tree.js
+++ b/src/routes/api/tree.js
@@ -2,6 +2,7 @@
const becca = require('../../becca/becca');
const log = require('../../services/log');
+const NotFoundError = require("../../errors/not_found_error");
function getNotesAndBranchesAndAttributes(noteIds) {
noteIds = new Set(noteIds);
@@ -141,7 +142,7 @@ function getTree(req) {
}
if (!(subTreeNoteId in becca.notes)) {
- return [404, `Note ${subTreeNoteId} not found in the cache`];
+ throw new NotFoundError(`Note '${subTreeNoteId}' not found in the cache`);
}
collect(becca.notes[subTreeNoteId]);
diff --git a/src/routes/login.js b/src/routes/login.js
index bf7a44226..f824931c6 100644
--- a/src/routes/login.js
+++ b/src/routes/login.js
@@ -6,6 +6,7 @@ const myScryptService = require('../services/my_scrypt');
const log = require('../services/log');
const passwordService = require("../services/password");
const assetPath = require("../services/asset_path");
+const ValidationError = require("../errors/validation_error");
function loginPage(req, res) {
res.render('login', {
@@ -23,7 +24,7 @@ function setPasswordPage(req, res) {
function setPassword(req, res) {
if (passwordService.isPasswordSet()) {
- return [400, "Password has been already set"];
+ throw new ValidationError("Password has been already set");
}
let {password1, password2} = req.body;
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 9a46fa3f5..ff1dde1fa 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -5,6 +5,7 @@ const loginRoute = require('./login');
const indexRoute = require('./index');
const utils = require('../services/utils');
const multer = require('multer');
+const ValidationError = require("../errors/validation_error");
// API routes
const treeApiRoute = require('./api/tree');
@@ -61,6 +62,7 @@ const csurf = require('csurf');
const {createPartialContentHandler} = require("express-partial-content");
const rateLimit = require("express-rate-limit");
const AbstractEntity = require("../becca/entities/abstract_entity");
+const NotFoundError = require("../errors/not_found_error");
const csrfMiddleware = csurf({
cookie: true,
@@ -169,13 +171,7 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
log.request(req, res, Date.now() - start, responseLength);
})
- .catch(e => {
- log.error(`${method} ${path} threw exception: ` + e.stack);
-
- res.setHeader("Content-Type", "text/plain")
- .status(500)
- .send(e.message);
- });
+ .catch(e => handleException(method, path, e, res));
}
else {
const responseLength = resultHandler(req, res, result);
@@ -185,15 +181,33 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
}
}
catch (e) {
- log.error(`${method} ${path} threw exception: ` + e.stack);
-
- res.setHeader("Content-Type", "text/plain")
- .status(500)
- .send(e.message);
+ handleException(method, path, e, res);
}
});
}
+function handleException(method, path, e, res) {
+ log.error(`${method} ${path} threw exception: ` + e.stack);
+
+ if (e instanceof ValidationError) {
+ res.setHeader("Content-Type", "application/json")
+ .status(400)
+ .send({
+ message: e.message
+ });
+ } if (e instanceof NotFoundError) {
+ res.setHeader("Content-Type", "application/json")
+ .status(404)
+ .send({
+ message: e.message
+ });
+ } else {
+ res.setHeader("Content-Type", "text/plain")
+ .status(500)
+ .send(e.message);
+ }
+}
+
const MAX_ALLOWED_FILE_SIZE_MB = 250;
const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
@@ -281,6 +295,7 @@ function register(app) {
apiRoute(GET, '/api/edited-notes/:date', noteRevisionsApiRoute.getEditedNotesOnDate);
apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
+ apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent);
apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToNote);
apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter);
@@ -326,6 +341,8 @@ function register(app) {
apiRoute(POST, '/api/special-notes/save-sql-console', specialNotesRoute.saveSqlConsole);
apiRoute(POST, '/api/special-notes/search-note', specialNotesRoute.createSearchNote);
apiRoute(POST, '/api/special-notes/save-search-note', specialNotesRoute.saveSearchNote);
+ apiRoute(POST, '/api/special-notes/launchers/:noteId/reset', specialNotesRoute.resetLauncher);
+ apiRoute(POST, '/api/special-notes/launchers/:parentNoteId/:launcherType', specialNotesRoute.createLauncher);
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
diff --git a/src/routes/setup.js b/src/routes/setup.js
index 9930c7be4..f79f3919d 100644
--- a/src/routes/setup.js
+++ b/src/routes/setup.js
@@ -9,7 +9,8 @@ function setupPage(req, res) {
if (sqlInit.isDbInitialized()) {
if (utils.isElectron()) {
const windowService = require('../services/window');
- windowService.createMainWindow();
+ const {app} = require('electron');
+ windowService.createMainWindow(app);
windowService.closeSetupWindow();
}
else {
diff --git a/src/services/app_info.js b/src/services/app_info.js
index 1c0f7a678..8d26a786e 100644
--- a/src/services/app_info.js
+++ b/src/services/app_info.js
@@ -4,8 +4,8 @@ const build = require('./build');
const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
-const APP_DB_VERSION = 197;
-const SYNC_VERSION = 26;
+const APP_DB_VERSION = 205;
+const SYNC_VERSION = 28;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {
diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js
index ec3705e0f..238e604a8 100644
--- a/src/services/backend_script_api.js
+++ b/src/services/backend_script_api.js
@@ -212,7 +212,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @property {string} parentNoteId - MANDATORY
* @property {string} title - MANDATORY
* @property {string|buffer} content - MANDATORY
- * @property {string} type - text, code, file, image, search, book, relation-map, canvas - MANDATORY
+ * @property {string} type - text, code, file, image, search, book, relationMap, canvas - MANDATORY
* @property {string} mime - value is derived from default mimes for type
* @property {boolean} isProtected - default is false
* @property {boolean} isExpanded - default is false
diff --git a/src/services/branches.js b/src/services/branches.js
index c18ee8b32..f35db98df 100644
--- a/src/services/branches.js
+++ b/src/services/branches.js
@@ -1,5 +1,5 @@
-const treeService = require("./tree.js");
-const sql = require("./sql.js");
+const treeService = require("./tree");
+const sql = require("./sql");
function moveBranchToNote(sourceBranch, targetParentNoteId) {
if (sourceBranch.parentNoteId === targetParentNoteId) {
diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js
index d538a169f..43f67cea2 100644
--- a/src/services/builtin_attributes.js
+++ b/src/services/builtin_attributes.js
@@ -39,7 +39,6 @@ module.exports = [
{ type: 'label', name: 'pageSize' },
{ type: 'label', name: 'viewType' },
{ type: 'label', name: 'mapRootNoteId' },
- { type: 'label', name: 'bookmarked' },
{ type: 'label', name: 'bookmarkFolder' },
{ type: 'label', name: 'sorted' },
{ type: 'label', name: 'sortDirection' },
@@ -61,6 +60,7 @@ module.exports = [
{ type: 'label', name: 'template' },
{ type: 'label', name: 'toc' },
{ type: 'label', name: 'color' },
+ { type: 'label', name: 'keepCurrentHoisting'},
// relation names
{ type: 'relation', name: 'internalLink' },
diff --git a/src/services/cloning.js b/src/services/cloning.js
index 395cb6772..00cd0f721 100644
--- a/src/services/cloning.js
+++ b/src/services/cloning.js
@@ -66,17 +66,16 @@ function cloneNoteToBranch(noteId, parentBranchId, prefix) {
}
function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
- if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
- return { success: false, message: 'Note is deleted.' };
+ if (isNoteDeleted(noteId)) {
+ return { success: false, message: `Note '${noteId}' is deleted.` };
+ } else if (isNoteDeleted(parentNoteId)) {
+ return { success: false, message: `Note '${parentNoteId}' is deleted.` };
}
const parentNote = becca.getNote(parentNoteId);
if (parentNote.type === 'search') {
- return {
- success: false,
- message: "Can't clone into a search note"
- };
+ return { success: false, message: "Can't clone into a search note" };
}
const validationResult = treeService.validateParentChild(parentNoteId, noteId);
@@ -92,7 +91,7 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
isExpanded: 0
}).save();
- log.info(`Ensured note ${noteId} is in parent note ${parentNoteId} with prefix ${prefix}`);
+ log.info(`Ensured note '${noteId}' is in parent note '${parentNoteId}' with prefix '${prefix}'`);
return { success: true };
}
@@ -102,26 +101,37 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
const branch = becca.getBranch(branchId);
if (branch) {
- if (branch.getNote().getParentBranches().length <= 1) {
- throw new Error(`Cannot remove branch ${branch.branchId} between child ${noteId} and parent ${parentNoteId} because this would delete the note as well.`);
+ if (!branch.isWeak && branch.getNote().getStrongParentBranches().length <= 1) {
+ return {
+ success: false,
+ message: `Cannot remove branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' because this would delete the note as well.`
+ };
}
branch.deleteBranch();
- log.info(`Ensured note ${noteId} is NOT in parent note ${parentNoteId}`);
+ log.info(`Ensured note '${noteId}' is NOT in parent note '${parentNoteId}'`);
+
+ return { success: true };
}
}
function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
if (present) {
- ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
+ return ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
}
else {
- ensureNoteIsAbsentFromParent(noteId, parentNoteId);
+ return ensureNoteIsAbsentFromParent(noteId, parentNoteId);
}
}
function cloneNoteAfter(noteId, afterBranchId) {
+ if (['hidden', 'root'].includes(noteId)) {
+ return { success: false, message: 'Cloning the given note is forbidden.' };
+ } else if (afterBranchId === 'hidden') {
+ return { success: false, message: 'Cannot clone after the hidden branch.' };
+ }
+
const afterNote = becca.getBranch(afterBranchId);
if (isNoteDeleted(noteId) || isNoteDeleted(afterNote.parentNoteId)) {
@@ -157,7 +167,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
isExpanded: 0
}).save();
- log.info(`Cloned note ${noteId} into parent note ${afterNote.parentNoteId} after note ${afterNote.noteId}, branch ${afterBranchId}`);
+ log.info(`Cloned note '${noteId}' into parent note '${afterNote.parentNoteId}' after note '${afterNote.noteId}', branch ${afterBranchId}`);
return { success: true, branchId: branch.branchId };
}
@@ -165,7 +175,7 @@ function cloneNoteAfter(noteId, afterBranchId) {
function isNoteDeleted(noteId) {
const note = becca.getNote(noteId);
- return note.isDeleted;
+ return !note || note.isDeleted;
}
module.exports = {
diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js
index 0731989c1..2b3b344e0 100644
--- a/src/services/consistency_checks.js
+++ b/src/services/consistency_checks.js
@@ -140,7 +140,7 @@ class ConsistencyChecks {
});
this.findAndFixIssues(`
- SELECT branchId, branches.noteId AS parentNoteId
+ SELECT branchId, branches.parentNoteId AS parentNoteId
FROM branches
LEFT JOIN notes ON notes.noteId = branches.parentNoteId
WHERE branches.isDeleted = 0
diff --git a/src/services/export/single.js b/src/services/export/single.js
index 881bb394b..838631444 100644
--- a/src/services/export/single.js
+++ b/src/services/export/single.js
@@ -41,7 +41,7 @@ function exportSingleNote(taskContext, branch, format, res) {
extension = mimeTypes.extension(note.mime) || 'code';
mime = note.mime;
}
- else if (note.type === 'relation-map' || note.type === 'canvas' || note.type === 'search') {
+ else if (note.type === 'relationMap' || note.type === 'canvas' || note.type === 'search') {
payload = content;
extension = 'json';
mime = 'application/json';
diff --git a/src/services/handlers.js b/src/services/handlers.js
index e093a9219..5ce6ea8a9 100644
--- a/src/services/handlers.js
+++ b/src/services/handlers.js
@@ -4,6 +4,8 @@ const treeService = require('./tree');
const noteService = require('./notes');
const becca = require('../becca/becca');
const Attribute = require('../becca/entities/attribute');
+const debounce = require('debounce');
+const specialNotesService = require("./special_notes");
function runAttachedRelations(note, relationName, originEntity) {
if (!note) {
diff --git a/src/services/hidden_subtree.js b/src/services/hidden_subtree.js
new file mode 100644
index 000000000..d550d79d2
--- /dev/null
+++ b/src/services/hidden_subtree.js
@@ -0,0 +1,302 @@
+const becca = require("../becca/becca");
+const noteService = require("./notes");
+const log = require("./log");
+
+const LBTPL_ROOT = "lbTplRoot";
+const LBTPL_BASE = "lbTplBase";
+const LBTPL_COMMAND = "lbTplCommand";
+const LBTPL_NOTE_LAUNCHER = "lbTplNoteLauncher";
+const LBTPL_SCRIPT = "lbTplScript";
+const LBTPL_BUILTIN_WIDGET = "lbTplBuiltinWidget";
+const LBTPL_SPACER = "lbTplSpacer";
+const LBTPL_CUSTOM_WIDGET = "lbTplCustomWidget";
+
+const HIDDEN_SUBTREE_DEFINITION = {
+ id: 'hidden',
+ title: 'hidden',
+ type: 'doc',
+ icon: 'bx bx-chip',
+ // we want to keep the hidden subtree always last, otherwise there will be problems with e.g. keyboard navigation
+ // over tree when it's in the middle
+ notePosition: 999_999_999,
+ attributes: [
+ // isInheritable: false means that this notePath is automatically not preffered but at the same time
+ // the flag is not inherited to the children
+ { type: 'label', name: 'archived' },
+ { type: 'label', name: 'excludeFromNoteMap', isInheritable: true }
+ ],
+ children: [
+ {
+ id: 'search',
+ title: 'search',
+ type: 'doc'
+ },
+ {
+ id: 'globalNoteMap',
+ title: 'Note Map',
+ type: 'noteMap',
+ attributes: [
+ { type: 'label', name: 'mapRootNoteId', value: 'hoisted' },
+ { type: 'label', name: 'keepCurrentHoisting' }
+ ]
+ },
+ {
+ id: 'sqlConsole',
+ title: 'SQL Console',
+ type: 'doc',
+ icon: 'bx-data'
+ },
+ {
+ id: 'share',
+ title: 'Shared Notes',
+ type: 'doc',
+ attributes: [ { type: 'label', name: 'docName', value: 'share' } ]
+ },
+ {
+ id: 'bulkAction',
+ title: 'Bulk action',
+ type: 'doc',
+ },
+ {
+ // place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
+ id: 'userHidden',
+ title: 'User Hidden',
+ type: 'text',
+ },
+ {
+ id: LBTPL_ROOT,
+ title: 'Launch Bar Templates',
+ type: 'doc',
+ children: [
+ {
+ id: LBTPL_BASE,
+ title: 'Base Abstract Launcher',
+ type: 'doc'
+ },
+ {
+ id: LBTPL_COMMAND,
+ title: 'Command Launcher',
+ type: 'doc',
+ attributes: [
+ { type: 'relation', name: 'template', value: LBTPL_BASE },
+ { type: 'label', name: 'launcherType', value: 'command' },
+ { type: 'label', name: 'docName', value: 'launchbar_command_launcher' }
+ ]
+ },
+ {
+ id: LBTPL_NOTE_LAUNCHER,
+ title: 'Note Launcher',
+ type: 'doc',
+ attributes: [
+ { type: 'relation', name: 'template', value: LBTPL_BASE },
+ { type: 'label', name: 'launcherType', value: 'note' },
+ { type: 'label', name: 'relation:targetNote', value: 'promoted' },
+ { type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
+ { type: 'label', name: 'docName', value: 'launchbar_note_launcher' }
+ ]
+ },
+ {
+ id: LBTPL_SCRIPT,
+ title: 'Script Launcher',
+ type: 'doc',
+ attributes: [
+ { type: 'relation', name: 'template', value: LBTPL_BASE },
+ { type: 'label', name: 'launcherType', value: 'script' },
+ { type: 'label', name: 'relation:script', value: 'promoted' },
+ { type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
+ { type: 'label', name: 'docName', value: 'launchbar_script_launcher' }
+ ]
+ },
+ {
+ id: LBTPL_BUILTIN_WIDGET,
+ title: 'Built-in Widget',
+ type: 'doc',
+ attributes: [
+ { type: 'relation', name: 'template', value: LBTPL_BASE },
+ { type: 'label', name: 'launcherType', value: 'builtinWidget' }
+ ]
+ },
+ {
+ id: LBTPL_SPACER,
+ title: 'Spacer',
+ type: 'doc',
+ icon: 'bx-move-vertical',
+ attributes: [
+ { type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET },
+ { type: 'label', name: 'builtinWidget', value: 'spacer' },
+ { type: 'label', name: 'label:baseSize', value: 'promoted,number' },
+ { type: 'label', name: 'label:growthFactor', value: 'promoted,number' },
+ { type: 'label', name: 'docName', value: 'launchbar_spacer' }
+ ]
+ },
+ {
+ id: LBTPL_CUSTOM_WIDGET,
+ title: 'Custom Widget',
+ type: 'doc',
+ attributes: [
+ { type: 'relation', name: 'template', value: LBTPL_BASE },
+ { type: 'label', name: 'launcherType', value: 'customWidget' },
+ { type: 'label', name: 'relation:widget', value: 'promoted' },
+ { type: 'label', name: 'docName', value: 'launchbar_widget_launcher' }
+ ]
+ },
+ ]
+ },
+ {
+ id: 'lbRoot',
+ title: 'Launch bar',
+ type: 'doc',
+ icon: 'bx-sidebar',
+ isExpanded: true,
+ attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
+ children: [
+ {
+ id: 'lbAvailableLaunchers',
+ title: 'Available Launchers',
+ type: 'doc',
+ icon: 'bx-hide',
+ isExpanded: true,
+ attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
+ children: [
+ { id: 'lbBackInHistory', title: 'Back in history', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square' },
+ { id: 'lbForwardInHistory', title: 'Forward in history', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square' },
+ ]
+ },
+ {
+ id: 'lbVisibleLaunchers',
+ title: 'Visible Launchers',
+ type: 'doc',
+ icon: 'bx-show',
+ isExpanded: true,
+ attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
+ children: [
+ { id: 'lbNewNote', title: 'New Note', type: 'launcher', command: 'createNoteIntoInbox', icon: 'bx bx-file-blank' },
+ { id: 'lbSearch', title: 'Search Notes', type: 'launcher', command: 'searchNotes', icon: 'bx bx-search', attributes: [
+ { type: 'label', name: 'desktopOnly' }
+ ] },
+ { id: 'lbJumpTo', title: 'Jump to Note', type: 'launcher', command: 'jumpToNote', icon: 'bx bx-send', attributes: [
+ { type: 'label', name: 'desktopOnly' }
+ ] },
+ { id: 'lbNoteMap', title: 'Note Map', type: 'launcher', targetNoteId: 'globalNoteMap', icon: 'bx bx-map-alt' },
+ { id: 'lbCalendar', title: 'Calendar', type: 'launcher', builtinWidget: 'calendar', icon: 'bx bx-calendar' },
+ { id: 'lbRecentChanges', title: 'Recent Changes', type: 'launcher', command: 'showRecentChanges', icon: 'bx bx-history', attributes: [
+ { type: 'label', name: 'desktopOnly' }
+ ] },
+ { id: 'lbSpacer1', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "50", growthFactor: "0" },
+ { id: 'lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
+ { id: 'lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
+ { id: 'lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
+ { id: 'lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' }
+ ]
+ }
+ ]
+ },
+ {
+ id: 'options',
+ title: 'Options',
+ type: 'book',
+ children: [
+ { id: 'optionsAppearance', title: 'Appearance', type: 'contentWidget', icon: 'bx-layout' },
+ { id: 'optionsShortcuts', title: 'Shortcuts', type: 'contentWidget', icon: 'bxs-keyboard' },
+ { id: 'optionsTextNotes', title: 'Text Notes', type: 'contentWidget', icon: 'bx-text' },
+ { id: 'optionsCodeNotes', title: 'Code Notes', type: 'contentWidget', icon: 'bx-code' },
+ { id: 'optionsImages', title: 'Images', type: 'contentWidget', icon: 'bx-image' },
+ { id: 'optionsSpellcheck', title: 'Spellcheck', type: 'contentWidget', icon: 'bx-check-double' },
+ { id: 'optionsPassword', title: 'Password', type: 'contentWidget', icon: 'bx-lock' },
+ { id: 'optionsEtapi', title: 'ETAPI', type: 'contentWidget', icon: 'bx-extension' },
+ { id: 'optionsBackup', title: 'Backup', type: 'contentWidget', icon: 'bx-data' },
+ { id: 'optionsSync', title: 'Sync', type: 'contentWidget', icon: 'bx-wifi' },
+ { id: 'optionsOther', title: 'Other', type: 'contentWidget', icon: 'bx-dots-horizontal' },
+ { id: 'optionsAdvanced', title: 'Advanced', type: 'contentWidget' }
+ ]
+ }
+ ]
+};
+
+function checkHiddenSubtree() {
+ checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION);
+}
+
+function checkHiddenSubtreeRecursively(parentNoteId, item) {
+ if (!item.id || !item.type || !item.title) {
+ throw new Error(`Item does not contain mandatory properties: ` + JSON.stringify(item));
+ }
+
+ let note = becca.notes[item.id];
+ let branch = becca.branches[item.id];
+
+ const attrs = [...(item.attributes || [])];
+
+ if (item.icon) {
+ attrs.push({ type: 'label', name: 'iconClass', value: 'bx ' + item.icon });
+ }
+
+ if (!note) {
+ ({note, branch} = noteService.createNewNote({
+ branchId: item.id,
+ noteId: item.id,
+ title: item.title,
+ type: item.type,
+ parentNoteId: parentNoteId,
+ content: '',
+ ignoreForbiddenParents: true
+ }));
+
+ if (item.type === 'launcher') {
+ if (item.command) {
+ attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
+ attrs.push({ type: 'label', name: 'command', value: item.command });
+ } else if (item.builtinWidget) {
+ if (item.builtinWidget === 'spacer') {
+ attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
+ attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
+ attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
+ } else {
+ attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
+ }
+
+ attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget });
+ } else if (item.targetNoteId) {
+ attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER });
+ attrs.push({ type: 'relation', name: 'targetNote', value: item.targetNoteId });
+ } else {
+ throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
+ }
+ }
+ }
+
+ if (note.type !== item.type) {
+ // enforce correct note type
+ note.type = item.type;
+ note.save();
+ }
+
+ if (!branch) {
+ // not sure if there's some better way to recover
+ log.error(`Cannot find branch id='${item.id}', ignoring...`);
+ } else {
+ if (item.notePosition !== undefined && branch.notePosition !== item.notePosition) {
+ branch.notePosition = item.notePosition;
+ branch.save();
+ }
+
+ if (item.isExpanded !== undefined && branch.isExpanded !== item.isExpanded) {
+ branch.isExpanded = item.isExpanded;
+ branch.save();
+ }
+ }
+
+ for (const attr of attrs) {
+ if (!note.hasAttribute(attr.type, attr.name)) {
+ note.addAttribute(attr.type, attr.name, attr.value);
+ }
+ }
+
+ for (const child of item.children || []) {
+ checkHiddenSubtreeRecursively(item.id, child);
+ }
+}
+
+module.exports = {
+ checkHiddenSubtree
+};
diff --git a/src/services/hoisted_note.js b/src/services/hoisted_note.js
new file mode 100644
index 000000000..9b4d2f023
--- /dev/null
+++ b/src/services/hoisted_note.js
@@ -0,0 +1,27 @@
+const cls = require("./cls");
+const becca = require("../becca/becca");
+
+function getHoistedNoteId() {
+ return cls.getHoistedNoteId();
+}
+
+function isHoistedInHiddenSubtree() {
+ const hoistedNoteId = getHoistedNoteId();
+
+ if (hoistedNoteId === 'root') {
+ return false;
+ }
+
+ const hoistedNote = becca.getNote(hoistedNoteId);
+
+ if (!hoistedNote) {
+ throw new Error(`Cannot find hoisted note ${hoistedNoteId}`);
+ }
+
+ return hoistedNote.hasAncestor('hidden');
+}
+
+module.exports = {
+ getHoistedNoteId,
+ isHoistedInHiddenSubtree
+};
diff --git a/src/services/import/zip.js b/src/services/import/zip.js
index e182126c1..a2f859a3f 100644
--- a/src/services/import/zip.js
+++ b/src/services/import/zip.js
@@ -187,7 +187,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
title: noteTitle,
content: '',
noteId: noteId,
- type: noteMeta ? noteMeta.type : 'text',
+ type: resolveNoteType(noteMeta.type),
mime: noteMeta ? noteMeta.mime : 'text/html',
prefix: noteMeta ? noteMeta.prefix : '',
isExpanded: noteMeta ? noteMeta.isExpanded : false,
@@ -258,12 +258,14 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
return;
}
- const {type, mime} = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath);
+ let {type, mime} = noteMeta ? noteMeta : detectFileTypeAndMime(taskContext, filePath);
if (type !== 'file' && type !== 'image') {
content = content.toString("UTF-8");
}
+ type = resolveNoteType(type);
+
if ((noteMeta && noteMeta.format === 'markdown')
|| (!noteMeta && taskContext.data.textImportedAsText && ['text/markdown', 'text/x-markdown'].includes(mime))) {
const parsed = mdReader.parse(content);
@@ -344,7 +346,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}
}
- if (type === 'relation-map' && noteMeta) {
+ if (type === 'relationMap' && noteMeta) {
const relationMapLinks = (noteMeta.attributes || [])
.filter(attr => attr.type === 'relation' && attr.name === 'relationMapLink');
@@ -531,6 +533,22 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
return firstNote;
}
+function resolveNoteType(type) {
+ type = type || 'text';
+
+ // BC for ZIPs created in Triliun 0.57 and older
+ if (type === 'relation-map') {
+ type = 'relationMap';
+ } else if (type === 'note-map') {
+ type = 'noteMap';
+ } else if (type === 'web-view') {
+ type = 'webView';
+ }
+
+ return type;
+}
+
+
module.exports = {
importZip
};
diff --git a/src/services/note_types.js b/src/services/note_types.js
index b85d90bc1..6e970f2d3 100644
--- a/src/services/note_types.js
+++ b/src/services/note_types.js
@@ -5,10 +5,13 @@ module.exports = [
'file',
'image',
'search',
- 'relation-map',
+ 'relationMap',
'book',
- 'note-map',
+ 'noteMap',
'mermaid',
'canvas',
- 'web-view'
+ 'webView',
+ 'launcher',
+ 'doc',
+ 'contentWidget'
];
diff --git a/src/services/notes.js b/src/services/notes.js
index cbc3952fb..aa7bc5c26 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -18,7 +18,8 @@ const Branch = require('../becca/entities/branch');
const Note = require('../becca/entities/note');
const Attribute = require('../becca/entities/attribute');
const dayjs = require("dayjs");
-const htmlSanitizer = require("./html_sanitizer.js");
+const htmlSanitizer = require("./html_sanitizer");
+const ValidationError = require("../errors/validation_error");
function getNewNotePosition(parentNoteId) {
const note = becca.notes[parentNoteId];
@@ -50,9 +51,9 @@ function deriveMime(type, mime) {
mime = 'text/html';
} else if (type === 'code' || type === 'mermaid') {
mime = 'text/plain';
- } else if (['relation-map', 'search', 'canvas'].includes(type)) {
+ } else if (['relationMap', 'search', 'canvas'].includes(type)) {
mime = 'application/json';
- } else if (['render', 'book', 'web-view'].includes(type)) {
+ } else if (['render', 'book', 'webView'].includes(type)) {
mime = '';
} else {
mime = 'application/octet-stream';
@@ -103,12 +104,30 @@ function getNewNoteTitle(parentNote) {
return title;
}
+function getAndValidateParent(params) {
+ const parentNote = becca.notes[params.parentNoteId];
+
+ if (!parentNote) {
+ throw new ValidationError(`Parent note "${params.parentNoteId}" not found.`);
+ }
+
+ if (parentNote.type === 'launcher' && parentNote.noteId !== 'lbBookmarks') {
+ throw new ValidationError(`Creating child notes into launcher notes is not allowed.`);
+ }
+
+ if (!params.ignoreForbiddenParents && (['lbRoot', 'hidden'].includes(parentNote.noteId) || parentNote.isOptions())) {
+ throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`);
+ }
+
+ return parentNote;
+}
+
/**
* Following object properties are mandatory:
* - {string} parentNoteId
* - {string} title
* - {*} content
- * - {string} type - text, code, file, image, search, book, relation-map, canvas, render
+ * - {string} type - text, code, file, image, search, book, relationMap, canvas, render
*
* Following are optional (have defaults)
* - {string} mime - value is derived from default mimes for type
@@ -121,11 +140,7 @@ function getNewNoteTitle(parentNote) {
* @return {{note: Note, branch: Branch}}
*/
function createNewNote(params) {
- const parentNote = becca.notes[params.parentNoteId];
-
- if (!parentNote) {
- throw new Error(`Parent note "${params.parentNoteId}" not found.`);
- }
+ const parentNote = getAndValidateParent(params);
if (params.title === null || params.title === undefined) {
params.title = getNewNoteTitle(parentNote);
@@ -282,7 +297,7 @@ function protectNote(note, protect) {
}
function findImageLinks(content, foundLinks) {
- const re = /src="[^"]*api\/images\/([a-zA-Z0-9]+)\//g;
+ const re = /src="[^"]*api\/images\/([a-zA-Z0-9_]+)\//g;
let match;
while (match = re.exec(content)) {
@@ -298,7 +313,7 @@ function findImageLinks(content, foundLinks) {
}
function findInternalLinks(content, foundLinks) {
- const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g;
+ const re = /href="[^"]*#root[a-zA-Z0-9_\/]*\/([a-zA-Z0-9_]+)\/?"/g;
let match;
while (match = re.exec(content)) {
@@ -313,7 +328,7 @@ function findInternalLinks(content, foundLinks) {
}
function findIncludeNoteLinks(content, foundLinks) {
- const re = /]*>/g;
+ const re = /]*>/g;
let match;
while (match = re.exec(content)) {
@@ -477,7 +492,7 @@ function downloadImages(noteId, content) {
}
function saveLinks(note, content) {
- if (note.type !== 'text' && note.type !== 'relation-map') {
+ if (note.type !== 'text' && note.type !== 'relationMap') {
return content;
}
@@ -494,7 +509,7 @@ function saveLinks(note, content) {
content = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
}
- else if (note.type === 'relation-map') {
+ else if (note.type === 'relationMap') {
findRelationMapLinks(content, foundLinks);
}
else {
@@ -665,7 +680,7 @@ function getUndeletedParentBranchIds(noteId, deleteId) {
}
function scanForLinks(note) {
- if (!note || !['text', 'relation-map'].includes(note.type)) {
+ if (!note || !['text', 'relationMap'].includes(note.type)) {
return;
}
@@ -855,7 +870,7 @@ function duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapp
let content = origNote.getContent();
- if (['text', 'relation-map', 'search'].includes(origNote.type)) {
+ if (['text', 'relationMap', 'search'].includes(origNote.type)) {
// fix links in the content
content = replaceByMap(content, noteIdMapping);
}
diff --git a/src/services/scheduler.js b/src/services/scheduler.js
index 3f253ba9b..70dc23589 100644
--- a/src/services/scheduler.js
+++ b/src/services/scheduler.js
@@ -5,8 +5,8 @@ const config = require('./config');
const log = require('./log');
const sql = require("./sql");
const becca = require("../becca/becca");
-const specialNotesService = require("../services/special_notes");
const protectedSessionService = require("../services/protected_session");
+const hiddenSubtreeService = require("./hidden_subtree");
function getRunAtHours(note) {
try {
@@ -52,13 +52,13 @@ function runNotesWithLabel(runAttrValue) {
sqlInit.dbReady.then(() => {
if (!process.env.TRILIUM_SAFE_MODE) {
+ cls.init(() => hiddenSubtreeService.checkHiddenSubtree());
+
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
-
- setTimeout(cls.wrap(() => specialNotesService.createMissingSpecialNotes()), 10 * 1000);
}
setInterval(() => protectedSessionService.checkProtectedSessionExpiration(), 30000);
diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js
index 569f773b8..c71addb17 100644
--- a/src/services/search/search_context.js
+++ b/src/services/search/search_context.js
@@ -1,6 +1,6 @@
"use strict";
-const cls = require('../cls');
+const hoistedNoteService = require("../hoisted_note");
class SearchContext {
constructor(params = {}) {
@@ -9,8 +9,10 @@ class SearchContext {
this.ignoreHoistedNote = !!params.ignoreHoistedNote;
this.ancestorNoteId = params.ancestorNoteId;
- if (!this.ancestorNoteId && !this.ignoreHoistedNote) {
- this.ancestorNoteId = cls.getHoistedNoteId();
+ if (!this.ancestorNoteId && !this.ignoreHoistedNote && !hoistedNoteService.isHoistedInHiddenSubtree()) {
+ // hoisting in hidden subtree should not limit autocomplete
+ // since we want to link (create relations) to the normal non-hidden notes
+ this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
}
this.ancestorDepth = params.ancestorDepth;
diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js
index ad88449e2..2f72e0b1b 100644
--- a/src/services/search/services/parse.js
+++ b/src/services/search/services/parse.js
@@ -12,7 +12,7 @@ const PropertyComparisonExp = require('../expressions/property_comparison');
const AttributeExistsExp = require('../expressions/attribute_exists');
const LabelComparisonExp = require('../expressions/label_comparison');
const NoteFlatTextExp = require('../expressions/note_flat_text');
-const NoteContentFulltextExp = require('../expressions/note_content_fulltext.js');
+const NoteContentFulltextExp = require('../expressions/note_content_fulltext');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit');
const AncestorExp = require("../expressions/ancestor");
const buildComparator = require('./build_comparator');
diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js
index 42897ebc5..dee2816c6 100644
--- a/src/services/search/services/search.js
+++ b/src/services/search/services/search.js
@@ -10,7 +10,7 @@ const becca = require('../../../becca/becca');
const beccaService = require('../../../becca/becca_service');
const utils = require('../../utils');
const log = require('../../log');
-const scriptService = require("../../script.js");
+const scriptService = require("../../script");
function searchFromNote(note) {
let searchResultNoteIds, highlightedTokens;
diff --git a/src/services/special_notes.js b/src/services/special_notes.js
index ebe447d98..a8785492f 100644
--- a/src/services/special_notes.js
+++ b/src/services/special_notes.js
@@ -4,6 +4,8 @@ const becca = require("../becca/becca");
const noteService = require("./notes");
const cls = require("./cls");
const dateUtils = require("./date_utils");
+const log = require("./log");
+const hiddenSubtreeService = require("./hidden_subtree");
function getInboxNote(date) {
const hoistedNote = getHoistedNote();
@@ -29,97 +31,9 @@ function getInboxNote(date) {
return inbox;
}
-function getHiddenRoot() {
- let hidden = becca.getNote('hidden');
-
- if (!hidden) {
- hidden = noteService.createNewNote({
- branchId: 'hidden',
- noteId: 'hidden',
- title: 'hidden',
- type: 'text',
- content: '',
- parentNoteId: 'root'
- }).note;
-
- // isInheritable: false means that this notePath is automatically not preffered but at the same time
- // the flag is not inherited to the children
- hidden.addLabel('archived', "", false);
- hidden.addLabel('excludeFromNoteMap', "", true);
- }
-
- return hidden;
-}
-
-function getSearchRoot() {
- let searchRoot = becca.getNote('search');
-
- if (!searchRoot) {
- searchRoot = noteService.createNewNote({
- noteId: 'search',
- title: 'search',
- type: 'text',
- content: '',
- parentNoteId: getHiddenRoot().noteId
- }).note;
- }
-
- return searchRoot;
-}
-
-function getSinglesNoteRoot() {
- let singlesNoteRoot = becca.getNote('singles');
-
- if (!singlesNoteRoot) {
- singlesNoteRoot = noteService.createNewNote({
- noteId: 'singles',
- title: 'singles',
- type: 'text',
- content: '',
- parentNoteId: getHiddenRoot().noteId
- }).note;
- }
-
- return singlesNoteRoot;
-}
-
-function getGlobalNoteMap() {
- let globalNoteMap = becca.getNote('globalnotemap');
-
- if (!globalNoteMap) {
- globalNoteMap = noteService.createNewNote({
- noteId: 'globalnotemap',
- title: 'Global Note Map',
- type: 'note-map',
- content: '',
- parentNoteId: getSinglesNoteRoot().noteId
- }).note;
-
- globalNoteMap.addLabel('mapRootNoteId', 'hoisted');
- }
-
- return globalNoteMap;
-}
-
-function getSqlConsoleRoot() {
- let sqlConsoleRoot = becca.getNote('sqlconsole');
-
- if (!sqlConsoleRoot) {
- sqlConsoleRoot = noteService.createNewNote({
- noteId: 'sqlconsole',
- title: 'SQL Console',
- type: 'text',
- content: '',
- parentNoteId: getHiddenRoot().noteId
- }).note;
- }
-
- return sqlConsoleRoot;
-}
-
function createSqlConsole() {
const {note} = noteService.createNewNote({
- parentNoteId: getSqlConsoleRoot().noteId,
+ parentNoteId: 'sqlConsole',
title: 'SQL Console',
content: "SELECT title, isDeleted, isProtected FROM notes WHERE noteId = ''\n\n\n\n",
type: 'code',
@@ -127,6 +41,8 @@ function createSqlConsole() {
});
note.setLabel("sqlConsole", dateUtils.localNowDate());
+ note.setLabel('iconClass', 'bx bx-data');
+ note.setLabel('keepCurrentHoisting');
return note;
}
@@ -152,7 +68,7 @@ function saveSqlConsole(sqlConsoleNoteId) {
function createSearchNote(searchString, ancestorNoteId) {
const {note} = noteService.createNewNote({
- parentNoteId: getSearchRoot().noteId,
+ parentNoteId: 'search',
title: 'Search: ' + searchString,
content: "",
type: 'search',
@@ -160,6 +76,7 @@ function createSearchNote(searchString, ancestorNoteId) {
});
note.setLabel('searchString', searchString);
+ note.setLabel('keepCurrentHoisting');
if (ancestorNoteId) {
note.setRelation('ancestor', ancestorNoteId);
@@ -202,52 +119,76 @@ function getHoistedNote() {
return becca.getNote(cls.getHoistedNoteId());
}
-function getShareRoot() {
- let shareRoot = becca.getNote('share');
+function createLauncher(parentNoteId, launcherType) {
+ let note;
- if (!shareRoot) {
- shareRoot = noteService.createNewNote({
- branchId: 'share',
- noteId: 'share',
- title: 'Shared notes',
- type: 'text',
+ if (launcherType === 'note') {
+ note = noteService.createNewNote({
+ title: "Note Launcher",
+ type: 'launcher',
content: '',
- parentNoteId: 'root'
+ parentNoteId: parentNoteId
}).note;
+
+ note.addRelation('template', 'lbTplNoteLauncher');
+ } else if (launcherType === 'script') {
+ note = noteService.createNewNote({
+ title: "Script Launcher",
+ type: 'launcher',
+ content: '',
+ parentNoteId: parentNoteId
+ }).note;
+
+ note.addRelation('template', 'lbTplScriptLauncher');
+ } else if (launcherType === 'customWidget') {
+ note = noteService.createNewNote({
+ title: "Widget Launcher",
+ type: 'launcher',
+ content: '',
+ parentNoteId: parentNoteId
+ }).note;
+
+ note.addRelation('template', 'lbTplCustomWidget');
+ } else if (launcherType === 'spacer') {
+ note = noteService.createNewNote({
+ title: "Spacer",
+ type: 'launcher',
+ content: '',
+ parentNoteId: parentNoteId
+ }).note;
+
+ note.addRelation('template', 'lbTplSpacer');
+ } else {
+ throw new Error(`Unrecognized launcher type '${launcherType}'`);
}
- return shareRoot;
+ return {
+ success: true,
+ note
+ };
}
-function getBulkActionNote() {
- let bulkActionNote = becca.getNote('bulkaction');
+function resetLauncher(noteId) {
+ const note = becca.getNote(noteId);
- if (!bulkActionNote) {
- bulkActionNote = noteService.createNewNote({
- branchId: 'bulkaction',
- noteId: 'bulkaction',
- title: 'Bulk action',
- type: 'text',
- content: '',
- parentNoteId: getHiddenRoot().noteId
- }).note;
+ if (note.isLauncherConfig()) {
+ if (note) {
+ if (noteId === 'lbRoot') {
+ // deleting hoisted notes are not allowed, so we just reset the children
+ for (const childNote of note.getChildNotes()) {
+ childNote.deleteNote();
+ }
+ } else {
+ note.deleteNote();
+ }
+ } else {
+ log.info(`Note ${noteId} has not been found and cannot be reset.`);
+ }
+ } else {
+ log.info(`Note ${noteId} is not a resettable launcher note.`);
}
- return bulkActionNote;
-}
-
-function createMissingSpecialNotes() {
- getSinglesNoteRoot();
- getSqlConsoleRoot();
- getGlobalNoteMap();
- getBulkActionNote();
- // share root is not automatically created since it's visible in the tree and many won't need it/use it
-
- const hidden = getHiddenRoot();
-
- if (!hidden.hasOwnedLabel('excludeFromNoteMap')) {
- hidden.addLabel('excludeFromNoteMap', "", true);
- }
+ hiddenSubtreeService.checkHiddenSubtree();
}
module.exports = {
@@ -256,7 +197,6 @@ module.exports = {
saveSqlConsole,
createSearchNote,
saveSearchNote,
- createMissingSpecialNotes,
- getShareRoot,
- getBulkActionNote,
+ createLauncher,
+ resetLauncher
};
diff --git a/src/services/task_context.js b/src/services/task_context.js
index d59bc96b3..f86b6fd59 100644
--- a/src/services/task_context.js
+++ b/src/services/task_context.js
@@ -6,7 +6,7 @@ const ws = require('./ws');
const taskContexts = {};
class TaskContext {
- constructor(taskId, taskType, data) {
+ constructor(taskId, taskType, data = null) {
this.taskId = taskId;
this.taskType = taskType;
this.data = data;
@@ -24,7 +24,7 @@ class TaskContext {
}
/** @returns {TaskContext} */
- static getInstance(taskId, taskType, data) {
+ static getInstance(taskId, taskType, data = null) {
if (!taskContexts[taskId]) {
taskContexts[taskId] = new TaskContext(taskId, taskType, data);
}
diff --git a/src/services/tree.js b/src/services/tree.js
index d6e96b861..13dfd5058 100644
--- a/src/services/tree.js
+++ b/src/services/tree.js
@@ -30,13 +30,13 @@ function getNotes(noteIds) {
}
function validateParentChild(parentNoteId, childNoteId, branchId = null) {
- if (childNoteId === 'root') {
- return { success: false, message: 'Cannot move root note.'};
+ if (['root', 'hidden', 'share', 'lbRoot', 'lbAvailableLaunchers', 'lbVisibleLaunchers'].includes(childNoteId)) {
+ return { success: false, message: `Cannot change this note's location.`};
}
if (parentNoteId === 'none') {
// this shouldn't happen
- return { success: false, message: 'Cannot move anything into root parent.' };
+ return { success: false, message: `Cannot move anything into 'none' parent.` };
}
const existing = getExistingBranch(parentNoteId, childNoteId);
@@ -58,6 +58,13 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) {
};
}
+ if (parentNoteId !== 'lbBookmarks' && becca.getNote(parentNoteId).type === 'launcher') {
+ return {
+ success: false,
+ message: 'Launcher note cannot have any children.'
+ };
+ }
+
return { success: true };
}
@@ -180,6 +187,10 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder
for (const note of notes) {
const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId);
+ if (branch.branchId === 'hidden') {
+ position = 999_999_999;
+ }
+
if (branch.notePosition !== position) {
sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
[position, branch.branchId]);
diff --git a/src/services/utils.js b/src/services/utils.js
index 0a3cffa86..af0e66f8e 100644
--- a/src/services/utils.js
+++ b/src/services/utils.js
@@ -168,7 +168,7 @@ const STRING_MIME_TYPES = [
function isStringNote(type, mime) {
// render and book are string note in the sense that they are expected to contain empty string
- return ["text", "code", "relation-map", "search", "render", "book", "mermaid", "canvas"].includes(type)
+ return ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)
|| mime.startsWith('text/')
|| STRING_MIME_TYPES.includes(mime);
}
@@ -192,7 +192,7 @@ function formatDownloadTitle(filename, type, mime) {
if (type === 'text') {
return filename + '.html';
- } else if (['relation-map', 'canvas', 'search'].includes(type)) {
+ } else if (['relationMap', 'canvas', 'search'].includes(type)) {
return filename + '.json';
} else {
if (!mime) {
diff --git a/src/views/mobile.ejs b/src/views/mobile.ejs
index c0b3569e1..c6ad8f887 100644
--- a/src/views/mobile.ejs
+++ b/src/views/mobile.ejs
@@ -116,7 +116,8 @@
isDev: <%= isDev %>,
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
- assetPath: "<%= assetPath %>"
+ assetPath: "<%= assetPath %>",
+ isMainWindow: true
};