mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	Merge branch 'master' into m43
This commit is contained in:
		
						commit
						768ac83e14
					
				
							
								
								
									
										2
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
								
							@ -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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
								
							@ -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
									
									
									
								
							
							
						
						
									
										34
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -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": {
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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: ''});
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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') {
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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}) {
 | 
			
		||||
 | 
			
		||||
@ -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} */
 | 
			
		||||
 | 
			
		||||
@ -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});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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});
 | 
			
		||||
 | 
			
		||||
@ -114,8 +114,34 @@ 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) {
 | 
			
		||||
@ -149,6 +175,7 @@ module.exports = {
 | 
			
		||||
    moveBranchBeforeNote,
 | 
			
		||||
    moveBranchAfterNote,
 | 
			
		||||
    setExpanded,
 | 
			
		||||
    setExpandedForSubtree,
 | 
			
		||||
    deleteBranch,
 | 
			
		||||
    setPrefix
 | 
			
		||||
};
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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" };
 | 
			
		||||
 | 
			
		||||
@ -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");
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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;');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user