Merge branch 'master' into m43

This commit is contained in:
zadam 2020-05-05 19:38:42 +02:00
commit 768ac83e14
28 changed files with 216 additions and 233 deletions

View file

@ -5,7 +5,7 @@
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/../trilium-data/document.db</jdbc-url>
<jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url>
</data-source>
<data-source source="LOCAL" name="document" uuid="066dc5f4-4097-429e-8cf1-3adc0a9d648a">
<driver-ref>sqlite.xerial</driver-ref>

View file

@ -1,6 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />

34
package-lock.json generated
View file

@ -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": {

View file

@ -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",

View file

@ -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 {

View file

@ -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);

View file

@ -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: ''});
}
}

View file

@ -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') {

View file

@ -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();

View file

@ -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');

View file

@ -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);
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}) {

View file

@ -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.<string, NoteShort>} */
@ -34,22 +36,22 @@ class TreeCache {
/** @type {Object.<string, Promise<NoteComplement>>} */
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<NoteShort>} */
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} */

View file

@ -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 <kbd>Ctrl+Click</kbd>', 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 <kbd data-command="createNoteAfter"></kbd>', 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});
}
}
}

View file

@ -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 (<span id="outstanding-syncs-count">0</span>)
</a>
<a class="dropdown-item" data-trigger-command="openNewWindow">
<span class="bx bx-window-open"></span>
Open new window
<kbd data-command="openNewWindow"></kbd>
</a>
<a class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-terminal"></span>
Open Dev Tools

View file

@ -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);
}
}

View file

@ -51,7 +51,7 @@ const TPL = `
position: absolute;
top: 10px;
right: 20px;
z-index: 1000;
z-index: 100;
}
.tree-settings-popup {
@ -362,9 +362,6 @@ 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)
@ -373,7 +370,6 @@ export default class NoteTreeWidget extends TabAwareWidget {
}
});
}
}
});
this.$tree.on('contextmenu', '.fancytree-node', e => {
@ -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 setExpandedStatusForSubtree(node, isExpanded) {
if (!node) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
node = this.getNodesByNoteId(hoistedNoteId)[0];
}
const {branchIds} = await server.put(`branches/${node.data.branchId}/expanded-subtree/${isExpanded ? 1 : 0}`);
treeCache.getBranches(branchIds, true).forEach(branch => branch.isExpanded = isExpanded);
await this.batchUpdate(async () => {
await node.load(true);
if (node.data.noteId !== 'root') { // root is always expanded
await node.setExpanded(isExpanded, {noEvents: true});
}
});
}
async expandTree(node = null) {
if (!node) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
node = this.getNodesByNoteId(hoistedNoteId)[0];
await this.setExpandedStatusForSubtree(node, true);
}
this.batchUpdate(async () => {
try {
this.tree.autoLoadingDisabled = true;
// 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);
node.visit(node => node.setExpanded(true), true);
}
finally {
this.tree.autoLoadingDisabled = false;
}
});
}
collapseTree(node = null) {
if (!node) {
const hoistedNoteId = hoistedNoteService.getHoistedNoteId();
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);
}
await this.batchUpdate(async () => {
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 node of this.getNodesByNoteId(noteId)) {
this.updateNode(node);
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);

View file

@ -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});

View file

@ -114,9 +114,35 @@ async function moveBranchAfterNote(req) {
async function setExpanded(req) {
const {branchId, expanded} = req.params;
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) {
const last = req.query.last === 'true';
@ -149,6 +175,7 @@ module.exports = {
moveBranchBeforeNote,
moveBranchAfterNote,
setExpanded,
setExpandedForSubtree,
deleteBranch,
setPrefix
};

View file

@ -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);

View file

@ -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" };

View file

@ -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");

View file

@ -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

View file

@ -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"
},
{

View file

@ -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);

View file

@ -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() {

View file

@ -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
}
]
});
}
}

View file

@ -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 {

View file

@ -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;');