diff --git a/package-lock.json b/package-lock.json
index 69ee337f2..d1b5a9a5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "trilium",
- "version": "0.41.5",
+ "version": "0.41.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -2832,9 +2832,9 @@
"integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI="
},
"dayjs": {
- "version": "1.8.25",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.25.tgz",
- "integrity": "sha512-Pk36juDfQQGDCgr0Lqd1kw15w3OS6xt21JaLPE3lCfsEf8KrERGwDNwvK1tRjrjqFC0uZBJncT4smZQ4F+uV5g=="
+ "version": "1.8.26",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.26.tgz",
+ "integrity": "sha512-KqtAuIfdNfZR5sJY1Dixr2Is4ZvcCqhb0dZpCOt5dGEFiMzoIbjkTSzUb4QKTCsP+WNpGwUjAFIZrnZvUxxkhw=="
},
"debug": {
"version": "4.1.1",
diff --git a/package.json b/package.json
index 79338af86..4d96ee213 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"commonmark": "0.29.1",
"cookie-parser": "1.4.5",
"csurf": "1.11.0",
- "dayjs": "1.8.25",
+ "dayjs": "1.8.26",
"debug": "4.1.1",
"ejs": "3.1.2",
"electron-debug": "3.0.1",
diff --git a/src/public/app/layouts/desktop_main_window_layout.js b/src/public/app/layouts/desktop_main_window_layout.js
index 9330c2792..bf0e78b37 100644
--- a/src/public/app/layouts/desktop_main_window_layout.js
+++ b/src/public/app/layouts/desktop_main_window_layout.js
@@ -103,7 +103,7 @@ export default class DesktopMainWindowLayout {
}
getRootWidget(appContext) {
- appContext.mainTreeWidget = new NoteTreeWidget();
+ appContext.mainTreeWidget = new NoteTreeWidget("main");
return new FlexContainer('column')
.setParent(appContext)
diff --git a/src/public/app/layouts/mobile_layout.js b/src/public/app/layouts/mobile_layout.js
index 84f7d7b73..a1eb75cb9 100644
--- a/src/public/app/layouts/mobile_layout.js
+++ b/src/public/app/layouts/mobile_layout.js
@@ -73,7 +73,7 @@ export default class MobileLayout {
.child(new ScreenContainer("tree", 'column')
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-4 col-xl-4")
.child(new MobileGlobalButtonsWidget())
- .child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
+ .child(new NoteTreeWidget("main").cssBlock(FANCYTREE_CSS)))
.child(new ScreenContainer("detail", "column")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-8")
.child(new FlexContainer('row')
diff --git a/src/public/app/services/tree_builder.js b/src/public/app/services/tree_builder.js
deleted file mode 100644
index d6eb09fc5..000000000
--- a/src/public/app/services/tree_builder.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import utils from "./utils.js";
-import treeCache from "./tree_cache.js";
-import ws from "./ws.js";
-import hoistedNoteService from "./hoisted_note.js";
-
-async function prepareRootNode() {
- await treeCache.initializedPromise;
-
- const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
-
- let hoistedBranch;
-
- if (hoistedNoteId === 'root') {
- hoistedBranch = treeCache.getBranch('root');
- }
- else {
- const hoistedNote = await treeCache.getNote(hoistedNoteId);
- hoistedBranch = (await hoistedNote.getBranches())[0];
- }
-
- return await prepareNode(hoistedBranch);
-}
-
-async function prepareChildren(note) {
- if (note.type === 'search') {
- return await prepareSearchNoteChildren(note);
- }
- else {
- return await prepareNormalNoteChildren(note);
- }
-}
-
-const NOTE_TYPE_ICONS = {
- "file": "bx bx-file",
- "image": "bx bx-image",
- "code": "bx bx-code",
- "render": "bx bx-extension",
- "search": "bx bx-file-find",
- "relation-map": "bx bx-map-alt",
- "book": "bx bx-book"
-};
-
-function getIconClass(note) {
- const labels = note.getLabels('iconClass');
-
- return labels.map(l => l.value).join(' ');
-}
-
-function getIcon(note) {
- const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
-
- const iconClass = getIconClass(note);
-
- if (iconClass) {
- return iconClass;
- }
- else if (note.noteId === 'root') {
- return "bx bx-chevrons-right";
- }
- else if (note.noteId === hoistedNoteId) {
- return "bx bxs-arrow-from-bottom";
- }
- else if (note.type === 'text') {
- if (note.hasChildren()) {
- return "bx bx-folder";
- }
- else {
- return "bx bx-note";
- }
- }
- else {
- return NOTE_TYPE_ICONS[note.type];
- }
-}
-
-async function prepareNode(branch) {
- const note = await branch.getNote();
-
- if (!note) {
- throw new Error(`Branch has no note ` + branch.noteId);
- }
-
- const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
- const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
-
- const node = {
- noteId: note.noteId,
- parentNoteId: branch.parentNoteId,
- branchId: branch.branchId,
- isProtected: note.isProtected,
- noteType: note.type,
- title: utils.escapeHtml(title),
- extraClasses: getExtraClasses(note),
- icon: getIcon(note),
- refKey: note.noteId,
- expanded: branch.isExpanded || hoistedNoteId === note.noteId,
- key: utils.randomString(12) // this should prevent some "duplicate key" errors
- };
-
- const childBranches = getChildBranchesWithoutImages(note);
-
- node.folder = childBranches.length > 0
- || note.type === 'search'
-
- node.lazy = node.folder && !node.expanded;
-
- if (node.folder && node.expanded) {
- node.children = await prepareChildren(note);
- }
-
- return node;
-}
-
-async function prepareNormalNoteChildren(parentNote) {
- utils.assertArguments(parentNote);
-
- const noteList = [];
-
- for (const branch of getChildBranchesWithoutImages(parentNote)) {
- const node = await prepareNode(branch);
-
- noteList.push(node);
- }
-
- return noteList;
-}
-
-function getChildBranchesWithoutImages(parentNote) {
- const childBranches = parentNote.getChildBranches();
-
- if (!childBranches) {
- ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
- return;
- }
-
- const imageLinks = parentNote.getRelations('imageLink');
-
- // image is already visible in the parent note so no need to display it separately in the book
- return childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
-}
-
-async function prepareSearchNoteChildren(note) {
- await treeCache.reloadNotes([note.noteId]);
-
- const newNote = await treeCache.getNote(note.noteId);
-
- return await prepareNormalNoteChildren(newNote);
-}
-
-function getExtraClasses(note) {
- utils.assertArguments(note);
-
- const extraClasses = [];
-
- if (note.isProtected) {
- extraClasses.push("protected");
- }
-
- if (note.getParentNoteIds().length > 1) {
- extraClasses.push("multiple-parents");
- }
-
- const cssClass = note.getCssClass();
-
- if (cssClass) {
- extraClasses.push(cssClass);
- }
-
- extraClasses.push(utils.getNoteTypeClass(note.type));
-
- if (note.mime) { // some notes should not have mime type (e.g. render)
- extraClasses.push(utils.getMimeTypeClass(note.mime));
- }
-
- if (note.hasLabel('archived')) {
- extraClasses.push("archived");
- }
-
- return extraClasses.join(" ");
-}
-
-export default {
- prepareRootNode,
- prepareBranch: prepareChildren,
- getExtraClasses,
- getIcon,
- getChildBranchesWithoutImages
-}
\ No newline at end of file
diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js
index 278ad98a4..a0b7ba235 100644
--- a/src/public/app/widgets/note_tree.js
+++ b/src/public/app/widgets/note_tree.js
@@ -3,7 +3,6 @@ import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import contextMenu from "../services/context_menu.js";
import treeCache from "../services/tree_cache.js";
-import treeBuilder from "../services/tree_builder.js";
import branchService from "../services/branches.js";
import ws from "../services/ws.js";
import TabAwareWidget from "./tab_aware_widget.js";
@@ -15,17 +14,24 @@ import keyboardActionsService from "../services/keyboard_actions.js";
import clipboard from "../services/clipboard.js";
import protectedSessionService from "../services/protected_session.js";
import syncService from "../services/sync.js";
+import options from "../services/options.js";
const TPL = `
-
+
`;
+const NOTE_TYPE_ICONS = {
+ "file": "bx bx-file",
+ "image": "bx bx-image",
+ "code": "bx bx-code",
+ "render": "bx bx-extension",
+ "search": "bx bx-file-find",
+ "relation-map": "bx bx-map-alt",
+ "book": "bx bx-book"
+};
+
export default class NoteTreeWidget extends TabAwareWidget {
+ constructor(treeName) {
+ super();
+
+ this.treeName = treeName;
+ }
+
doRender() {
this.$widget = $(TPL);
+ this.$tree = this.$widget.find('.tree');
- this.$widget.on("click", ".unhoist-button", hoistedNoteService.unhoist);
- this.$widget.on("click", ".refresh-search-button", () => this.refreshSearch());
+ this.$tree.on("click", ".unhoist-button", hoistedNoteService.unhoist);
+ this.$tree.on("click", ".refresh-search-button", () => this.refreshSearch());
// fancytree doesn't support middle click so this is a way to support it
- this.$widget.on('mousedown', '.fancytree-title', e => {
+ this.$tree.on('mousedown', '.fancytree-title', e => {
if (e.which === 2) {
const node = $.ui.fancytree.getNode(e);
@@ -67,20 +133,76 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
});
+ this.$treeSettingsPopup = this.$widget.find('.tree-settings-popup');
+ this.$hideArchivedNotesCheckbox = this.$treeSettingsPopup.find('.hide-archived-notes');
+ this.$hideIncludedImages = this.$treeSettingsPopup.find('.hide-included-images');
+
+ this.$treeSettingsButton = this.$widget.find('.tree-settings-button');
+ this.$treeSettingsButton.on("click", e => {
+ if (this.$treeSettingsPopup.is(":visible")) {
+ this.$treeSettingsPopup.hide();
+ return;
+ }
+
+ this.$hideArchivedNotesCheckbox.prop("checked", this.hideArchivedNotes);
+ this.$hideIncludedImages.prop("checked", this.hideIncludedImages);
+
+ let top = this.$treeSettingsButton[0].offsetTop;
+ let left = this.$treeSettingsButton[0].offsetLeft;
+ top += this.$treeSettingsButton.outerHeight();
+ left += this.$treeSettingsButton.outerWidth() - this.$treeSettingsPopup.outerWidth();
+
+ if (left < 0) {
+ left = 0;
+ }
+
+ this.$treeSettingsPopup.css({
+ display: "block",
+ top: top,
+ left: left
+ }).addClass("show");
+ });
+
+ this.$saveTreeSettingsButton = this.$treeSettingsPopup.find('.save-tree-settings-button');
+ this.$saveTreeSettingsButton.on('click', async () => {
+ await this.setHideArchivedNotes(this.$hideArchivedNotesCheckbox.prop("checked"));
+ await this.setHideIncludedImages(this.$hideIncludedImages.prop("checked"));
+
+ this.$treeSettingsPopup.hide();
+
+ this.reloadTreeFromCache();
+ });
+
this.initialized = this.initFancyTree();
return this.$widget;
}
- async initFancyTree() {
- const treeData = [await treeBuilder.prepareRootNode()];
+ get hideArchivedNotes() {
+ return options.is("hideArchivedNotes_" + this.treeName);
+ }
- this.$widget.fancytree({
+ async setHideArchivedNotes(val) {
+ await options.save("hideArchivedNotes_" + this.treeName, val.toString());
+ }
+
+ get hideIncludedImages() {
+ return options.is("hideIncludedImages_" + this.treeName);
+ }
+
+ async setHideIncludedImages(val) {
+ await options.save("hideIncludedImages_" + this.treeName, val.toString());
+ }
+
+ async initFancyTree() {
+ const treeData = [await this.prepareRootNode()];
+
+ this.$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: utils.isMobile() ? ["dnd5", "clones"] : ["hotkeys", "dnd5", "clones"],
source: treeData,
- scrollParent: this.$widget,
+ scrollParent: this.$tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
const targetType = data.targetType;
@@ -191,10 +313,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
}
},
- lazyLoad: function(event, data) {
+ lazyLoad: (event, data) => {
const noteId = data.node.data.noteId;
- data.result = treeCache.getNote(noteId).then(note => treeBuilder.prepareBranch(note));
+ data.result = treeCache.getNote(noteId).then(note => this.prepareChildren(note));
},
clones: {
highlightActiveClones: true
@@ -236,7 +358,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
});
- this.$widget.on('contextmenu', '.fancytree-node', e => {
+ this.$tree.on('contextmenu', '.fancytree-node', e => {
const node = $.ui.fancytree.getNode(e);
import("../services/tree_context_menu.js").then(({default: TreeContextMenu}) => {
@@ -247,7 +369,191 @@ export default class NoteTreeWidget extends TabAwareWidget {
return false; // blocks default browser right click menu
});
- this.tree = $.ui.fancytree.getTree(this.$widget);
+ this.tree = $.ui.fancytree.getTree(this.$tree);
+ }
+
+ async prepareRootNode() {
+ await treeCache.initializedPromise;
+
+ const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
+
+ let hoistedBranch;
+
+ if (hoistedNoteId === 'root') {
+ hoistedBranch = treeCache.getBranch('root');
+ }
+ else {
+ const hoistedNote = await treeCache.getNote(hoistedNoteId);
+ hoistedBranch = (await hoistedNote.getBranches())[0];
+ }
+
+ return await this.prepareNode(hoistedBranch);
+ }
+
+ async prepareChildren(note) {
+ if (note.type === 'search') {
+ return await this.prepareSearchNoteChildren(note);
+ }
+ else {
+ return await this.prepareNormalNoteChildren(note);
+ }
+ }
+
+ getIconClass(note) {
+ const labels = note.getLabels('iconClass');
+
+ return labels.map(l => l.value).join(' ');
+ }
+
+ getIcon(note) {
+ const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
+
+ const iconClass = this.getIconClass(note);
+
+ if (iconClass) {
+ return iconClass;
+ }
+ else if (note.noteId === 'root') {
+ return "bx bx-chevrons-right";
+ }
+ else if (note.noteId === hoistedNoteId) {
+ return "bx bxs-arrow-from-bottom";
+ }
+ else if (note.type === 'text') {
+ if (note.hasChildren()) {
+ return "bx bx-folder";
+ }
+ else {
+ return "bx bx-note";
+ }
+ }
+ else {
+ return NOTE_TYPE_ICONS[note.type];
+ }
+ }
+
+ async prepareNode(branch) {
+ const note = await branch.getNote();
+
+ if (!note) {
+ throw new Error(`Branch has no note ` + branch.noteId);
+ }
+
+ const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
+ const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
+
+ const node = {
+ noteId: note.noteId,
+ parentNoteId: branch.parentNoteId,
+ branchId: branch.branchId,
+ isProtected: note.isProtected,
+ noteType: note.type,
+ title: utils.escapeHtml(title),
+ extraClasses: this.getExtraClasses(note),
+ icon: this.getIcon(note),
+ refKey: note.noteId,
+ expanded: branch.isExpanded || hoistedNoteId === note.noteId,
+ key: utils.randomString(12) // this should prevent some "duplicate key" errors
+ };
+
+ const childBranches = await this.getChildBranches(note);
+
+ node.folder = childBranches.length > 0
+ || note.type === 'search'
+
+ node.lazy = node.folder && !node.expanded;
+
+ if (node.folder && node.expanded) {
+ node.children = await this.prepareChildren(note);
+ }
+
+ return node;
+ }
+
+ async prepareNormalNoteChildren(parentNote) {
+ utils.assertArguments(parentNote);
+
+ const noteList = [];
+
+ for (const branch of await this.getChildBranches(parentNote)) {
+ const node = await this.prepareNode(branch);
+
+ noteList.push(node);
+ }
+
+ return noteList;
+ }
+
+ async getChildBranches(parentNote) {
+ let childBranches = parentNote.getChildBranches();
+
+ if (!childBranches) {
+ ws.logError(`No children for ${parentNote}. This shouldn't happen.`);
+ return;
+ }
+
+ if (this.hideIncludedImages) {
+ const imageLinks = parentNote.getRelations('imageLink');
+
+ // image is already visible in the parent note so no need to display it separately in the book
+ childBranches = childBranches.filter(branch => !imageLinks.find(rel => rel.value === branch.noteId));
+ }
+
+ if (this.hideArchivedNotes) {
+ const filteredBranches = [];
+
+ for (const childBranch of childBranches) {
+ const childNote = await childBranch.getNote();
+
+ if (!childNote.hasLabel('archived')) {
+ filteredBranches.push(childBranch);
+ }
+ }
+
+ childBranches = filteredBranches;
+ }
+
+ return childBranches;
+ }
+
+ async prepareSearchNoteChildren(note) {
+ await treeCache.reloadNotes([note.noteId]);
+
+ const newNote = await treeCache.getNote(note.noteId);
+
+ return await this.prepareNormalNoteChildren(newNote);
+ }
+
+ getExtraClasses(note) {
+ utils.assertArguments(note);
+
+ const extraClasses = [];
+
+ if (note.isProtected) {
+ extraClasses.push("protected");
+ }
+
+ if (note.getParentNoteIds().length > 1) {
+ extraClasses.push("multiple-parents");
+ }
+
+ const cssClass = note.getCssClass();
+
+ if (cssClass) {
+ extraClasses.push(cssClass);
+ }
+
+ extraClasses.push(utils.getNoteTypeClass(note.type));
+
+ if (note.mime) { // some notes should not have mime type (e.g. render)
+ extraClasses.push(utils.getMimeTypeClass(note.mime));
+ }
+
+ if (note.hasLabel('archived')) {
+ extraClasses.push("archived");
+ }
+
+ return extraClasses.join(" ");
}
/** @return {FancytreeNode[]} */
@@ -374,6 +680,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
let foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) { // note might be recently created so we'll force reload and try again
+ parentNode.lazy = true;
await parentNode.load(true);
foundChildNode = this.findChildNode(parentNode, childNoteId);
@@ -410,16 +717,16 @@ export default class NoteTreeWidget extends TabAwareWidget {
return this.getNodeFromPath(notePath, true, expandOpts);
}
- updateNode(node) {
+ async updateNode(node) {
const note = treeCache.getNoteFromCache(node.data.noteId);
const branch = treeCache.getBranch(node.data.branchId);
node.data.isProtected = note.isProtected;
node.data.noteType = note.type;
- node.folder = treeBuilder.getChildBranchesWithoutImages(note).length > 0
+ node.folder = (await this.getChildBranches(note)).length > 0
|| note.type === 'search';
- node.icon = treeBuilder.getIcon(note);
- node.extraClasses = treeBuilder.getExtraClasses(note);
+ node.icon = this.getIcon(note);
+ node.extraClasses = this.getExtraClasses(note);
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
node.renderTitle();
}
@@ -476,6 +783,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
async refreshSearch() {
const activeNode = this.getActiveNode();
+ activeNode.lazy = true;
activeNode.load(true);
activeNode.setExpanded(true);
@@ -569,6 +877,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
for (const noteId of noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
+ node.lazy = true;
await node.load(true);
this.updateNode(node);
@@ -631,7 +940,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const activeNotePath = activeNode !== null ? treeService.getNotePath(activeNode) : null;
- const rootNode = await treeBuilder.prepareRootNode();
+ const rootNode = await this.prepareRootNode();
await this.batchUpdate(async () => {
await this.tree.reload([rootNode]);
diff --git a/src/routes/api/options.js b/src/routes/api/options.js
index 33ea40052..fbb94f58e 100644
--- a/src/routes/api/options.js
+++ b/src/routes/api/options.js
@@ -110,7 +110,9 @@ async function getUserThemes() {
function isAllowed(name) {
return ALLOWED_OPTIONS.has(name)
|| name.startsWith("keyboardShortcuts")
- || name.endsWith("Collapsed");
+ || name.endsWith("Collapsed")
+ || name.startsWith("hideArchivedNotes")
+ || name.startsWith("hideIncludedImages");
}
module.exports = {
diff --git a/src/services/options_init.js b/src/services/options_init.js
index 4f1ab32fa..43d8060d9 100644
--- a/src/services/options_init.js
+++ b/src/services/options_init.js
@@ -82,7 +82,9 @@ const defaultOptions = [
{ name: 'rightPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false },
- { name: 'eraseNotesAfterTimeInSeconds', value: '604800', isSynced: true } // default is 7 days
+ { name: 'eraseNotesAfterTimeInSeconds', value: '604800', isSynced: true }, // default is 7 days
+ { name: 'hideArchivedNotes_main', value: 'false', isSynced: false }, // default is 7 days
+ { name: 'hideIncludedImages_main', value: 'true', isSynced: false } // default is 7 days
];
async function initStartupOptions() {
diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs
index e56e4ad0d..dd387edf2 100644
--- a/src/views/desktop.ejs
+++ b/src/views/desktop.ejs
@@ -64,7 +64,7 @@
-
+