diff --git a/db/demo.zip b/db/demo.zip
index 7f106b568..a2327ff33 100644
Binary files a/db/demo.zip and b/db/demo.zip differ
diff --git a/docs/backend_api/AbstractEntity.html b/docs/backend_api/AbstractEntity.html
index e017cf1c9..ab08501ce 100644
--- a/docs/backend_api/AbstractEntity.html
+++ b/docs/backend_api/AbstractEntity.html
@@ -158,6 +158,8 @@
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -265,7 +267,7 @@
Source:
diff --git a/docs/backend_api/Attribute.html b/docs/backend_api/Attribute.html
index 59b518036..8633da77b 100644
--- a/docs/backend_api/Attribute.html
+++ b/docs/backend_api/Attribute.html
@@ -1030,6 +1030,8 @@ and relation (representing named relationship between source and target note)
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -1142,7 +1144,7 @@ and relation (representing named relationship between source and target note)Source:
diff --git a/docs/backend_api/Branch.html b/docs/backend_api/Branch.html
index 4be4b47f5..f89ff45ad 100644
--- a/docs/backend_api/Branch.html
+++ b/docs/backend_api/Branch.html
@@ -94,7 +94,7 @@ parents.
Source:
@@ -205,7 +205,7 @@ parents.
Source:
@@ -263,7 +263,7 @@ parents.
Source:
@@ -331,7 +331,7 @@ parents.
Source:
@@ -399,7 +399,7 @@ parents.
Source:
@@ -467,7 +467,7 @@ parents.
Source:
@@ -525,7 +525,7 @@ parents.
Source:
@@ -593,7 +593,7 @@ parents.
Source:
@@ -661,7 +661,7 @@ parents.
Source:
@@ -729,7 +729,7 @@ parents.
Source:
@@ -757,6 +757,210 @@ parents.
+ deleteBranch(deleteIdopt , taskContextopt ) → {boolean}
+
+
+
+
+
+
+
+ Delete a branch. If this is a last note's branch, delete the note as well.
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ deleteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+ optional delete identified
+
+
+
+
+
+
+ taskContext
+
+
+
+
+
+TaskContext
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ - true if note has been deleted, false otherwise
+
+
+
+
+
+
+ Type
+
+
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
markAsDeleted(deleteIdopt )
@@ -766,6 +970,8 @@ parents.
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -878,7 +1084,7 @@ parents.
Source:
diff --git a/docs/backend_api/EtapiToken.html b/docs/backend_api/EtapiToken.html
index bffcb3658..b675d8cdc 100644
--- a/docs/backend_api/EtapiToken.html
+++ b/docs/backend_api/EtapiToken.html
@@ -587,6 +587,8 @@ from tokenHash and token.
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -699,7 +701,7 @@ from tokenHash and token.
Source:
diff --git a/docs/backend_api/Note.html b/docs/backend_api/Note.html
index 5a24d464a..c5eea4f2f 100644
--- a/docs/backend_api/Note.html
+++ b/docs/backend_api/Note.html
@@ -93,7 +93,7 @@
Source:
@@ -204,7 +204,7 @@
Source:
@@ -279,7 +279,7 @@
Source:
@@ -347,7 +347,7 @@
Source:
@@ -415,7 +415,7 @@
Source:
@@ -486,7 +486,7 @@
Source:
@@ -554,7 +554,7 @@
Source:
@@ -622,7 +622,7 @@
Source:
@@ -690,7 +690,7 @@
Source:
@@ -758,7 +758,7 @@
Source:
@@ -833,7 +833,7 @@
Source:
@@ -901,7 +901,7 @@
Source:
@@ -969,7 +969,7 @@
Source:
@@ -1037,7 +1037,7 @@
Source:
@@ -1112,7 +1112,7 @@
Source:
@@ -1180,7 +1180,7 @@
Source:
@@ -1248,7 +1248,7 @@
Source:
@@ -1316,7 +1316,7 @@
Source:
@@ -1384,7 +1384,7 @@
Source:
@@ -1452,7 +1452,7 @@
Source:
@@ -1528,7 +1528,7 @@
Source:
@@ -1630,7 +1630,7 @@
Source:
@@ -1678,6 +1678,188 @@
+
+
+
+
+
+
+ deleteNote(deleteIdopt , taskContextopt )
+
+
+
+
+
+
+
+ (Soft) delete a note and all its descendants.
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ deleteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+ optional delete identified
+
+
+
+
+
+
+ taskContext
+
+
+
+
+
+TaskContext
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1732,7 +1914,7 @@
Source:
@@ -1838,7 +2020,7 @@
Source:
@@ -2012,7 +2194,7 @@
Source:
@@ -2212,7 +2394,7 @@
Source:
@@ -2390,7 +2572,7 @@
Source:
@@ -2501,7 +2683,7 @@
Source:
@@ -2603,7 +2785,7 @@
Source:
@@ -2705,7 +2887,7 @@
Source:
@@ -2807,7 +2989,7 @@
Source:
@@ -2909,7 +3091,7 @@
Source:
@@ -3017,7 +3199,7 @@
Source:
@@ -3123,7 +3305,7 @@
Source:
@@ -3274,7 +3456,7 @@
Source:
@@ -3444,7 +3626,7 @@
Source:
@@ -3599,7 +3781,7 @@
Source:
@@ -3769,7 +3951,7 @@
Source:
@@ -3875,7 +4057,7 @@
Source:
@@ -4077,7 +4259,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4255,7 +4437,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4413,7 +4595,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4583,7 +4765,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4738,7 +4920,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -4908,7 +5090,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5063,7 +5245,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5233,7 +5415,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5388,7 +5570,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5497,7 +5679,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5599,7 +5781,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5750,7 +5932,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5920,7 +6102,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6075,7 +6257,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6184,7 +6366,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6293,7 +6475,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6395,7 +6577,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6497,7 +6679,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6599,7 +6781,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6706,7 +6888,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6808,7 +6990,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6959,7 +7141,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7137,7 +7319,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7292,7 +7474,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7447,7 +7629,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7602,7 +7784,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7752,7 +7934,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7858,7 +8040,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7964,7 +8146,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8070,7 +8252,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8176,7 +8358,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8282,7 +8464,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8349,6 +8531,8 @@ This method can be significantly faster than the getAttribute()
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -8461,7 +8645,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8672,7 +8856,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8852,7 +9036,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9032,7 +9216,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9354,7 +9538,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9534,7 +9718,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9694,7 +9878,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9936,7 +10120,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -10147,7 +10331,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -10358,7 +10542,7 @@ This method can be significantly faster than the getAttribute()
Source:
diff --git a/docs/backend_api/NoteRevision.html b/docs/backend_api/NoteRevision.html
index f9575d7df..ad8b80285 100644
--- a/docs/backend_api/NoteRevision.html
+++ b/docs/backend_api/NoteRevision.html
@@ -1300,6 +1300,8 @@ It's used for seamless note versioning.
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -1412,7 +1414,7 @@ It's used for seamless note versioning.
Source:
diff --git a/docs/backend_api/Option.html b/docs/backend_api/Option.html
index 2c43460c7..c812d8ea6 100644
--- a/docs/backend_api/Option.html
+++ b/docs/backend_api/Option.html
@@ -445,6 +445,8 @@
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -557,7 +559,7 @@
Source:
diff --git a/docs/backend_api/RecentNote.html b/docs/backend_api/RecentNote.html
index 264580c72..621598ed2 100644
--- a/docs/backend_api/RecentNote.html
+++ b/docs/backend_api/RecentNote.html
@@ -377,6 +377,8 @@
Mark the entity as (soft) deleted. It will be completely erased later.
+
+This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
@@ -489,7 +491,7 @@
Source:
diff --git a/docs/backend_api/becca_entities_abstract_entity.js.html b/docs/backend_api/becca_entities_abstract_entity.js.html
index 43b0899a8..a66b5654a 100644
--- a/docs/backend_api/becca_entities_abstract_entity.js.html
+++ b/docs/backend_api/becca_entities_abstract_entity.js.html
@@ -139,6 +139,8 @@ class AbstractEntity {
/**
* Mark the entity as (soft) deleted. It will be completely erased later.
*
+ * This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
+ *
* @param [deleteId=null]
*/
markAsDeleted(deleteId = null) {
diff --git a/docs/backend_api/becca_entities_branch.js.html b/docs/backend_api/becca_entities_branch.js.html
index 94ffed9dc..73fd62a3b 100644
--- a/docs/backend_api/becca_entities_branch.js.html
+++ b/docs/backend_api/becca_entities_branch.js.html
@@ -32,6 +32,10 @@ const Note = require('./note');
const AbstractEntity = require("./abstract_entity");
const sql = require("../../services/sql");
const dateUtils = require("../../services/date_utils");
+const utils = require("../../services/utils.js");
+const TaskContext = require("../../services/task_context.js");
+const cls = require("../../services/cls.js");
+const log = require("../../services/log.js");
/**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
@@ -142,6 +146,63 @@ class Branch extends AbstractEntity {
return !(this.branchId in this.becca.branches);
}
+ /**
+ * Delete a branch. If this is a last note's branch, delete the note as well.
+ *
+ * @param {string} [deleteId] - optional delete identified
+ * @param {TaskContext} [taskContext]
+ *
+ * @return {boolean} - true if note has been deleted, false otherwise
+ */
+ deleteBranch(deleteId, taskContext) {
+ if (!deleteId) {
+ deleteId = utils.randomString(10);
+ }
+
+ if (!taskContext) {
+ taskContext = new TaskContext('no-progress-reporting');
+ }
+
+ taskContext.increaseProgressCount();
+
+ if (this.branchId === 'root'
+ || this.noteId === 'root'
+ || this.noteId === cls.getHoistedNoteId()) {
+
+ throw new Error("Can't delete root or hoisted branch/note");
+ }
+
+ this.markAsDeleted(deleteId);
+
+ const note = this.getNote();
+ const notDeletedBranches = note.getParentBranches();
+
+ if (notDeletedBranches.length === 0) {
+ for (const childBranch of note.getChildBranches()) {
+ childBranch.deleteBranch(deleteId, taskContext);
+ }
+
+ // first delete children and then parent - this will show up better in recent changes
+
+ log.info("Deleting note " + note.noteId);
+
+ for (const attribute of note.getOwnedAttributes()) {
+ attribute.markAsDeleted(deleteId);
+ }
+
+ for (const relation of note.getTargetRelations()) {
+ relation.markAsDeleted(deleteId);
+ }
+
+ note.markAsDeleted(deleteId);
+
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
beforeSaving() {
if (this.notePosition === undefined || this.notePosition === null) {
// TODO finding new position can be refactored into becca
diff --git a/docs/backend_api/becca_entities_note.js.html b/docs/backend_api/becca_entities_note.js.html
index 2aae3ae51..ad2571123 100644
--- a/docs/backend_api/becca_entities_note.js.html
+++ b/docs/backend_api/becca_entities_note.js.html
@@ -36,6 +36,7 @@ const dateUtils = require('../../services/date_utils');
const entityChangesService = require('../../services/entity_changes');
const AbstractEntity = require("./abstract_entity");
const NoteRevision = require("./note_revision");
+const TaskContext = require("../../services/task_context.js");
const LABEL = 'label';
const RELATION = 'relation';
@@ -1153,6 +1154,26 @@ class Note extends AbstractEntity {
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
}
+ /**
+ * (Soft) delete a note and all its descendants.
+ *
+ * @param {string} [deleteId] - optional delete identified
+ * @param {TaskContext} [taskContext]
+ */
+ deleteNote(deleteId, taskContext) {
+ if (!deleteId) {
+ deleteId = utils.randomString(10);
+ }
+
+ if (!taskContext) {
+ taskContext = new TaskContext('no-progress-reporting');
+ }
+
+ for (const branch of this.getParentBranches()) {
+ branch.deleteBranch(deleteId, taskContext);
+ }
+ }
+
decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
diff --git a/package-lock.json b/package-lock.json
index 50926f6e1..0ea3bd1ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,7 +30,7 @@
"express-partial-content": "1.0.2",
"express-rate-limit": "6.3.0",
"express-session": "1.17.2",
- "fs-extra": "10.0.1",
+ "fs-extra": "10.1.0",
"helmet": "5.0.2",
"html": "1.0.0",
"html2plaintext": "2.1.4",
@@ -45,7 +45,7 @@
"jsdom": "19.0.0",
"mime-types": "2.1.35",
"multer": "1.4.4",
- "node-abi": "3.8.0",
+ "node-abi": "3.15.0",
"normalize-strings": "1.1.1",
"open": "8.4.0",
"portscanner": "2.2.0",
@@ -75,7 +75,7 @@
"cross-env": "7.0.3",
"electron": "16.2.1",
"electron-builder": "23.0.3",
- "electron-packager": "15.4.0",
+ "electron-packager": "15.5.0",
"electron-rebuild": "3.2.7",
"esm": "3.2.25",
"jasmine": "4.1.0",
@@ -4211,12 +4211,13 @@
}
},
"node_modules/electron-packager": {
- "version": "15.4.0",
- "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.4.0.tgz",
- "integrity": "sha512-JrrLcBP15KGrPj0cZ/ALKGmaQ4gJkn3mocf0E3bRKdR3kxKWYcDRpCvdhksYDXw/r3I6tMEcZ7XzyApWFXdVpw==",
+ "version": "15.5.0",
+ "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.5.0.tgz",
+ "integrity": "sha512-8mITLQgTm9xdrO8XL/PsK0EZGU7zK/ay7TI8M1C9pc1UZ++HlaWQJBRJHlOXf4TL/7FsiF4OciEhiqhMn+LKQQ==",
"dev": true,
"dependencies": {
"@electron/get": "^1.6.0",
+ "@electron/universal": "^1.2.1",
"asar": "^3.1.0",
"cross-spawn-windows-exe": "^1.2.0",
"debug": "^4.0.1",
@@ -4245,25 +4246,22 @@
"url": "https://github.com/electron/electron-packager?sponsor=1"
}
},
- "node_modules/electron-packager/node_modules/asar": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/asar/-/asar-3.1.0.tgz",
- "integrity": "sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==",
+ "node_modules/electron-packager/node_modules/@electron/universal": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz",
+ "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==",
"dev": true,
"dependencies": {
- "chromium-pickle-js": "^0.2.0",
- "commander": "^5.0.0",
- "glob": "^7.1.6",
- "minimatch": "^3.0.4"
- },
- "bin": {
- "asar": "bin/asar.js"
+ "@malept/cross-spawn-promise": "^1.1.0",
+ "asar": "^3.1.0",
+ "debug": "^4.3.1",
+ "dir-compare": "^2.4.0",
+ "fs-extra": "^9.0.1",
+ "minimatch": "^3.0.4",
+ "plist": "^3.0.4"
},
"engines": {
- "node": ">=10.12.0"
- },
- "optionalDependencies": {
- "@types/glob": "^7.1.1"
+ "node": ">=8.6"
}
},
"node_modules/electron-packager/node_modules/cross-spawn-windows-exe": {
@@ -5676,9 +5674,9 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs-extra": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz",
- "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -7785,9 +7783,9 @@
"dev": true
},
"node_modules/node-abi": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz",
- "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz",
+ "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==",
"dependencies": {
"semver": "^7.3.5"
},
@@ -14795,12 +14793,13 @@
}
},
"electron-packager": {
- "version": "15.4.0",
- "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.4.0.tgz",
- "integrity": "sha512-JrrLcBP15KGrPj0cZ/ALKGmaQ4gJkn3mocf0E3bRKdR3kxKWYcDRpCvdhksYDXw/r3I6tMEcZ7XzyApWFXdVpw==",
+ "version": "15.5.0",
+ "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-15.5.0.tgz",
+ "integrity": "sha512-8mITLQgTm9xdrO8XL/PsK0EZGU7zK/ay7TI8M1C9pc1UZ++HlaWQJBRJHlOXf4TL/7FsiF4OciEhiqhMn+LKQQ==",
"dev": true,
"requires": {
"@electron/get": "^1.6.0",
+ "@electron/universal": "^1.2.1",
"asar": "^3.1.0",
"cross-spawn-windows-exe": "^1.2.0",
"debug": "^4.0.1",
@@ -14820,17 +14819,19 @@
"yargs-parser": "^20.0.0"
},
"dependencies": {
- "asar": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/asar/-/asar-3.1.0.tgz",
- "integrity": "sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==",
+ "@electron/universal": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz",
+ "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==",
"dev": true,
"requires": {
- "@types/glob": "^7.1.1",
- "chromium-pickle-js": "^0.2.0",
- "commander": "^5.0.0",
- "glob": "^7.1.6",
- "minimatch": "^3.0.4"
+ "@malept/cross-spawn-promise": "^1.1.0",
+ "asar": "^3.1.0",
+ "debug": "^4.3.1",
+ "dir-compare": "^2.4.0",
+ "fs-extra": "^9.0.1",
+ "minimatch": "^3.0.4",
+ "plist": "^3.0.4"
}
},
"cross-spawn-windows-exe": {
@@ -15757,9 +15758,9 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz",
- "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -17411,9 +17412,9 @@
"dev": true
},
"node-abi": {
- "version": "3.8.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz",
- "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz",
+ "integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==",
"requires": {
"semver": "^7.3.5"
}
diff --git a/package.json b/package.json
index 35bb0f668..8e4220b73 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
- "version": "0.51.0-beta",
+ "version": "0.51.2",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -45,7 +45,7 @@
"express-partial-content": "1.0.2",
"express-rate-limit": "6.3.0",
"express-session": "1.17.2",
- "fs-extra": "10.0.1",
+ "fs-extra": "10.1.0",
"helmet": "5.0.2",
"html": "1.0.0",
"html2plaintext": "2.1.4",
@@ -60,7 +60,7 @@
"jsdom": "19.0.0",
"mime-types": "2.1.35",
"multer": "1.4.4",
- "node-abi": "3.8.0",
+ "node-abi": "3.15.0",
"normalize-strings": "1.1.1",
"open": "8.4.0",
"portscanner": "2.2.0",
@@ -87,7 +87,7 @@
"cross-env": "7.0.3",
"electron": "16.2.1",
"electron-builder": "23.0.3",
- "electron-packager": "15.4.0",
+ "electron-packager": "15.5.0",
"electron-rebuild": "3.2.7",
"esm": "3.2.25",
"jasmine": "4.1.0",
diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js
index b27e985f5..f2cebbaae 100644
--- a/src/becca/becca_service.js
+++ b/src/becca/becca_service.js
@@ -67,7 +67,7 @@ function getNoteTitle(childNoteId, parentNoteId) {
const parentNote = becca.notes[parentNoteId];
if (!childNote) {
- log.info(`Cannot find note in cache for noteId ${childNoteId}`);
+ log.info(`Cannot find note in cache for noteId '${childNoteId}'`);
return "[error fetching title]";
}
@@ -162,7 +162,7 @@ function getNotePath(noteId) {
const note = becca.notes[noteId];
if (!note) {
- console.trace(`Cannot find note ${noteId} in cache.`);
+ console.trace(`Cannot find note '${noteId}' in cache.`);
return;
}
diff --git a/src/becca/entities/abstract_entity.js b/src/becca/entities/abstract_entity.js
index 9981373c3..574c5bae5 100644
--- a/src/becca/entities/abstract_entity.js
+++ b/src/becca/entities/abstract_entity.js
@@ -111,6 +111,8 @@ class AbstractEntity {
/**
* Mark the entity as (soft) deleted. It will be completely erased later.
*
+ * This is a low level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
+ *
* @param [deleteId=null]
*/
markAsDeleted(deleteId = null) {
diff --git a/src/becca/entities/branch.js b/src/becca/entities/branch.js
index 10ce46a54..8df3febb8 100644
--- a/src/becca/entities/branch.js
+++ b/src/becca/entities/branch.js
@@ -4,6 +4,10 @@ const Note = require('./note');
const AbstractEntity = require("./abstract_entity");
const sql = require("../../services/sql");
const dateUtils = require("../../services/date_utils");
+const utils = require("../../services/utils.js");
+const TaskContext = require("../../services/task_context.js");
+const cls = require("../../services/cls.js");
+const log = require("../../services/log.js");
/**
* Branch represents a relationship between a child note and its parent note. Trilium allows a note to have multiple
@@ -114,6 +118,63 @@ class Branch extends AbstractEntity {
return !(this.branchId in this.becca.branches);
}
+ /**
+ * Delete a branch. If this is a last note's branch, delete the note as well.
+ *
+ * @param {string} [deleteId] - optional delete identified
+ * @param {TaskContext} [taskContext]
+ *
+ * @return {boolean} - true if note has been deleted, false otherwise
+ */
+ deleteBranch(deleteId, taskContext) {
+ if (!deleteId) {
+ deleteId = utils.randomString(10);
+ }
+
+ if (!taskContext) {
+ taskContext = new TaskContext('no-progress-reporting');
+ }
+
+ taskContext.increaseProgressCount();
+
+ if (this.branchId === 'root'
+ || this.noteId === 'root'
+ || this.noteId === cls.getHoistedNoteId()) {
+
+ throw new Error("Can't delete root or hoisted branch/note");
+ }
+
+ this.markAsDeleted(deleteId);
+
+ const note = this.getNote();
+ const notDeletedBranches = note.getParentBranches();
+
+ if (notDeletedBranches.length === 0) {
+ for (const childBranch of note.getChildBranches()) {
+ childBranch.deleteBranch(deleteId, taskContext);
+ }
+
+ // first delete children and then parent - this will show up better in recent changes
+
+ log.info("Deleting note " + note.noteId);
+
+ for (const attribute of note.getOwnedAttributes()) {
+ attribute.markAsDeleted(deleteId);
+ }
+
+ for (const relation of note.getTargetRelations()) {
+ relation.markAsDeleted(deleteId);
+ }
+
+ note.markAsDeleted(deleteId);
+
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
beforeSaving() {
if (this.notePosition === undefined || this.notePosition === null) {
// TODO finding new position can be refactored into becca
diff --git a/src/becca/entities/note.js b/src/becca/entities/note.js
index 5f23e2d15..595e41052 100644
--- a/src/becca/entities/note.js
+++ b/src/becca/entities/note.js
@@ -8,6 +8,7 @@ const dateUtils = require('../../services/date_utils');
const entityChangesService = require('../../services/entity_changes');
const AbstractEntity = require("./abstract_entity");
const NoteRevision = require("./note_revision");
+const TaskContext = require("../../services/task_context.js");
const LABEL = 'label';
const RELATION = 'relation';
@@ -237,7 +238,7 @@ class Note extends AbstractEntity {
setContent(content, ignoreMissingProtectedSession = false) {
if (content === null || content === undefined) {
- throw new Error(`Cannot set null content to note ${this.noteId}`);
+ throw new Error(`Cannot set null content to note '${this.noteId}'`);
}
if (this.isStringNote()) {
@@ -259,7 +260,7 @@ class Note extends AbstractEntity {
pojo.content = protectedSessionService.encrypt(pojo.content);
}
else if (!ignoreMissingProtectedSession) {
- throw new Error(`Cannot update content of noteId=${this.noteId} since we're out of protected session.`);
+ throw new Error(`Cannot update content of noteId '${this.noteId}' since we're out of protected session.`);
}
}
@@ -1125,6 +1126,26 @@ class Note extends AbstractEntity {
return cloningService.cloneNoteToBranch(this.noteId, branch.branchId);
}
+ /**
+ * (Soft) delete a note and all its descendants.
+ *
+ * @param {string} [deleteId] - optional delete identified
+ * @param {TaskContext} [taskContext]
+ */
+ deleteNote(deleteId, taskContext) {
+ if (!deleteId) {
+ deleteId = utils.randomString(10);
+ }
+
+ if (!taskContext) {
+ taskContext = new TaskContext('no-progress-reporting');
+ }
+
+ for (const branch of this.getParentBranches()) {
+ branch.deleteBranch(deleteId, taskContext);
+ }
+ }
+
decrypt() {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
diff --git a/src/etapi/branches.js b/src/etapi/branches.js
index 71117e339..be6d9f516 100644
--- a/src/etapi/branches.js
+++ b/src/etapi/branches.js
@@ -71,7 +71,7 @@ function register(router) {
return res.sendStatus(204);
}
- noteService.deleteBranch(branch, null, new TaskContext('no-progress-reporting'));
+ branch.deleteBranch();
res.sendStatus(204);
});
diff --git a/src/etapi/notes.js b/src/etapi/notes.js
index b35c296ff..788b0f4ff 100644
--- a/src/etapi/notes.js
+++ b/src/etapi/notes.js
@@ -98,7 +98,7 @@ function register(router) {
return res.sendStatus(204);
}
- noteService.deleteNote(note, null, new TaskContext('no-progress-reporting'));
+ note.deleteNote(null, new TaskContext('no-progress-reporting'));
res.sendStatus(204);
});
diff --git a/src/public/app/entities/note_short.js b/src/public/app/entities/note_short.js
index e8bb1de42..3ea507266 100644
--- a/src/public/app/entities/note_short.js
+++ b/src/public/app/entities/note_short.js
@@ -125,7 +125,7 @@ class NoteShort {
return JSON.parse(content);
}
catch (e) {
- console.log(`Cannot parse content of note ${this.noteId}: `, e.message);
+ console.log(`Cannot parse content of note '${this.noteId}': `, e.message);
return null;
}
diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js
index abc59d7e0..91af4f1b2 100644
--- a/src/public/app/services/froca.js
+++ b/src/public/app/services/froca.js
@@ -179,7 +179,7 @@ class Froca {
const searchResultNoteIds = await server.get('search-note/' + note.noteId);
if (!Array.isArray(searchResultNoteIds)) {
- throw new Error(`Search note ${note.noteId} failed: ${searchResultNoteIds}`);
+ throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`);
}
// reset all the virtual branches from old search results
@@ -254,7 +254,7 @@ class Froca {
return null;
}
else if (!noteId) {
- console.trace(`Falsy noteId ${noteId}, returning null.`);
+ console.trace(`Falsy noteId '${noteId}', returning null.`);
return null;
}
@@ -312,7 +312,7 @@ class Froca {
if (!this.noteComplementPromises[noteId]) {
this.noteComplementPromises[noteId] = server.get('notes/' + noteId)
.then(row => new NoteComplement(row))
- .catch(e => console.error(`Cannot get note complement for note ${noteId}`));
+ .catch(e => console.error(`Cannot get note complement for note '${noteId}'`));
// we don't want to keep large payloads forever in memory so we clean that up quite quickly
// this cache is more meant to share the data between different components within one business transaction (e.g. loading of the note into the tab context and all the components)
diff --git a/src/public/app/widgets/buttons/button_widget.js b/src/public/app/widgets/buttons/button_widget.js
index d925f2357..92f4d701d 100644
--- a/src/public/app/widgets/buttons/button_widget.js
+++ b/src/public/app/widgets/buttons/button_widget.js
@@ -71,7 +71,6 @@ export default class ButtonWidget extends NoteContextAwareWidget {
}
this.$widget
- .attr("title", this.settings.title)
.addClass(this.settings.icon);
}
diff --git a/src/public/app/widgets/buttons/edit_button.js b/src/public/app/widgets/buttons/edit_button.js
index af70cc6aa..9b458fcca 100644
--- a/src/public/app/widgets/buttons/edit_button.js
+++ b/src/public/app/widgets/buttons/edit_button.js
@@ -1,5 +1,7 @@
import ButtonWidget from "./button_widget.js";
import appContext from "../../services/app_context.js";
+import attributeService from "../../services/attributes.js";
+import protectedSessionHolder from "../../services/protected_session_holder.js";
export default class EditButton extends ButtonWidget {
isEnabled() {
@@ -22,9 +24,29 @@ export default class EditButton extends ButtonWidget {
}
async refreshWithNote(note) {
- // can't do this in isEnabled() since isReadOnly is async
- this.toggleInt(await this.noteContext.isReadOnly());
+ if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
+ this.toggleInt(false);
+ }
+ else {
+ // prevent flickering by assuming hidden before async operation
+ this.toggleInt(false);
+
+ // can't do this in isEnabled() since isReadOnly is async
+ this.toggleInt(await this.noteContext.isReadOnly());
+ }
await super.refreshWithNote(note);
}
+
+ entitiesReloadedEvent({loadResults}) {
+ if (loadResults.getAttributes().find(
+ attr => attr.type === 'label'
+ && attr.name.toLowerCase().includes("readonly")
+ && attributeService.isAffecting(attr, this.note)
+ )) {
+ this.noteContext.readOnlyTemporarilyDisabled = false;
+
+ this.refresh();
+ }
+ }
}
diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js
index 4c3ddd3a4..f6de7bda5 100644
--- a/src/public/app/widgets/note_detail.js
+++ b/src/public/app/widgets/note_detail.js
@@ -154,7 +154,8 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
// https://github.com/zadam/trilium/issues/2522
this.$widget.toggleClass("full-height",
!this.noteContext.hasNoteList()
- && ['editable-text', 'editable-code', 'canvas-note'].includes(this.type));
+ && ['editable-text', 'editable-code', 'canvas-note'].includes(this.type)
+ && this.mime !== 'text/x-sqlite;schema=trilium');
}
getTypeWidget() {
diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js
index e82ba6d68..d497ecbbe 100644
--- a/src/public/app/widgets/type_widgets/editable_text.js
+++ b/src/public/app/widgets/type_widgets/editable_text.js
@@ -191,7 +191,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
await this.initialized;
this.textEditor.model.change(writer => {
- const insertPosition = this.textEditor.model.document.selection.getFirstPosition();
+ const insertPosition = this.textEditor.model.document.selection.getLastPosition();
writer.insertText(text, insertPosition);
});
}
diff --git a/src/public/app/widgets/type_widgets/type_widget.js b/src/public/app/widgets/type_widgets/type_widget.js
index 7123b51db..faa1ff98a 100644
--- a/src/public/app/widgets/type_widgets/type_widget.js
+++ b/src/public/app/widgets/type_widgets/type_widget.js
@@ -1,4 +1,5 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
+import appContext from "../../services/app_context.js";
export default class TypeWidget extends NoteContextAwareWidget {
// for overriding
@@ -34,7 +35,7 @@ export default class TypeWidget extends NoteContextAwareWidget {
}
isActive() {
- return this.$widget.is(":visible");
+ return this.$widget.is(":visible") && this.noteContext?.ntxId === appContext.tabManager.activeNtxId;
}
/**
diff --git a/src/routes/api/branches.js b/src/routes/api/branches.js
index f05a6e0c3..3af5fe8ed 100644
--- a/src/routes/api/branches.js
+++ b/src/routes/api/branches.js
@@ -194,7 +194,7 @@ function deleteBranch(req) {
const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes');
const deleteId = utils.randomString(10);
- const noteDeleted = noteService.deleteBranch(branch, deleteId, taskContext);
+ const noteDeleted = branch.deleteBranch(deleteId, taskContext);
if (eraseNotes) {
noteService.eraseNotesWithDeleteId(deleteId);
diff --git a/src/routes/api/clipper.js b/src/routes/api/clipper.js
index d777b9bb9..b8bb158e7 100644
--- a/src/routes/api/clipper.js
+++ b/src/routes/api/clipper.js
@@ -138,7 +138,7 @@ function processContent(images, note, content) {
value: imageNote.noteId
}).save();
- log.info(`Replacing ${imageId} with ${url} in note ${note.noteId}`);
+ log.info(`Replacing '${imageId}' with '${url}' in note '${note.noteId}'`);
rewrittenContent = utils.replaceAll(rewrittenContent, imageId, url);
}
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index f602189e2..5f334edc8 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -73,7 +73,7 @@ function deleteNote(req) {
const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
- noteService.deleteNote(note, deleteId, taskContext);
+ note.deleteNote(deleteId, taskContext);
if (eraseNotes) {
noteService.eraseNotesWithDeleteId(deleteId);
@@ -96,7 +96,7 @@ function sortChildNotes(req) {
const noteId = req.params.noteId;
const {sortBy, sortDirection} = req.body;
- log.info(`Sorting ${noteId} children with ${sortBy} ${sortDirection}`);
+ log.info(`Sorting '${noteId}' children with ${sortBy} ${sortDirection}`);
const reverse = sortDirection === 'desc';
@@ -196,11 +196,11 @@ function changeTitle(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} has not been found`];
+ return [404, `Note '${noteId}' has not been found`];
}
if (!note.isContentAvailable()) {
- return [400, `Note ${noteId} is not available for change`];
+ return [400, `Note '${noteId}' is not available for change`];
}
const noteTitleChanged = note.title !== title;
@@ -289,10 +289,10 @@ function uploadModifiedFile(req) {
const note = becca.getNote(noteId);
if (!note) {
- return [404, `Note ${noteId} has not been found`];
+ return [404, `Note '${noteId}' has not been found`];
}
- log.info(`Updating note ${noteId} with content from ${filePath}`);
+ log.info(`Updating note '${noteId}' with content from ${filePath}`);
noteRevisionService.createNoteRevision(note);
diff --git a/src/services/build.js b/src/services/build.js
index f5dc88301..6717d2293 100644
--- a/src/services/build.js
+++ b/src/services/build.js
@@ -1 +1 @@
-module.exports = { buildDate:"2022-04-10T14:13:51+02:00", buildRevision: "a04becc4ec653e21c2c80aa9d9ef5b7c9a8e1aa8" };
+module.exports = { buildDate:"2022-05-01T23:18:35+02:00", buildRevision: "b3763eed610fa3f2aabbcbdbd21efca704a5dd08" };
diff --git a/src/services/cloning.js b/src/services/cloning.js
index 6fbd3adf1..d929a1cbf 100644
--- a/src/services/cloning.js
+++ b/src/services/cloning.js
@@ -89,8 +89,7 @@ function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
throw new Error(`Cannot remove branch ${branch.branchId} between child ${noteId} and parent ${parentNoteId} because this would delete the note as well.`);
}
- const deleteId = utils.randomString(10);
- noteService.deleteBranch(branch, deleteId, new TaskContext());
+ branch.deleteBranch();
log.info(`Ensured note ${noteId} is NOT in parent note ${parentNoteId}`);
}
diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js
index 267c9fec3..dbf4b4478 100644
--- a/src/services/consistency_checks.js
+++ b/src/services/consistency_checks.js
@@ -56,41 +56,66 @@ class ConsistencyChecks {
childToParents[childNoteId].push(parentNoteId);
}
+ /** @returns {boolean} true if cycle was found and we should try again */
const checkTreeCycle = (noteId, path) => {
if (noteId === 'root') {
- return;
- }
-
- if (!childToParents[noteId] || childToParents[noteId].length === 0) {
- logError(`No parents found for note ${noteId}`);
-
- this.unrecoveredConsistencyErrors = true;
- return;
+ return false;
}
for (const parentNoteId of childToParents[noteId]) {
if (path.includes(parentNoteId)) {
- logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
+ if (this.autoFix) {
+ const branch = becca.getBranchFromChildAndParent(noteId, parentNoteId);
+ branch.markAsDeleted('cycle-autofix');
+ logFix(`Branch '${branch.branchId}' between child '${noteId}' and parent '${parentNoteId}' has been deleted since it was causing a tree cycle.`);
- this.unrecoveredConsistencyErrors = true;
+ return true;
+ }
+ else {
+ logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`);
+
+ this.unrecoveredConsistencyErrors = true;
+ }
} else {
const newPath = path.slice();
newPath.push(noteId);
- checkTreeCycle(parentNoteId, newPath);
+ const retryNeeded = checkTreeCycle(parentNoteId, newPath);
+
+ if (retryNeeded) {
+ return true;
+ }
}
}
+
+ return false;
};
const noteIds = Object.keys(childToParents);
for (const noteId of noteIds) {
- checkTreeCycle(noteId, []);
+ const retryNeeded = checkTreeCycle(noteId, []);
+
+ if (retryNeeded) {
+ return true;
+ }
}
- if (childToParents['root'].length !== 1 || childToParents['root'][0] !== 'none') {
- logError('Incorrect root parent: ' + JSON.stringify(childToParents['root']));
- this.unrecoveredConsistencyErrors = true;
+ return false;
+ }
+
+ checkAndRepairTreeCycles() {
+ let treeFixed = false;
+
+ while (this.checkTreeCycles()) {
+ // fixing cycle means deleting branches, we might need to create a new branch to recover the note
+ this.findExistencyIssues();
+
+ treeFixed = true;
+ }
+
+ if (treeFixed) {
+ this.reloadNeeded = true;
}
}
@@ -646,7 +671,7 @@ class ConsistencyChecks {
if (!this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency
- this.checkTreeCycles();
+ this.checkAndRepairTreeCycles();
}
if (this.reloadNeeded) {
diff --git a/src/services/notes.js b/src/services/notes.js
index 4d62ae274..41ad60d09 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -17,6 +17,7 @@ const becca = require('../becca/becca');
const Branch = require('../becca/entities/branch');
const Note = require('../becca/entities/note');
const Attribute = require('../becca/entities/attribute');
+const TaskContext = require("./task_context.js");
function getNewNotePosition(parentNoteId) {
const note = becca.notes[parentNoteId];
@@ -138,7 +139,7 @@ function createNewNote(params) {
triggerNoteTitleChanged(note);
triggerChildNoteCreated(note, parentNote);
- log.info(`Created new note ${note.noteId}, branch ${branch.branchId} of type ${note.type}, mime ${note.mime}`);
+ log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`);
return {
note,
@@ -284,10 +285,10 @@ async function downloadImage(noteId, imageUrl) {
imageUrlToNoteIdMapping[imageUrl] = note.noteId;
- log.info(`Download of ${imageUrl} succeeded and was saved as image note ${note.noteId}`);
+ log.info(`Download of '${imageUrl}' succeeded and was saved as image note '${note.noteId}'`);
}
catch (e) {
- log.error(`Download of ${imageUrl} for note ${noteId} failed with error: ${e.message} ${e.stack}`);
+ log.error(`Download of '${imageUrl}' for note '${noteId}' failed with error: ${e.message} ${e.stack}`);
}
}
@@ -372,7 +373,7 @@ function downloadImages(noteId, content) {
const origNote = becca.getNote(noteId);
if (!origNote) {
- log.error(`Cannot find note ${noteId} to replace image link.`);
+ log.error(`Cannot find note '${noteId}' to replace image link.`);
return;
}
@@ -393,7 +394,7 @@ function downloadImages(noteId, content) {
scanForLinks(origNote);
- console.log(`Fixed the image links for note ${noteId} to the offline saved.`);
+ console.log(`Fixed the image links for note '${noteId}' to the offline saved.`);
}
});
}, 5000);
@@ -490,7 +491,7 @@ function updateNote(noteId, noteUpdates) {
const note = becca.getNote(noteId);
if (!note.isContentAvailable()) {
- throw new Error(`Note ${noteId} is not available for change!`);
+ throw new Error(`Note '${noteId}' is not available for change!`);
}
saveNoteRevision(note);
@@ -524,69 +525,6 @@ function updateNote(noteId, noteUpdates) {
};
}
-/**
- * @param {Branch} branch
- * @param {string|null} deleteId
- * @param {TaskContext} taskContext
- *
- * @return {boolean} - true if note has been deleted, false otherwise
- */
-function deleteBranch(branch, deleteId, taskContext) {
- taskContext.increaseProgressCount();
-
- if (!branch) {
- return false;
- }
-
- if (branch.branchId === 'root'
- || branch.noteId === 'root'
- || branch.noteId === cls.getHoistedNoteId()) {
-
- throw new Error("Can't delete root or hoisted branch/note");
- }
-
- branch.markAsDeleted(deleteId);
-
- const note = branch.getNote();
- const notDeletedBranches = note.getParentBranches();
-
- if (notDeletedBranches.length === 0) {
- for (const childBranch of note.getChildBranches()) {
- deleteBranch(childBranch, deleteId, taskContext);
- }
-
- // first delete children and then parent - this will show up better in recent changes
-
- log.info("Deleting note " + note.noteId);
-
- for (const attribute of note.getOwnedAttributes()) {
- attribute.markAsDeleted(deleteId);
- }
-
- for (const relation of note.getTargetRelations()) {
- relation.markAsDeleted(deleteId);
- }
-
- note.markAsDeleted(deleteId);
-
- return true;
- }
- else {
- return false;
- }
-}
-
-/**
- * @param {Note} note
- * @param {string|null} deleteId
- * @param {TaskContext} taskContext
- */
-function deleteNote(note, deleteId, taskContext) {
- for (const branch of note.getParentBranches()) {
- deleteBranch(branch, deleteId, taskContext);
- }
-}
-
/**
* @param {string} noteId
* @param {TaskContext} taskContext
@@ -595,7 +533,7 @@ function undeleteNote(noteId, taskContext) {
const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!note.isDeleted) {
- log.error(`Note ${noteId} is not deleted and thus cannot be undeleted.`);
+ log.error(`Note '${noteId}' is not deleted and thus cannot be undeleted.`);
return;
}
@@ -938,8 +876,6 @@ module.exports = {
createNewNote,
createNewNoteWithTarget,
updateNote,
- deleteBranch,
- deleteNote,
undeleteNote,
protectNoteRecursively,
scanForLinks,
diff --git a/src/services/search/expressions/note_content_protected_fulltext.js b/src/services/search/expressions/note_content_protected_fulltext.js
index b24d3801a..05130e087 100644
--- a/src/services/search/expressions/note_content_protected_fulltext.js
+++ b/src/services/search/expressions/note_content_protected_fulltext.js
@@ -10,7 +10,7 @@ const utils = require("../../utils");
// FIXME: create common subclass with NoteContentUnprotectedFulltextExp to avoid duplication
class NoteContentProtectedFulltextExp extends Expression {
- constructor(operator, tokens, raw) {
+ constructor(operator, {tokens, raw, flatText}) {
super();
if (operator !== '*=*') {
@@ -19,6 +19,7 @@ class NoteContentProtectedFulltextExp extends Expression {
this.tokens = tokens;
this.raw = !!raw;
+ this.flatText = !!flatText;
}
execute(inputNoteSet) {
@@ -33,7 +34,7 @@ class NoteContentProtectedFulltextExp extends Expression {
for (let {noteId, type, mime, content} of sql.iterateRows(`
SELECT noteId, type, mime, content
FROM notes JOIN note_contents USING (noteId)
- WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) {
+ WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 1`)) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
continue;
@@ -49,7 +50,17 @@ class NoteContentProtectedFulltextExp extends Expression {
content = this.preprocessContent(content, type, mime);
- if (!this.tokens.find(token => !content.includes(token))) {
+ const nonMatchingToken = this.tokens.find(token =>
+ !content.includes(token) &&
+ (
+ // in case of default fulltext search we should consider both title, attrs and content
+ // so e.g. "hello world" should match when "hello" is in title and "world" in content
+ !this.flatText
+ || !becca.notes[noteId].getFlatText().includes(token)
+ )
+ );
+
+ if (!nonMatchingToken) {
resultNoteSet.add(becca.notes[noteId]);
}
}
diff --git a/src/services/search/expressions/note_content_unprotected_fulltext.js b/src/services/search/expressions/note_content_unprotected_fulltext.js
index 60df92570..7abbd0d78 100644
--- a/src/services/search/expressions/note_content_unprotected_fulltext.js
+++ b/src/services/search/expressions/note_content_unprotected_fulltext.js
@@ -8,7 +8,7 @@ const utils = require("../../utils");
// FIXME: create common subclass with NoteContentProtectedFulltextExp to avoid duplication
class NoteContentUnprotectedFulltextExp extends Expression {
- constructor(operator, tokens, raw) {
+ constructor(operator, {tokens, raw, flatText}) {
super();
if (operator !== '*=*') {
@@ -17,6 +17,7 @@ class NoteContentUnprotectedFulltextExp extends Expression {
this.tokens = tokens;
this.raw = !!raw;
+ this.flatText = !!flatText;
}
execute(inputNoteSet) {
@@ -27,7 +28,7 @@ class NoteContentUnprotectedFulltextExp extends Expression {
for (let {noteId, type, mime, content} of sql.iterateRows(`
SELECT noteId, type, mime, content
FROM notes JOIN note_contents USING (noteId)
- WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) {
+ WHERE type IN ('text', 'code', 'mermaid') AND isDeleted = 0 AND isProtected = 0`)) {
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in becca.notes)) {
continue;
@@ -35,7 +36,17 @@ class NoteContentUnprotectedFulltextExp extends Expression {
content = this.preprocessContent(content, type, mime);
- if (!this.tokens.find(token => !content.includes(token))) {
+ const nonMatchingToken = this.tokens.find(token =>
+ !content.includes(token) &&
+ (
+ // in case of default fulltext search we should consider both title, attrs and content
+ // so e.g. "hello world" should match when "hello" is in title and "world" in content
+ !this.flatText
+ || !becca.notes[noteId].getFlatText().includes(token)
+ )
+ );
+
+ if (!nonMatchingToken) {
resultNoteSet.add(becca.notes[noteId]);
}
}
diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js
index 02d2cc3bb..9ba5ce506 100644
--- a/src/services/search/services/parse.js
+++ b/src/services/search/services/parse.js
@@ -32,8 +32,8 @@ function getFulltext(tokens, searchContext) {
if (!searchContext.fastSearch) {
return new OrExp([
new NoteFlatTextExp(tokens),
- new NoteContentProtectedFulltextExp('*=*', tokens),
- new NoteContentUnprotectedFulltextExp('*=*', tokens)
+ new NoteContentProtectedFulltextExp('*=*', {tokens, flatText: true}),
+ new NoteContentUnprotectedFulltextExp('*=*', {tokens, flatText: true})
]);
}
else {
@@ -141,8 +141,8 @@ function getExpression(tokens, searchContext, level = 0) {
i++;
return new OrExp([
- new NoteContentUnprotectedFulltextExp(operator, [tokens[i].token], raw),
- new NoteContentProtectedFulltextExp(operator, [tokens[i].token], raw)
+ new NoteContentUnprotectedFulltextExp(operator, {tokens: [tokens[i].token], raw }),
+ new NoteContentProtectedFulltextExp(operator, {tokens: [tokens[i].token], raw })
]);
}
@@ -196,8 +196,8 @@ function getExpression(tokens, searchContext, level = 0) {
return new OrExp([
new PropertyComparisonExp(searchContext, 'title', '*=*', tokens[i].token),
- new NoteContentProtectedFulltextExp('*=*', [tokens[i].token]),
- new NoteContentUnprotectedFulltextExp('*=*', [tokens[i].token])
+ new NoteContentProtectedFulltextExp('*=*', {tokens: [tokens[i].token]}),
+ new NoteContentUnprotectedFulltextExp('*=*', {tokens: [tokens[i].token]})
]);
}
diff --git a/src/share/routes.js b/src/share/routes.js
index b8678c053..44d1cd26d 100644
--- a/src/share/routes.js
+++ b/src/share/routes.js
@@ -62,21 +62,23 @@ function register(router) {
});
router.get('/share/:shareId', (req, res, next) => {
- const {shareId} = req.params;
-
shacaLoader.ensureLoad();
+ const {shareId} = req.params;
+
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
renderNote(note, res);
});
router.get('/share/api/notes/:noteId', (req, res, next) => {
+ shacaLoader.ensureLoad();
+
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
- return res.status(404).send(`Note ${noteId} not found`);
+ return res.status(404).send(`Note '${noteId}' not found`);
}
addNoIndexHeader(note, res);
@@ -85,11 +87,13 @@ function register(router) {
});
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
+ shacaLoader.ensureLoad();
+
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
- return res.status(404).send(`Note ${noteId} not found`);
+ return res.status(404).send(`Note '${noteId}' not found`);
}
addNoIndexHeader(note, res);
@@ -106,11 +110,13 @@ function register(router) {
res.send(note.getContent());
});
- router.get(['/share/api/images/:noteId/:filename', '/share/api/images/:noteId'], (req, res, next) => {
+ router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
+ shacaLoader.ensureLoad();
+
const image = shaca.getNote(req.params.noteId);
if (!image) {
- return res.status(404).send(`Note ${noteId} not found`);
+ return res.status(404).send(`Note '${req.params.noteId}' not found`);
}
else if (!["image", "canvas-note"].includes(image.type)) {
return res.status(400).send("Requested note is not a shareable image");
@@ -123,6 +129,7 @@ function register(router) {
try {
const data = JSON.parse(content)
const svg = data.svg || ' '
+ addNoIndexHeader(image, res);
res.set('Content-Type', "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(svg);
@@ -132,19 +139,20 @@ function register(router) {
} else {
// normal image
res.set('Content-Type', image.mime);
-
+ addNoIndexHeader(image, res);
res.send(image.getContent());
}
-
});
// used for PDF viewing
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
+ shacaLoader.ensureLoad();
+
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
- return res.status(404).send(`Note ${noteId} not found`);
+ return res.status(404).send(`Note '${noteId}' not found`);
}
addNoIndexHeader(note, res);