diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
index cd4172576..a9dfc5250 100644
--- a/.idea/dataSources.xml
+++ b/.idea/dataSources.xml
@@ -5,7 +5,7 @@
sqlite.xerial
true
org.sqlite.JDBC
- jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db
+ jdbc:sqlite:$USER_HOME$/trilium-data/document.db
sqlite.xerial
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 146ab09b7..84795bdad 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,6 +1,7 @@
+
diff --git a/package-lock.json b/package-lock.json
index d1b5a9a5e..56d4c5c00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "trilium",
- "version": "0.41.6",
+ "version": "0.42.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3345,9 +3345,9 @@
}
},
"electron": {
- "version": "9.0.0-beta.21",
- "resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.21.tgz",
- "integrity": "sha512-xFOD8I4RB9IkpVKnzoHwHvDNGvGl1IinpYTyQ7o7FAgSnkvP/upI1JtzE5Ff6PlAdyIGnbC+Rz1hJIfmAXxVuQ==",
+ "version": "9.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-9.0.0-beta.22.tgz",
+ "integrity": "sha512-dfqAf+CXXTKcNDj7DU7mYsmx+oZQcXOvJnZ8ZsgAHjrE9Tv8zsYUgCP3JlO4Z8CIazgleKXYmgh6H2stdK7fEA==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
@@ -3785,9 +3785,9 @@
"dev": true
},
"mime": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
- "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.5.tgz",
+ "integrity": "sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w==",
"dev": true
},
"supports-color": {
@@ -4448,9 +4448,9 @@
}
},
"file-type": {
- "version": "14.2.0",
- "resolved": "https://registry.npmjs.org/file-type/-/file-type-14.2.0.tgz",
- "integrity": "sha512-CAkX5G5jq8LIgFu++dpM3giMZadYdU+QVQoPLajjNboo8IzaR4cKpBCVEuz+suhd/vHqoAJeSWhEubKjRPQHJg==",
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-14.3.0.tgz",
+ "integrity": "sha512-s71v6jMkbfwVdj87csLeNpL5K93mv4lN+lzgzifoICtPHhnXokDwBa3jrzfg+z6FK872iYJ0vS0i74v8XmoFDA==",
"requires": {
"readable-web-to-node-stream": "^2.0.0",
"strtok3": "^6.0.0",
@@ -9725,7 +9725,6 @@
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
- "dev": true,
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@@ -9752,14 +9751,12 @@
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
- "dev": true
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
- "dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@@ -10351,12 +10348,13 @@
"integrity": "sha512-1bBO+me3gXRfqwRR3K9aNDoSbTkQ87o6fSjj/BE2gSHHsK3qIDR+LoFZHgZ6kSPdFBoLTsy5/w/+8PBBaK+lvg=="
},
"sqlite3": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.2.0.tgz",
- "integrity": "sha512-roEOz41hxui2Q7uYnWsjMOTry6TcNUNmp8audCx18gF10P2NknwdpF+E+HKvz/F2NvPKGGBF4NGc+ZPQ+AABwg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.1.tgz",
+ "integrity": "sha512-CvT5XY+MWnn0HkbwVKJAyWEMfzpAPwnTiB3TobA5Mri44SrTovmmh499NPQP+gatkeOipqPlBLel7rn4E/PCQg==",
"requires": {
"nan": "^2.12.1",
- "node-pre-gyp": "^0.11.0"
+ "node-pre-gyp": "^0.11.0",
+ "request": "^2.87.0"
}
},
"squeak": {
diff --git a/package.json b/package.json
index 4d96ee213..92c2961ae 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
- "version": "0.41.6",
+ "version": "0.42.0-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -37,7 +37,7 @@
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.1",
- "file-type": "14.2.0",
+ "file-type": "14.3.0",
"fs-extra": "9.0.0",
"helmet": "3.22.0",
"html": "1.0.0",
@@ -67,7 +67,7 @@
"session-file-store": "1.4.0",
"simple-node-logger": "18.12.24",
"sqlite": "4.0.7",
- "sqlite3": "4.2.0",
+ "sqlite3": "4.1.1",
"string-similarity": "4.0.1",
"tar-stream": "2.1.2",
"turndown": "6.0.0",
@@ -78,7 +78,7 @@
"yazl": "^2.5.1"
},
"devDependencies": {
- "electron": "9.0.0-beta.21",
+ "electron": "9.0.0-beta.22",
"electron-builder": "22.6.0",
"electron-packager": "14.2.1",
"electron-rebuild": "1.10.1",
diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js
index 68e14f3be..2b1dbf610 100644
--- a/src/public/app/entities/note_short.js
+++ b/src/public/app/entities/note_short.js
@@ -8,6 +8,8 @@ const RELATION = 'relation';
const RELATION_DEFINITION = 'relation-definition';
/**
+ * FIXME: since there's no "full note" anymore we can rename this to Note
+ *
* This note's representation is used in note tree and is kept in TreeCache.
*/
class NoteShort {
diff --git a/src/public/app/services/app_context.js b/src/public/app/services/app_context.js
index 75c1c820c..b71224839 100644
--- a/src/public/app/services/app_context.js
+++ b/src/public/app/services/app_context.js
@@ -100,19 +100,6 @@ class AppContext extends Component {
getComponentByEl(el) {
return $(el).closest(".component").prop('component');
}
-
- async openInNewWindow(notePath) {
- if (utils.isElectron()) {
- const {ipcRenderer} = utils.dynamicRequire('electron');
-
- ipcRenderer.send('create-extra-window', {notePath});
- }
- else {
- const url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?extra=1#' + notePath;
-
- window.open(url, '', 'width=1000,height=800');
- }
- }
}
const appContext = new AppContext(window.glob.isMainWindow);
diff --git a/src/public/app/services/entrypoints.js b/src/public/app/services/entrypoints.js
index 80d148c7c..262bf6e19 100644
--- a/src/public/app/services/entrypoints.js
+++ b/src/public/app/services/entrypoints.js
@@ -182,4 +182,21 @@ export default class Entrypoints extends Component {
}
createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); }
+
+ async openInWindowCommand({notePath}) {
+ if (utils.isElectron()) {
+ const {ipcRenderer} = utils.dynamicRequire('electron');
+
+ ipcRenderer.send('create-extra-window', {notePath});
+ }
+ else {
+ const url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?extra=1#' + notePath;
+
+ window.open(url, '', 'width=1000,height=800');
+ }
+ }
+
+ async openNewWindowCommand() {
+ this.openInWindowCommand({notePath: ''});
+ }
}
diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js
index 97732821d..9a32f3e6c 100644
--- a/src/public/app/services/link.js
+++ b/src/public/app/services/link.js
@@ -114,7 +114,7 @@ function newTabContextMenu(e) {
y: e.pageY,
items: [
{title: "Open note in new tab", command: "openNoteInNewTab", uiIcon: "arrow-up-right"},
- {title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "arrow-up-right"}
+ {title: "Open note in new window", command: "openNoteInNewWindow", uiIcon: "window-open"}
],
selectMenuItemHandler: ({command}) => {
if (command === 'openNoteInNewTab') {
diff --git a/src/public/app/services/main_tree_executors.js b/src/public/app/services/main_tree_executors.js
index 61717e64f..c4bab5b28 100644
--- a/src/public/app/services/main_tree_executors.js
+++ b/src/public/app/services/main_tree_executors.js
@@ -59,7 +59,7 @@ export default class MainTreeExecutors extends Component {
target: 'after',
targetBranchId: node.data.branchId,
isProtected: isProtected,
- saveSelection: true
+ saveSelection: false
});
await ws.waitForMaxKnownSyncId();
diff --git a/src/public/app/services/protected_session.js b/src/public/app/services/protected_session.js
index 100df55e1..29f6e4e9a 100644
--- a/src/public/app/services/protected_session.js
+++ b/src/public/app/services/protected_session.js
@@ -31,6 +31,15 @@ function enterProtectedSession() {
return dfd.promise();
}
+async function reloadData() {
+ const allNoteIds = Object.keys(treeCache.notes);
+
+ await treeCache.loadInitialTree();
+
+ // make sure that all notes used in the application are loaded, including the ones not shown in the tree
+ await treeCache.reloadNotes(allNoteIds, true);
+}
+
async function setupProtectedSession(password) {
const response = await enterProtectedSessionOnServer(password);
@@ -42,7 +51,7 @@ async function setupProtectedSession(password) {
protectedSessionHolder.setProtectedSessionId(response.protectedSessionId);
protectedSessionHolder.touchProtectedSession();
- await treeCache.loadInitialTree();
+ await reloadData();
await appContext.triggerEvent('treeCacheReloaded');
diff --git a/src/public/app/services/tab_manager.js b/src/public/app/services/tab_manager.js
index a69cd5551..5463e295c 100644
--- a/src/public/app/services/tab_manager.js
+++ b/src/public/app/services/tab_manager.js
@@ -82,7 +82,7 @@ export default class TabManager extends Component {
if (filteredTabs.length === 0) {
filteredTabs.push({
- notePath: 'root',
+ notePath: this.isMainWindow ? 'root' : '',
active: true
});
}
@@ -196,7 +196,9 @@ export default class TabManager extends Component {
async openTabWithNote(notePath, activate, tabId = null) {
const tabContext = await this.openEmptyTab(tabId);
- await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
+ if (notePath) {
+ await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
+ }
if (activate) {
this.activateTab(tabContext.tabId, false);
@@ -265,6 +267,9 @@ export default class TabManager extends Component {
this.children = this.children.filter(tc => tc.tabId !== tabId);
+ // remove dangling autocompletes after closing the tab
+ $(".algolia-autocomplete").remove();
+
this.triggerEvent('tabRemoved', {tabId});
this.tabsUpdate.scheduleUpdate();
@@ -327,7 +332,7 @@ export default class TabManager extends Component {
this.removeTab(tabId);
- appContext.openInNewWindow(notePath);
+ this.triggerCommand('openInWindow', {notePath});
}
async hoistedNoteChangedEvent({hoistedNoteId}) {
diff --git a/src/public/app/services/tree_cache.js b/src/public/app/services/tree_cache.js
index 7f4095f40..4b18b7fa8 100644
--- a/src/public/app/services/tree_cache.js
+++ b/src/public/app/services/tree_cache.js
@@ -20,6 +20,8 @@ class TreeCache {
async loadInitialTree() {
const resp = await server.get('tree');
+ await this.loadParents(resp, false);
+
// clear the cache only directly before adding new content which is important for e.g. switching to protected session
/** @type {Object.} */
@@ -34,22 +36,22 @@ class TreeCache {
/** @type {Object.>} */
this.noteComplementPromises = {};
- await this.loadParents(resp);
this.addResp(resp);
}
- async loadParents(resp) {
+ async loadParents(resp, additiveLoad) {
const noteIds = new Set(resp.notes.map(note => note.noteId));
const missingNoteIds = [];
+ const existingNotes = additiveLoad ? this.notes : {};
for (const branch of resp.branches) {
- if (!(branch.parentNoteId in this.notes) && !noteIds.has(branch.parentNoteId) && branch.parentNoteId !== 'none') {
+ if (!(branch.parentNoteId in existingNotes) && !noteIds.has(branch.parentNoteId) && branch.parentNoteId !== 'none') {
missingNoteIds.push(branch.parentNoteId);
}
}
for (const attr of resp.attributes) {
- if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in this.notes) && !noteIds.has(attr.value)) {
+ if (attr.type === 'relation' && attr.name === 'template' && !(attr.value in existingNotes) && !noteIds.has(attr.value)) {
missingNoteIds.push(attr.value);
}
}
@@ -61,7 +63,7 @@ class TreeCache {
resp.branches = resp.branches.concat(newResp.branches);
resp.attributes = resp.attributes.concat(newResp.attributes);
- await this.loadParents(resp);
+ await this.loadParents(resp, additiveLoad);
}
}
@@ -154,7 +156,7 @@ class TreeCache {
const resp = await server.post('tree/load', { noteIds });
- await this.loadParents(resp);
+ await this.loadParents(resp, true);
this.addResp(resp);
for (const note of resp.notes) {
@@ -231,7 +233,7 @@ class TreeCache {
/** @return {Promise} */
async getNote(noteId, silentNotFoundError = false) {
if (noteId === 'none') {
- console.log(`No 'none' note.`);
+ console.trace(`No 'none' note.`);
return null;
}
else if (!noteId) {
@@ -246,10 +248,10 @@ class TreeCache {
return this.notes[noteId];
}
- getBranches(branchIds) {
+ getBranches(branchIds, silentNotFoundError = false) {
return branchIds
- .map(branchId => this.getBranch(branchId))
- .filter(b => b !== null);
+ .map(branchId => this.getBranch(branchId, silentNotFoundError))
+ .filter(b => !!b);
}
/** @return {Branch} */
diff --git a/src/public/app/services/tree_context_menu.js b/src/public/app/services/tree_context_menu.js
index dd0b9a6bd..002956ebd 100644
--- a/src/public/app/services/tree_context_menu.js
+++ b/src/public/app/services/tree_context_menu.js
@@ -40,9 +40,9 @@ class TreeContextMenu {
async getMenuItems() {
const note = await treeCache.getNote(this.node.data.noteId);
const branch = treeCache.getBranch(this.node.data.branchId);
- const parentNote = await treeCache.getNote(branch.parentNoteId);
const isNotRoot = note.noteId !== 'root';
const isHoisted = note.noteId === hoistedNoteService.getHoistedNoteId();
+ const parentNote = isNotRoot ? await treeCache.getNote(branch.parentNoteId) : null;
// some actions don't support multi-note so they are disabled when notes are selected
// the only exception is when the only selected note is the one that was right-clicked, then
@@ -57,7 +57,7 @@ class TreeContextMenu {
return [
{ title: 'Open in a new tab Ctrl+Click', command: "openInTab", uiIcon: "empty", enabled: noSelectedNotes },
- { title: 'Open in a new window', command: "openInWindow", uiIcon: "empty", enabled: noSelectedNotes },
+ { title: 'Open in a new window', command: "openInWindow", uiIcon: "window-open", enabled: noSelectedNotes },
{ title: 'Insert note after ', command: "insertNoteAfter", uiIcon: "plus",
items: insertNoteAfterEnabled ? this.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes },
@@ -113,9 +113,6 @@ class TreeContextMenu {
if (command === 'openInTab') {
appContext.tabManager.openTabWithNote(notePath);
}
- else if (command === 'openInWindow') {
- appContext.openInNewWindow(notePath);
- }
else if (command === "insertNoteAfter") {
const parentNoteId = this.node.data.parentNoteId;
const isProtected = await treeService.getParentProtectedStatus(this.node);
@@ -134,7 +131,7 @@ class TreeContextMenu {
});
}
else {
- this.treeWidget.triggerCommand(command, {node: this.node});
+ this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath});
}
}
}
diff --git a/src/public/app/widgets/global_menu.js b/src/public/app/widgets/global_menu.js
index 1cd3a2ec9..1c18b7f5d 100644
--- a/src/public/app/widgets/global_menu.js
+++ b/src/public/app/widgets/global_menu.js
@@ -1,5 +1,4 @@
import BasicWidget from "./basic_widget.js";
-import keyboardActionService from "../services/keyboard_actions.js";
import utils from "../services/utils.js";
import syncService from "../services/sync.js";
@@ -39,6 +38,12 @@ const TPL = `
Sync (0)
+
+
+ Open new window
+
+
+
Open Dev Tools
diff --git a/src/public/app/widgets/note_title.js b/src/public/app/widgets/note_title.js
index f44421049..576dd5b81 100644
--- a/src/public/app/widgets/note_title.js
+++ b/src/public/app/widgets/note_title.js
@@ -57,6 +57,10 @@ export default class NoteTitleWidget extends TabAwareWidget {
this.$noteTitle.prop("readonly", note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable());
+ this.setProtectedStatus(note);
+ }
+
+ setProtectedStatus(note) {
this.$noteTitle.toggleClass("protected", !!note.isProtected);
}
@@ -88,7 +92,8 @@ export default class NoteTitleWidget extends TabAwareWidget {
entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteReloaded(this.noteId)) {
- this.refresh();
+ // not updating the title specifically since the synced title might be older than what the user is currently typing
+ this.setProtectedStatus(this.note);
}
}
diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js
index de97e8450..5c4cc4ae5 100644
--- a/src/public/app/widgets/note_tree.js
+++ b/src/public/app/widgets/note_tree.js
@@ -51,7 +51,7 @@ const TPL = `
position: absolute;
top: 10px;
right: 20px;
- z-index: 1000;
+ z-index: 100;
}
.tree-settings-popup {
@@ -362,17 +362,13 @@ export default class NoteTreeWidget extends TabAwareWidget {
},
// this is done to automatically lazy load all expanded notes after tree load
loadChildren: (event, data) => {
- // semaphore since the conflict when two processes are trying to load the same data
- // breaks the fancytree
- if (!this.tree || !this.tree.autoLoadingDisabled) {
- data.node.visit((subNode) => {
- // Load all lazy/unloaded child nodes
- // (which will trigger `loadChildren` recursively)
- if (subNode.isUndefined() && subNode.isExpanded()) {
- subNode.load();
- }
- });
- }
+ data.node.visit((subNode) => {
+ // Load all lazy/unloaded child nodes
+ // (which will trigger `loadChildren` recursively)
+ if (subNode.isUndefined() && subNode.isExpanded()) {
+ subNode.load();
+ }
+ });
}
});
@@ -423,7 +419,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
return labels.map(l => l.value).join(' ');
}
- getIcon(note) {
+ getIcon(note, isFolder) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
const iconClass = this.getIconClass(note);
@@ -438,7 +434,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
return "bx bxs-arrow-from-bottom";
}
else if (note.type === 'text') {
- if (note.hasChildren()) {
+ if (isFolder) {
return "bx bx-folder";
}
else {
@@ -460,6 +456,8 @@ export default class NoteTreeWidget extends TabAwareWidget {
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
+ const isFolder = this.isFolder(note);
+
const node = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
@@ -468,10 +466,10 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteType: note.type,
title: utils.escapeHtml(title),
extraClasses: this.getExtraClasses(note),
- icon: this.getIcon(note),
+ icon: this.getIcon(note, isFolder),
refKey: note.noteId,
lazy: true,
- folder: await this.isFolder(note),
+ folder: isFolder,
expanded: branch.isExpanded || hoistedNoteId === note.noteId,
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -483,12 +481,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
return node;
}
- async isFolder(note) {
+ isFolder(note) {
if (note.type === 'search') {
return true;
}
else {
- const childBranches = await this.getChildBranches(note);
+ const childBranches = this.getChildBranches(note);
return childBranches.length > 0;
}
@@ -499,7 +497,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
const noteList = [];
- for (const branch of await this.getChildBranches(parentNote)) {
+ for (const branch of this.getChildBranches(parentNote)) {
const node = await this.prepareNode(branch);
noteList.push(node);
@@ -508,7 +506,7 @@ export default class NoteTreeWidget extends TabAwareWidget {
return noteList;
}
- async getChildBranches(parentNote) {
+ getChildBranches(parentNote) {
let childBranches = parentNote.getChildBranches();
if (!childBranches) {
@@ -523,20 +521,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
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;
}
@@ -596,39 +580,32 @@ export default class NoteTreeWidget extends TabAwareWidget {
return notes;
}
- async expandTree(node = null) {
+ async setExpandedStatusForSubtree(node, isExpanded) {
if (!node) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
node = this.getNodesByNoteId(hoistedNoteId)[0];
}
- this.batchUpdate(async () => {
- try {
- this.tree.autoLoadingDisabled = true;
+ const {branchIds} = await server.put(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`);
- // trick - first force load of the whole subtree and then visit and expand.
- // unfortunately the two steps can't be combined
- await node.visitAndLoad(_ => {}, true);
+ treeCache.getBranches(branchIds, true).forEach(branch => branch.isExpanded = isExpanded);
- node.visit(node => node.setExpanded(true), true);
- }
- finally {
- this.tree.autoLoadingDisabled = false;
+ await this.batchUpdate(async () => {
+ await node.load(true);
+
+ if (node.data.noteId !== 'root') { // root is always expanded
+ await node.setExpanded(isExpanded, {noEvents: true});
}
});
}
- collapseTree(node = null) {
- if (!node) {
- const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
+ async expandTree(node = null) {
+ await this.setExpandedStatusForSubtree(node, true);
+ }
- node = this.getNodesByNoteId(hoistedNoteId)[0];
- }
-
- this.batchUpdate(() => {
- node.visit(node => node.setExpanded(false), true);
- });
+ async collapseTree(node = null) {
+ await this.setExpandedStatusForSubtree(node, false);
}
/**
@@ -740,14 +717,16 @@ export default class NoteTreeWidget extends TabAwareWidget {
return this.getNodeFromPath(notePath, true, expandOpts);
}
- async updateNode(node) {
+ updateNode(node) {
const note = treeCache.getNoteFromCache(node.data.noteId);
const branch = treeCache.getBranch(node.data.branchId);
+ const isFolder = this.isFolder(note);
+
node.data.isProtected = note.isProtected;
node.data.noteType = note.type;
- node.folder = await this.isFolder(note);
- node.icon = this.getIcon(note);
+ node.folder = isFolder;
+ node.icon = this.getIcon(note, isFolder);
node.extraClasses = this.getExtraClasses(note);
node.title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
node.renderTitle();
@@ -898,18 +877,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
noteIdsToUpdate.add(noteId);
}
- for (const noteId of noteIdsToReload) {
- for (const node of this.getNodesByNoteId(noteId)) {
- await node.load(true);
-
- this.updateNode(node);
- }
- }
-
await this.batchUpdate(async () => {
- for (const noteId of noteIdsToUpdate) {
+ for (const noteId of noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
- this.updateNode(node);
+ await node.load(true);
+
+ noteIdsToUpdate.add(noteId);
}
}
@@ -931,6 +904,13 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
});
+ // for some reason node update cannot be in the batchUpdate() block (node is not re-rendered)
+ for (const noteId of noteIdsToUpdate) {
+ for (const node of this.getNodesByNoteId(noteId)) {
+ this.updateNode(node);
+ }
+ }
+
if (activeNotePath) {
let node = await this.expandToNote(activeNotePath);
diff --git a/src/public/app/widgets/tab_row.js b/src/public/app/widgets/tab_row.js
index 7f896560b..fd89cf47a 100644
--- a/src/public/app/widgets/tab_row.js
+++ b/src/public/app/widgets/tab_row.js
@@ -258,9 +258,9 @@ export default class TabRowWidget extends BasicWidget {
x: e.pageX,
y: e.pageY,
items: [
- {title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "empty"},
- {title: "Close all tabs", command: "removeAllTabs", uiIcon: "empty"},
- {title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "empty"},
+ {title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "window-open"},
+ {title: "Close all tabs", command: "removeAllTabs", uiIcon: "x"},
+ {title: "Close all tabs except for this", command: "removeAllTabsExceptForThis", uiIcon: "x"},
],
selectMenuItemHandler: ({command}) => {
this.triggerCommand(command, {tabId});
diff --git a/src/routes/api/branches.js b/src/routes/api/branches.js
index 6917f5981..b0cfe0578 100644
--- a/src/routes/api/branches.js
+++ b/src/routes/api/branches.js
@@ -114,8 +114,34 @@ async function moveBranchAfterNote(req) {
async function setExpanded(req) {
const {branchId, expanded} = req.params;
- await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
- // we don't sync expanded label
+ if (branchId !== 'root') {
+ await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
+ // we don't sync expanded label
+ }
+}
+
+async function setExpandedForSubtree(req) {
+ const {branchId, expanded} = req.params;
+
+ let branchIds = await sql.getColumn(`
+ WITH RECURSIVE
+ tree(branchId, noteId) AS (
+ SELECT branchId, noteId FROM branches WHERE branchId = ?
+ UNION
+ SELECT branches.branchId, branches.noteId FROM branches
+ JOIN tree ON branches.parentNoteId = tree.noteId
+ WHERE branches.isDeleted = 0
+ )
+ SELECT branchId FROM tree`, [branchId]);
+
+ // root is always expanded
+ branchIds = branchIds.filter(branchId => branchId !== 'root');
+
+ await sql.executeMany(`UPDATE branches SET isExpanded = ${expanded} WHERE branchId IN (???)`, branchIds);
+
+ return {
+ branchIds
+ };
}
async function deleteBranch(req) {
@@ -149,6 +175,7 @@ module.exports = {
moveBranchBeforeNote,
moveBranchAfterNote,
setExpanded,
+ setExpandedForSubtree,
deleteBranch,
setPrefix
};
\ No newline at end of file
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 13783ec35..439ae1201 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -127,6 +127,7 @@ function register(app) {
apiRoute(PUT, '/api/branches/:branchId/move-before/:beforeBranchId', branchesApiRoute.moveBranchBeforeNote);
apiRoute(PUT, '/api/branches/:branchId/move-after/:afterBranchId', branchesApiRoute.moveBranchAfterNote);
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
+ apiRoute(PUT, '/api/branches/:branchId/expanded-subtree/:expanded', branchesApiRoute.setExpandedForSubtree);
apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch);
apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete);
diff --git a/src/services/build.js b/src/services/build.js
index 853f0d3c3..e47c8290e 100644
--- a/src/services/build.js
+++ b/src/services/build.js
@@ -1 +1 @@
-module.exports = { buildDate:"2020-04-27T23:46:48+02:00", buildRevision: "0a9462241360e0baac71863af3ce7fb07cfd8c87" };
+module.exports = { buildDate:"2020-05-04T21:59:14+02:00", buildRevision: "cafcb67a8a3a1943acac829590b34ff729b57e09" };
diff --git a/src/services/build_search_query.js b/src/services/build_search_query.js
index 6060daf6a..a7867aa03 100644
--- a/src/services/build_search_query.js
+++ b/src/services/build_search_query.js
@@ -12,9 +12,7 @@ const VIRTUAL_ATTRIBUTES = [
"type",
"mime",
"text",
- "parentCount",
- "attributeName",
- "attributeValue"
+ "parentCount"
];
module.exports = function(filters, selectedColumns = 'notes.*') {
@@ -35,29 +33,11 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
// forcing to use particular index since SQLite query planner would often choose something pretty bad
joins[alias] = `LEFT JOIN attributes AS ${alias} INDEXED BY IDX_attributes_noteId_index `
- + `ON ${alias}.noteId = notes.noteId AND ${alias}.isDeleted = 0 `
- + `AND ${alias}.name = '${property}' `;
+ + `ON ${alias}.noteId = notes.noteId `
+ + `AND ${alias}.name = '${property}' AND ${alias}.isDeleted = 0`;
accessor = `${alias}.value`;
}
- else if (['attributeType', 'attributeName', 'attributeValue'].includes(property)) {
- const alias = "attr_filter";
-
- if (!(alias in joins)) {
- joins[alias] = `LEFT JOIN attributes AS ${alias} INDEXED BY IDX_attributes_noteId_index `
- + `ON ${alias}.noteId = notes.noteId AND ${alias}.isDeleted = 0`;
- }
-
- if (property === 'attributeType') {
- accessor = `${alias}.type`
- } else if (property === 'attributeName') {
- accessor = `${alias}.name`
- } else if (property === 'attributeValue') {
- accessor = `${alias}.value`
- } else {
- throw new Error(`Unrecognized property ${property}`);
- }
- }
else if (property === 'content') {
const alias = "note_contents";
@@ -93,40 +73,33 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
});
}
+ let where = '1';
const params = [];
- function parseWhereFilters(filters) {
- let whereStmt = '';
-
for (const filter of filters) {
if (['isarchived', 'in', 'orderby', 'limit'].includes(filter.name.toLowerCase())) {
continue; // these are not real filters
}
- if (whereStmt) {
- whereStmt += " " + filter.relation + " ";
- }
-
- if (filter.children) {
- whereStmt += "(" + parseWhereFilters(filter.children) + ")";
- continue;
- }
+ where += " " + filter.relation + " ";
const accessor = getAccessor(filter.name);
if (filter.operator === 'exists') {
- whereStmt += `${accessor} IS NOT NULL`;
- } else if (filter.operator === 'not-exists') {
- whereStmt += `${accessor} IS NULL`;
- } else if (filter.operator === '=' || filter.operator === '!=') {
- whereStmt += `${accessor} ${filter.operator} ?`;
+ where += `${accessor} IS NOT NULL`;
+ }
+ else if (filter.operator === 'not-exists') {
+ where += `${accessor} IS NULL`;
+ }
+ else if (filter.operator === '=' || filter.operator === '!=') {
+ where += `${accessor} ${filter.operator} ?`;
params.push(filter.value);
} else if (filter.operator === '*=' || filter.operator === '!*=') {
- whereStmt += `${accessor}`
+ where += `${accessor}`
+ (filter.operator.includes('!') ? ' NOT' : '')
+ ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '');
} else if (filter.operator === '=*' || filter.operator === '!=*') {
- whereStmt += `${accessor}`
+ where += `${accessor}`
+ (filter.operator.includes('!') ? ' NOT' : '')
+ ` LIKE ` + utils.prepareSqlForLike('', filter.value, '%');
} else if (filter.operator === '*=*' || filter.operator === '!*=*') {
@@ -145,8 +118,9 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
condition = `(${condition} AND notes.isProtected = 0)`;
}
- whereStmt += condition;
- } else if ([">", ">=", "<", "<="].includes(filter.operator)) {
+ where += condition;
+ }
+ else if ([">", ">=", "<", "<="].includes(filter.operator)) {
let floatParam;
// from https://stackoverflow.com/questions/12643009/regular-expression-for-floating-point-numbers
@@ -156,10 +130,10 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
if (floatParam === undefined || isNaN(floatParam)) {
// if the value can't be parsed as float then we assume that string comparison should be used instead of numeric
- whereStmt += `${accessor} ${filter.operator} ?`;
+ where += `${accessor} ${filter.operator} ?`;
params.push(filter.value);
} else {
- whereStmt += `CAST(${accessor} AS DECIMAL) ${filter.operator} ?`;
+ where += `CAST(${accessor} AS DECIMAL) ${filter.operator} ?`;
params.push(floatParam);
}
} else {
@@ -167,11 +141,6 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
}
}
- return whereStmt;
- }
-
- const where = parseWhereFilters(filters);
-
if (orderBy.length === 0) {
// if no ordering is given then order at least by note title
orderBy.push("notes.title");
diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js
index a5898c397..ce8b50f05 100644
--- a/src/services/consistency_checks.js
+++ b/src/services/consistency_checks.js
@@ -617,6 +617,9 @@ class ConsistencyChecks {
await this.findSyncRowsIssues();
+ // root branch should always be expanded
+ await sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'");
+
if (this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency
diff --git a/src/services/keyboard_actions.js b/src/services/keyboard_actions.js
index 44d66586d..425df2f59 100644
--- a/src/services/keyboard_actions.js
+++ b/src/services/keyboard_actions.js
@@ -193,7 +193,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{
- separator: "Tabs"
+ separator: "Tabs & Windows"
},
{
actionName: "openNewTab",
@@ -219,6 +219,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
description: "Activates tab on the left",
scope: "window"
},
+ {
+ actionName: "openNewWindow",
+ defaultShortcuts: [],
+ description: "Open new empty window",
+ scope: "window"
+ },
{
diff --git a/src/services/notes.js b/src/services/notes.js
index d1cd0ab7f..4df9846e8 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -276,9 +276,9 @@ async function downloadImage(noteId, imageUrl) {
const downloadImagePromises = {};
function replaceUrl(content, url, imageNote) {
- const quoted = url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const quoted = url.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
- return content.replace(new RegExp(`\s+src=[\"']${quoted}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
+ return content.replace(new RegExp(`\\s+src=[\"']${quoted}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
}
async function downloadImages(noteId, content) {
@@ -288,11 +288,11 @@ async function downloadImages(noteId, content) {
const origContent = content;
while (match = re.exec(origContent)) {
- const url = match[1].toLowerCase();
+ const url = match[1];
- if (!url.startsWith('api/images/')
+ if (!url.includes('api/images/')
// this is and exception for the web clipper's "imageId"
- && (url.length !== 20 || url.startsWith('http'))) {
+ && (url.length !== 20 || url.toLowerCase().startsWith('http'))) {
if (url in downloadImagePromises) {
// download is already in progress
continue;
@@ -347,7 +347,7 @@ async function downloadImages(noteId, content) {
for (const url in imageUrlToNoteIdMapping) {
const imageNote = imageNotes.find(note => note.noteId === imageUrlToNoteIdMapping[url]);
- if (imageNote) {
+ if (imageNote && !imageNote.isDeleted) {
updatedContent = replaceUrl(updatedContent, url, imageNote);
}
}
@@ -356,6 +356,8 @@ async function downloadImages(noteId, content) {
if (updatedContent !== origContent) {
await origNote.setContent(updatedContent);
+ await scanForLinks(origNote);
+
console.log(`Fixed the image links for note ${noteId} to the offline saved.`);
}
}, 5000);
@@ -376,11 +378,11 @@ async function saveLinks(note, content) {
const foundLinks = [];
if (note.type === 'text') {
+ content = await downloadImages(note.noteId, content);
+
content = findImageLinks(content, foundLinks);
content = findInternalLinks(content, foundLinks);
content = findIncludeNoteLinks(content, foundLinks);
-
- content = await downloadImages(note.noteId, content);
}
else if (note.type === 'relation-map') {
findRelationMapLinks(content, foundLinks);
diff --git a/src/services/options_init.js b/src/services/options_init.js
index 43d8060d9..ad5b509b6 100644
--- a/src/services/options_init.js
+++ b/src/services/options_init.js
@@ -83,8 +83,8 @@ const defaultOptions = [
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false },
{ 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
+ { name: 'hideArchivedNotes_main', value: 'false', isSynced: false },
+ { name: 'hideIncludedImages_main', value: 'true', isSynced: false }
];
async function initStartupOptions() {
diff --git a/src/services/parse_filters.js b/src/services/parse_filters.js
index cccec35bb..412c9b527 100644
--- a/src/services/parse_filters.js
+++ b/src/services/parse_filters.js
@@ -60,20 +60,6 @@ module.exports = function (searchText) {
operator: '*=*',
value: searchText
});
-
- filters.push({
- relation: 'or',
- name: 'attributeName',
- operator: '*=*',
- value: searchText
- });
-
- filters.push({
- relation: 'or',
- name: 'attributeValue',
- operator: '*=*',
- value: searchText
- });
}
else {
const tokens = searchText.split(/\s+/);
@@ -81,27 +67,9 @@ module.exports = function (searchText) {
for (const token of tokens) {
filters.push({
relation: 'and',
- name: 'sub',
- children: [
- {
- relation: 'or',
name: 'text',
operator: '*=*',
value: token
- },
- {
- relation: 'or',
- name: 'attributeName',
- operator: '*=*',
- value: token
- },
- {
- relation: 'or',
- name: 'attributeValue',
- operator: '*=*',
- value: token
- }
- ]
});
}
}
diff --git a/src/services/sql.js b/src/services/sql.js
index a928f319e..325dbc292 100644
--- a/src/services/sql.js
+++ b/src/services/sql.js
@@ -163,6 +163,10 @@ async function executeScript(query) {
}
async function wrap(func, query) {
+ if (!dbConnection) {
+ throw new Error("DB connection not initialized yet");
+ }
+
const thisError = new Error();
try {
diff --git a/src/services/sql_init.js b/src/services/sql_init.js
index d258ea4e8..eaf0aaf23 100644
--- a/src/services/sql_init.js
+++ b/src/services/sql_init.js
@@ -13,16 +13,11 @@ const port = require('./port');
const Option = require('../entities/option');
const TaskContext = require('./task_context.js');
-async function createConnection() {
- return await sqlite.open({
+const dbConnection = new Promise(async (resolve, reject) => {
+ const db = await sqlite.open({
filename: dataDir.DOCUMENT_PATH,
driver: sqlite3.Database
});
-}
-
-const dbConnection = new Promise(async (resolve, reject) => {
- // no need to create new connection now since DB stays the same all the time
- const db = await createConnection();
db.run('PRAGMA journal_mode = WAL;');