diff --git a/docs/backend_api/AbstractBeccaEntity.html b/docs/backend_api/AbstractBeccaEntity.html
index e13c799bc..328d08f0c 100644
--- a/docs/backend_api/AbstractBeccaEntity.html
+++ b/docs/backend_api/AbstractBeccaEntity.html
@@ -991,7 +991,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BAttribute.html b/docs/backend_api/BAttribute.html
index 8f6af88c6..29eb98f90 100644
--- a/docs/backend_api/BAttribute.html
+++ b/docs/backend_api/BAttribute.html
@@ -1904,7 +1904,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BBranch.html b/docs/backend_api/BBranch.html
index ae6e4e014..f75738ee7 100644
--- a/docs/backend_api/BBranch.html
+++ b/docs/backend_api/BBranch.html
@@ -1916,7 +1916,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BEtapiToken.html b/docs/backend_api/BEtapiToken.html
index 1fab94869..2c6eb8218 100644
--- a/docs/backend_api/BEtapiToken.html
+++ b/docs/backend_api/BEtapiToken.html
@@ -1461,7 +1461,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BNote.html b/docs/backend_api/BNote.html
index d38f35c0f..b1d3e2b60 100644
--- a/docs/backend_api/BNote.html
+++ b/docs/backend_api/BNote.html
@@ -1318,7 +1318,7 @@ See addLabel, addRelation for more specific methods.
Source:
@@ -1654,7 +1654,7 @@ See addLabel, addRelation for more specific methods.
Source:
@@ -1900,7 +1900,7 @@ returned.
Source:
@@ -2135,7 +2135,7 @@ returned.
Source:
@@ -2335,7 +2335,7 @@ returned.
Source:
@@ -2556,6 +2556,10 @@ returned.
+
+ Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
+
+
@@ -2597,7 +2601,7 @@ returned.
Source:
@@ -2703,7 +2707,7 @@ returned.
Source:
@@ -2877,7 +2881,7 @@ returned.
Source:
@@ -3055,7 +3059,7 @@ returned.
Source:
@@ -3316,6 +3320,364 @@ returned.
+ getBestNotePath(hoistedNoteIdopt ) → {Array.<string>}
+
+
+
+
+
+
+
+ Returns note path considered to be the "best"
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+ Default
+
+
+ Description
+
+
+
+
+
+
+
+
+ hoistedNoteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ 'root'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ array of noteIds constituting the particular note path
+
+
+
+
+
+
+ Type
+
+
+
+Array.<string>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getBestNotePathString(hoistedNoteIdopt ) → {string}
+
+
+
+
+
+
+
+ Returns note path considered to be the "best"
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+ Default
+
+
+ Description
+
+
+
+
+
+
+
+
+ hoistedNoteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ 'root'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ serialized note path (e.g. 'root/a1h315/js725h')
+
+
+
+
+
+
+ Type
+
+
+
+string
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
getBranches() → {Array.<BBranch >}
@@ -3878,7 +4240,7 @@ returned.
Source:
@@ -3968,7 +4330,7 @@ returned.
Source:
@@ -4074,7 +4436,7 @@ returned.
Source:
@@ -4332,7 +4694,7 @@ returned.
Source:
@@ -4490,7 +4852,7 @@ returned.
Source:
@@ -4660,7 +5022,7 @@ returned.
Source:
@@ -4827,7 +5189,7 @@ returned.
Source:
@@ -4933,7 +5295,7 @@ returned.
Source:
@@ -5035,7 +5397,7 @@ returned.
Source:
@@ -5215,7 +5577,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5480,7 +5842,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5635,7 +5997,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5793,7 +6155,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -5963,7 +6325,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6130,7 +6492,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6285,7 +6647,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6443,7 +6805,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -6613,7 +6975,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7061,7 +7423,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7219,7 +7581,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7389,7 +7751,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7604,7 +7966,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7658,6 +8020,177 @@ This method can be significantly faster than the getAttribute()
+ getSortedNotePathRecords(hoistedNoteIdopt ) → {Array.<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array.<string>, isHidden: boolean}>}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+ Default
+
+
+ Description
+
+
+
+
+
+
+
+
+ hoistedNoteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ 'root'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+
+
+
+ Type
+
+
+
+Array.<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array.<string>, isHidden: boolean}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
getStrongParentBranches() → {Array.<BBranch >}
@@ -7812,7 +8345,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -7914,7 +8447,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8020,7 +8553,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8211,7 +8744,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -8962,7 +9495,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9160,7 +9693,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9358,7 +9891,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9556,7 +10089,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9706,7 +10239,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -9812,7 +10345,7 @@ This method can be significantly faster than the getAttribute()
Source:
@@ -10282,6 +10815,161 @@ This method can be significantly faster than the getAttribute()
+ isLabelTruthy(name) → {boolean}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ name
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+
+ label name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ true if label exists (including inherited) and does not have "false" value.
+
+
+
+
+
+
+ Type
+
+
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
isRoot() → {boolean}
@@ -10828,7 +11516,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -11008,7 +11696,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -11188,7 +11876,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -11383,7 +12071,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -11615,7 +12303,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -11795,7 +12483,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -11955,7 +12643,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -12197,7 +12885,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -12408,7 +13096,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -12619,7 +13307,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
Source:
@@ -12671,7 +13359,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BNoteRevision.html b/docs/backend_api/BNoteRevision.html
index 0951fa14d..3a9ccf0d3 100644
--- a/docs/backend_api/BNoteRevision.html
+++ b/docs/backend_api/BNoteRevision.html
@@ -2174,7 +2174,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BOption.html b/docs/backend_api/BOption.html
index b5990cb92..849051190 100644
--- a/docs/backend_api/BOption.html
+++ b/docs/backend_api/BOption.html
@@ -267,7 +267,7 @@
Source:
@@ -335,7 +335,7 @@
Source:
@@ -403,7 +403,7 @@
Source:
@@ -471,7 +471,7 @@
Source:
@@ -1319,7 +1319,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BRecentNote.html b/docs/backend_api/BRecentNote.html
index b36b38298..cdda77c51 100644
--- a/docs/backend_api/BRecentNote.html
+++ b/docs/backend_api/BRecentNote.html
@@ -1251,7 +1251,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
diff --git a/docs/backend_api/BackendScriptApi.html b/docs/backend_api/BackendScriptApi.html
index 12800f161..ba6ffbc2b 100644
--- a/docs/backend_api/BackendScriptApi.html
+++ b/docs/backend_api/BackendScriptApi.html
@@ -3254,7 +3254,7 @@ JSON MIME type. See also createNewNote() for more options.
- ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) → {void}
+ ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) → {Object}
@@ -3262,7 +3262,7 @@ JSON MIME type. See also createNewNote() for more options.
- If there's no branch between note and parent note, create one. Otherwise, do nothing.
+ If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch.
@@ -3437,7 +3437,7 @@ JSON MIME type. See also createNewNote() for more options.
-void
+Object
@@ -7889,7 +7889,7 @@ exists, then we'll use that transaction.
diff --git a/docs/backend_api/becca_entities_abstract_becca_entity.js.html b/docs/backend_api/becca_entities_abstract_becca_entity.js.html
index 4036de3f6..b64387b2d 100644
--- a/docs/backend_api/becca_entities_abstract_becca_entity.js.html
+++ b/docs/backend_api/becca_entities_abstract_becca_entity.js.html
@@ -212,7 +212,7 @@ module.exports = AbstractBeccaEntity;
diff --git a/docs/backend_api/becca_entities_battribute.js.html b/docs/backend_api/becca_entities_battribute.js.html
index 83fff96e5..d9715c60b 100644
--- a/docs/backend_api/becca_entities_battribute.js.html
+++ b/docs/backend_api/becca_entities_battribute.js.html
@@ -124,7 +124,7 @@ class BAttribute extends AbstractBeccaEntity {
}
if (this.type === 'relation' && !(this.value in this.becca.notes)) {
- throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it target not existing note '${this.value}'.`);
+ throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
}
}
@@ -276,7 +276,7 @@ module.exports = BAttribute;
diff --git a/docs/backend_api/becca_entities_bbranch.js.html b/docs/backend_api/becca_entities_bbranch.js.html
index b4fa7f567..b2261711d 100644
--- a/docs/backend_api/becca_entities_bbranch.js.html
+++ b/docs/backend_api/becca_entities_bbranch.js.html
@@ -319,7 +319,7 @@ module.exports = BBranch;
diff --git a/docs/backend_api/becca_entities_betapi_token.js.html b/docs/backend_api/becca_entities_betapi_token.js.html
index 92ab19328..329843522 100644
--- a/docs/backend_api/becca_entities_betapi_token.js.html
+++ b/docs/backend_api/becca_entities_betapi_token.js.html
@@ -120,7 +120,7 @@ module.exports = BEtapiToken;
diff --git a/docs/backend_api/becca_entities_bnote.js.html b/docs/backend_api/becca_entities_bnote.js.html
index 97889071a..0c729d838 100644
--- a/docs/backend_api/becca_entities_bnote.js.html
+++ b/docs/backend_api/becca_entities_bnote.js.html
@@ -125,7 +125,7 @@ class BNote extends AbstractBeccaEntity {
* @private */
this.parents = [];
/** @type {BNote[]}
- * @private*/
+ * @private */
this.children = [];
/** @type {BAttribute[]}
* @private */
@@ -135,11 +135,11 @@ class BNote extends AbstractBeccaEntity {
* @private */
this.__attributeCache = null;
/** @type {BAttribute[]|null}
- * @private*/
+ * @private */
this.inheritableAttributeCache = null;
/** @type {BAttribute[]}
- * @private*/
+ * @private */
this.targetRelations = [];
this.becca.addNote(this.noteId, this);
@@ -560,6 +560,20 @@ class BNote extends AbstractBeccaEntity {
*/
hasLabel(name, value) { return this.hasAttribute(LABEL, name, value); }
+ /**
+ * @param {string} name - label name
+ * @returns {boolean} true if label exists (including inherited) and does not have "false" value.
+ */
+ isLabelTruthy(name) {
+ const label = this.getLabel(name);
+
+ if (!label) {
+ return false;
+ }
+
+ return label && label.value !== 'false';
+ }
+
/**
* @param {string} name - label name
* @param {string} [value] - label value
@@ -761,6 +775,21 @@ class BNote extends AbstractBeccaEntity {
return this.hasAttribute('label', 'archived');
}
+ areAllNotePathsArchived() {
+ // there's a slight difference between note being itself archived and all its note paths being archived
+ // - note is archived when it itself has an archived label or inherits it
+ // - note does not have or inherit archived label, but each note paths contains a note with (non-inheritable)
+ // archived label
+
+ const bestNotePathRecord = this.getSortedNotePathRecords()[0];
+
+ if (!bestNotePathRecord) {
+ throw new Error(`No note path available for note '${this.noteId}'`);
+ }
+
+ return bestNotePathRecord.isArchived;
+ }
+
hasInheritableArchivedLabel() {
for (const attr of this.getAttributes()) {
if (attr.name === 'archived' && attr.type === LABEL && attr.isInheritable) {
@@ -1164,6 +1193,8 @@ class BNote extends AbstractBeccaEntity {
}
/**
+ * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
+ *
* @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
*/
getAllNotePaths() {
@@ -1171,18 +1202,73 @@ class BNote extends AbstractBeccaEntity {
return [['root']];
}
- const notePaths = [];
+ const parentNotes = this.getParentNotes();
+ let notePaths = [];
- for (const parentNote of this.getParentNotes()) {
- for (const parentPath of parentNote.getAllNotePaths()) {
- parentPath.push(this.noteId);
- notePaths.push(parentPath);
- }
+ if (parentNotes.length === 1) { // optimization for most common case
+ notePaths = parentNotes[0].getAllNotePaths();
+ } else {
+ notePaths = parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
+ }
+
+ for (const notePath of notePaths) {
+ notePath.push(this.noteId);
}
return notePaths;
}
+ /**
+ * @param {string} [hoistedNoteId='root']
+ * @return {Array<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array<string>, isHidden: boolean}>}
+ */
+ getSortedNotePathRecords(hoistedNoteId = 'root') {
+ const isHoistedRoot = hoistedNoteId === 'root';
+
+ const notePaths = this.getAllNotePaths().map(path => ({
+ notePath: path,
+ isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
+ isArchived: path.some(noteId => this.becca.notes[noteId].isArchived),
+ isHidden: path.includes('_hidden')
+ }));
+
+ notePaths.sort((a, b) => {
+ if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
+ return a.isInHoistedSubTree ? -1 : 1;
+ } else if (a.isArchived !== b.isArchived) {
+ return a.isArchived ? 1 : -1;
+ } else if (a.isHidden !== b.isHidden) {
+ return a.isHidden ? 1 : -1;
+ } else {
+ return a.notePath.length - b.notePath.length;
+ }
+ });
+
+ return notePaths;
+ }
+
+ /**
+ * Returns note path considered to be the "best"
+ *
+ * @param {string} [hoistedNoteId='root']
+ * @return {string[]} array of noteIds constituting the particular note path
+ */
+ getBestNotePath(hoistedNoteId = 'root') {
+ return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
+ }
+
+ /**
+ * Returns note path considered to be the "best"
+ *
+ * @param {string} [hoistedNoteId='root']
+ * @return {string} serialized note path (e.g. 'root/a1h315/js725h')
+ */
+ getBestNotePathString(hoistedNoteId = 'root') {
+ const notePath = this.getBestNotePath(hoistedNoteId);
+
+ return notePath?.join("/");
+ }
+
/**
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
*/
@@ -1196,9 +1282,7 @@ class BNote extends AbstractBeccaEntity {
return false;
} else if (parentNote.noteId === '_hidden') {
continue;
- }
-
- if (!parentNote.isHiddenCompletely()) {
+ } else if (!parentNote.isHiddenCompletely()) {
return false;
}
}
@@ -1392,7 +1476,7 @@ class BNote extends AbstractBeccaEntity {
/**
* @param parentNoteId
- * @returns {{success: boolean, message: string}}
+ * @returns {{success: boolean, message: string, branchId: string, notePath: string}}
*/
cloneTo(parentNoteId) {
const cloningService = require("../../services/cloning");
@@ -1550,7 +1634,7 @@ module.exports = BNote;
diff --git a/docs/backend_api/becca_entities_bnote_revision.js.html b/docs/backend_api/becca_entities_bnote_revision.js.html
index c140f1d10..ea663e51f 100644
--- a/docs/backend_api/becca_entities_bnote_revision.js.html
+++ b/docs/backend_api/becca_entities_bnote_revision.js.html
@@ -194,12 +194,14 @@ class BNoteRevision extends AbstractBeccaEntity {
utcDateLastEdited: this.utcDateLastEdited,
utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified,
+ content: this.content, // used when retrieving full note revision to frontend
contentLength: this.contentLength
};
}
getPojoToSave() {
const pojo = this.getPojo();
+ delete pojo.content; // not getting persisted
delete pojo.contentLength; // not getting persisted
if (pojo.isProtected) {
@@ -233,7 +235,7 @@ module.exports = BNoteRevision;
diff --git a/docs/backend_api/becca_entities_boption.js.html b/docs/backend_api/becca_entities_boption.js.html
index ab2c22f51..0f2856fdc 100644
--- a/docs/backend_api/becca_entities_boption.js.html
+++ b/docs/backend_api/becca_entities_boption.js.html
@@ -44,6 +44,11 @@ class BOption extends AbstractBeccaEntity {
constructor(row) {
super();
+ this.updateFromRow(row);
+ this.becca.options[this.name] = this;
+ }
+
+ updateFromRow(row) {
/** @type {string} */
this.name = row.name;
/** @type {string} */
@@ -52,8 +57,6 @@ class BOption extends AbstractBeccaEntity {
this.isSynced = !!row.isSynced;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
-
- this.becca.options[this.name] = this;
}
beforeSaving() {
@@ -89,7 +92,7 @@ module.exports = BOption;
diff --git a/docs/backend_api/becca_entities_brecent_note.js.html b/docs/backend_api/becca_entities_brecent_note.js.html
index 2e2f5ccb1..02f6858fb 100644
--- a/docs/backend_api/becca_entities_brecent_note.js.html
+++ b/docs/backend_api/becca_entities_brecent_note.js.html
@@ -77,7 +77,7 @@ module.exports = BRecentNote;
diff --git a/docs/backend_api/index.html b/docs/backend_api/index.html
index 55d778a7e..f9f7919b5 100644
--- a/docs/backend_api/index.html
+++ b/docs/backend_api/index.html
@@ -56,7 +56,7 @@
diff --git a/docs/backend_api/module-sql.html b/docs/backend_api/module-sql.html
index 6517bc0d4..2605d6530 100644
--- a/docs/backend_api/module-sql.html
+++ b/docs/backend_api/module-sql.html
@@ -1300,7 +1300,7 @@
diff --git a/docs/backend_api/services_backend_script_api.js.html b/docs/backend_api/services_backend_script_api.js.html
index 44f40bed6..bffce4bcd 100644
--- a/docs/backend_api/services_backend_script_api.js.html
+++ b/docs/backend_api/services_backend_script_api.js.html
@@ -165,13 +165,13 @@ function BackendScriptApi(currentNote, apiParams) {
this.getNoteWithLabel = attributeService.getNoteWithLabel;
/**
- * If there's no branch between note and parent note, create one. Otherwise, do nothing.
+ * If there's no branch between note and parent note, create one. Otherwise, do nothing. Returns the new or existing branch.
*
* @method
* @param {string} noteId
* @param {string} parentNoteId
* @param {string} prefix - if branch will be created between note and parent note, set this prefix
- * @returns {void}
+ * @returns {{branch: BBranch|null}}
*/
this.ensureNoteIsPresentInParent = cloningService.ensureNoteIsPresentInParent;
@@ -499,11 +499,11 @@ function BackendScriptApi(currentNote, apiParams) {
if (opts.type === 'script' && !opts.scriptNoteId) { throw new Error("scriptNoteId is mandatory for launchers of type 'script'"); }
if (opts.type === 'customWidget' && !opts.widgetNoteId) { throw new Error("widgetNoteId is mandatory for launchers of type 'customWidget'"); }
- const parentNoteId = !!opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers';
+ const parentNoteId = opts.isVisible ? '_lbVisibleLaunchers' : '_lbAvailableLaunchers';
const noteId = 'al_' + opts.id;
const launcherNote =
- becca.getNote(opts.id) ||
+ becca.getNote(noteId) ||
specialNotesService.createLauncher({
noteId: noteId,
parentNoteId: parentNoteId,
@@ -542,7 +542,7 @@ function BackendScriptApi(currentNote, apiParams) {
if (opts.icon) {
launcherNote.setLabel('iconClass', `bx ${opts.icon}`);
} else {
- launcherNote.removeLabel('keyboardShortcut');
+ launcherNote.removeLabel('iconClass');
}
return {note: launcherNote};
@@ -584,7 +584,7 @@ module.exports = BackendScriptApi;
diff --git a/docs/backend_api/services_sql.js.html b/docs/backend_api/services_sql.js.html
index ec2ac56e6..783bf8301 100644
--- a/docs/backend_api/services_sql.js.html
+++ b/docs/backend_api/services_sql.js.html
@@ -245,7 +245,7 @@ function wrap(query, func) {
// in these cases error should be simply ignored.
console.log(e.message);
- return null
+ return null;
}
throw e;
@@ -309,7 +309,7 @@ function fillParamList(paramIds, truncate = true) {
}
// doing it manually to avoid this showing up on the sloq query list
- const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`, paramIds);
+ const s = stmt(`INSERT INTO param_list VALUES ${paramIds.map(paramId => `(?)`).join(',')}`);
s.run(paramIds);
}
@@ -413,7 +413,7 @@ module.exports = {
diff --git a/docs/frontend_api/FAttribute.html b/docs/frontend_api/FAttribute.html
index 641c3a737..901dc598c 100644
--- a/docs/frontend_api/FAttribute.html
+++ b/docs/frontend_api/FAttribute.html
@@ -850,7 +850,7 @@ and relation (representing named relationship between source and target note)
diff --git a/docs/frontend_api/FBranch.html b/docs/frontend_api/FBranch.html
index 284bc72d5..4ec45ea97 100644
--- a/docs/frontend_api/FBranch.html
+++ b/docs/frontend_api/FBranch.html
@@ -1062,7 +1062,7 @@ parents.
diff --git a/docs/frontend_api/FNote.html b/docs/frontend_api/FNote.html
index cff8d1e8f..cf7e9782c 100644
--- a/docs/frontend_api/FNote.html
+++ b/docs/frontend_api/FNote.html
@@ -977,6 +977,116 @@
+ getAllNotePaths() → {Array.<Array.<string>>}
+
+
+
+
+
+
+
+ Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ - array of notePaths (each represented by array of noteIds constituting the particular note path)
+
+
+
+
+
+
+ Type
+
+
+
+Array.<Array.<string>>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
getAttribute(type, name) → {FAttribute }
@@ -1097,7 +1207,7 @@
Source:
@@ -1275,7 +1385,7 @@
Source:
@@ -1475,7 +1585,7 @@
Source:
@@ -1533,6 +1643,364 @@
+ getBestNotePath(hoistedNoteIdopt ) → {Array.<string>}
+
+
+
+
+
+
+
+ Returns note path considered to be the "best"
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+ Default
+
+
+ Description
+
+
+
+
+
+
+
+
+ hoistedNoteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ 'root'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ array of noteIds constituting the particular note path
+
+
+
+
+
+
+ Type
+
+
+
+Array.<string>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ getBestNotePathString(hoistedNoteIdopt ) → {string}
+
+
+
+
+
+
+
+ Returns note path considered to be the "best"
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+ Default
+
+
+ Description
+
+
+
+
+
+
+
+
+ hoistedNoteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ 'root'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ serialized note path (e.g. 'root/a1h315/js725h')
+
+
+
+
+
+
+ Type
+
+
+
+string
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
getBranchIds() → {Array.<string>}
@@ -1583,7 +2051,7 @@
Source:
@@ -1687,7 +2155,7 @@
Source:
@@ -1789,7 +2257,7 @@
Source:
@@ -1891,7 +2359,7 @@
Source:
@@ -1993,7 +2461,7 @@
Source:
@@ -2144,7 +2612,7 @@
Source:
@@ -2299,7 +2767,7 @@
Source:
@@ -2466,7 +2934,7 @@
Source:
@@ -2576,7 +3044,7 @@
Source:
@@ -2678,7 +3146,7 @@
Source:
@@ -2852,7 +3320,7 @@
Source:
@@ -3030,7 +3498,7 @@
Source:
@@ -3230,7 +3698,7 @@
Source:
@@ -3385,7 +3853,7 @@
Source:
@@ -3540,7 +4008,7 @@
Source:
@@ -3707,7 +4175,7 @@
Source:
@@ -3862,7 +4330,7 @@
Source:
@@ -4017,7 +4485,7 @@
Source:
@@ -4184,7 +4652,7 @@
Source:
@@ -4290,7 +4758,7 @@
Source:
@@ -4392,7 +4860,7 @@
Source:
@@ -4494,7 +4962,7 @@
Source:
@@ -4596,7 +5064,7 @@
Source:
@@ -4747,7 +5215,7 @@
Source:
@@ -4902,7 +5370,7 @@
Source:
@@ -5072,7 +5540,7 @@
Source:
@@ -5223,7 +5691,7 @@
Source:
@@ -5390,7 +5858,7 @@
Source:
@@ -5496,7 +5964,7 @@
Source:
@@ -5557,6 +6025,177 @@
+ getSortedNotePathRecords(hoistedNoteIdopt ) → {Array.<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array.<string>, isHidden: boolean}>}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+ Attributes
+
+
+
+ Default
+
+
+ Description
+
+
+
+
+
+
+
+
+ hoistedNoteId
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+ <optional>
+
+
+
+
+
+
+
+
+
+
+
+ 'root'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+
+
+
+ Type
+
+
+
+Array.<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array.<string>, isHidden: boolean}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
(async) getTargetRelationSourceNotes() → {Array.<FNote >}
@@ -5609,7 +6248,7 @@
Source:
@@ -5715,7 +6354,7 @@
Source:
@@ -5889,7 +6528,7 @@
Source:
@@ -5995,7 +6634,7 @@
Source:
@@ -6146,7 +6785,7 @@
Source:
@@ -6324,7 +6963,7 @@
Source:
@@ -6479,7 +7118,7 @@
Source:
@@ -6634,7 +7273,7 @@
Source:
@@ -6789,7 +7428,7 @@
Source:
@@ -6897,7 +7536,7 @@
Source:
@@ -6981,7 +7620,7 @@
Source:
@@ -7075,7 +7714,7 @@
Source:
@@ -7181,7 +7820,7 @@
Source:
@@ -7287,7 +7926,7 @@
Source:
@@ -7336,6 +7975,161 @@
+
+
+
+
+
+ isLabelTruthy(name) → {boolean}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Parameters:
+
+
+
+
+
+
+ Name
+
+
+ Type
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ name
+
+
+
+
+
+string
+
+
+
+
+
+
+
+
+
+ label name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Source:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Returns:
+
+
+
+ true if label exists (including inherited) and does not have "false" value.
+
+
+
+
+
+
+ Type
+
+
+
+boolean
+
+
+
+
+
+
+
+
+
+
+
+
@@ -7357,7 +8151,7 @@
diff --git a/docs/frontend_api/FNoteComplement.html b/docs/frontend_api/FNoteComplement.html
index 8c1bb3d94..1cac29fba 100644
--- a/docs/frontend_api/FNoteComplement.html
+++ b/docs/frontend_api/FNoteComplement.html
@@ -781,7 +781,7 @@
diff --git a/docs/frontend_api/FrontendScriptApi.html b/docs/frontend_api/FrontendScriptApi.html
index e866a8473..5e94a32c2 100644
--- a/docs/frontend_api/FrontendScriptApi.html
+++ b/docs/frontend_api/FrontendScriptApi.html
@@ -342,115 +342,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Properties:
-
-
-
-
-
-
-
-
- Type
-
-
-
-
-
- Description
-
-
-
-
-
-
-
-
-
-
-
-
-RightPanelWidget
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deprecated: use api.RightPanelWidget instead
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
@@ -556,115 +448,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- NoteContextCachingWidget
-
-
-
-
-
-
-
-
-
-
- Properties:
-
-
-
-
-
-
-
-
- Type
-
-
-
-
-
- Description
-
-
-
-
-
-
-
-
-
-
-
-
-NoteContextAwareWidget
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deprecated: use NoteContextAwareWidget instead
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
@@ -770,223 +554,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Properties:
-
-
-
-
-
-
-
-
- Type
-
-
-
-
-
- Description
-
-
-
-
-
-
-
-
-
-
-
-
-NoteContextAwareWidget
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deprecated: use NoteContextAwareWidget instead
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Properties:
-
-
-
-
-
-
-
-
- Type
-
-
-
-
-
- Description
-
-
-
-
-
-
-
-
-
-
-
-
-NoteContextAwareWidget
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Deprecated: use NoteContextAwareWidget instead
-
-
-
-
-
-
-
-
-
-
-
- Source:
-
@@ -1558,7 +1126,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -1713,7 +1281,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -2054,7 +1622,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -2191,7 +1759,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -2399,7 +1967,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -2781,7 +2349,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -2914,7 +2482,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -3024,7 +2592,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -3130,7 +2698,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -3236,7 +2804,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -3346,7 +2914,7 @@ available in the JS frontend notes. You can use e.g. api.showMessage(api.s
Source:
@@ -3457,7 +3025,7 @@ implementation of actual widget type.
Source:
@@ -3612,7 +3180,7 @@ implementation of actual widget type.
Source:
@@ -3767,7 +3335,7 @@ implementation of actual widget type.
Source:
@@ -3874,7 +3442,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4029,7 +3597,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4185,7 +3753,7 @@ if some action needs to happen on only one specific instance.
Source:
@@ -4386,7 +3954,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4492,7 +4060,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4647,7 +4215,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4802,7 +4370,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -4952,7 +4520,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5130,7 +4698,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5308,7 +4876,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5459,7 +5027,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5637,7 +5205,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5811,7 +5379,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -5966,7 +5534,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6120,7 +5688,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6275,7 +5843,7 @@ otherwise (by e.g. createNoteLink())
Source:
@@ -6436,7 +6004,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6596,7 +6164,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6752,7 +6320,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -6907,7 +6475,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7058,7 +6626,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7213,7 +6781,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7350,7 +6918,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7510,7 +7078,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7670,7 +7238,7 @@ Internally this serializes the anonymous function into string and sends it to ba
Source:
@@ -7762,7 +7330,7 @@ Typical use case is when new note has been created, we should wait until it is s
Source:
@@ -7832,7 +7400,7 @@ Typical use case is when new note has been created, we should wait until it is s
diff --git a/docs/frontend_api/entities_fattribute.js.html b/docs/frontend_api/entities_fattribute.js.html
index dffe972bc..a9cb49a5f 100644
--- a/docs/frontend_api/entities_fattribute.js.html
+++ b/docs/frontend_api/entities_fattribute.js.html
@@ -121,7 +121,7 @@ export default FAttribute;
diff --git a/docs/frontend_api/entities_fbranch.js.html b/docs/frontend_api/entities_fbranch.js.html
index 2b7ced98d..deae06846 100644
--- a/docs/frontend_api/entities_fbranch.js.html
+++ b/docs/frontend_api/entities_fbranch.js.html
@@ -105,7 +105,7 @@ export default FBranch;
diff --git a/docs/frontend_api/entities_fnote.js.html b/docs/frontend_api/entities_fnote.js.html
index a31f615a2..84616e2cb 100644
--- a/docs/frontend_api/entities_fnote.js.html
+++ b/docs/frontend_api/entities_fnote.js.html
@@ -101,7 +101,7 @@ class FNote {
this.mime = row.mime;
}
- addParent(parentNoteId, branchId) {
+ addParent(parentNoteId, branchId, sort = true) {
if (parentNoteId === 'none') {
return;
}
@@ -111,6 +111,10 @@ class FNote {
}
this.parentToBranch[parentNoteId] = branchId;
+
+ if (sort) {
+ this.sortParents();
+ }
}
addChild(childNoteId, branchId, sort = true) {
@@ -217,7 +221,7 @@ class FNote {
// will sort the parents so that non-search & non-archived are first and archived at the end
// this is done so that non-search & non-archived paths are always explored as first when looking for note path
- resortParents() {
+ sortParents() {
this.parents.sort((aNoteId, bNoteId) => {
const aBranchId = this.parentToBranch[aNoteId];
@@ -225,7 +229,7 @@ class FNote {
return 1;
}
- const aNote = this.froca.getNoteFromCache([aNoteId]);
+ const aNote = this.froca.getNoteFromCache(aNoteId);
if (aNote.isArchived || aNote.isHiddenCompletely()) {
return 1;
@@ -271,6 +275,11 @@ class FNote {
return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
}
+ /**
+ * @param {string[]} path
+ * @return {FAttribute[]}
+ * @private
+ */
__getCachedAttributes(path) {
// notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
// when template instance is a parent of template itself
@@ -323,63 +332,49 @@ class FNote {
return this.noteId === 'root';
}
- getAllNotePaths(encounteredNoteIds = null) {
+ /**
+ * Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
+ *
+ * @returns {string[][]} - array of notePaths (each represented by array of noteIds constituting the particular note path)
+ */
+ getAllNotePaths() {
if (this.noteId === 'root') {
return [['root']];
}
- if (!encounteredNoteIds) {
- encounteredNoteIds = new Set();
+ const parentNotes = this.getParentNotes().filter(note => note.type !== 'search');
+ let notePaths = [];
+
+ if (parentNotes.length === 1) { // optimization for most common case
+ notePaths = parentNotes[0].getAllNotePaths();
+ } else {
+ notePaths = parentNotes.flatMap(parentNote => parentNote.getAllNotePaths());
}
- encounteredNoteIds.add(this.noteId);
-
- const parentNotes = this.getParentNotes();
- let paths;
-
- if (parentNotes.length === 1) { // optimization for the most common case
- if (encounteredNoteIds.has(parentNotes[0].noteId)) {
- return [];
- }
- else {
- paths = parentNotes[0].getAllNotePaths(encounteredNoteIds);
- }
- }
- else {
- paths = [];
-
- for (const parentNote of parentNotes) {
- if (encounteredNoteIds.has(parentNote.noteId)) {
- continue;
- }
-
- const newSet = new Set(encounteredNoteIds);
-
- paths.push(...parentNote.getAllNotePaths(newSet));
- }
+ for (const notePath of notePaths) {
+ notePath.push(this.noteId);
}
- for (const path of paths) {
- path.push(this.noteId);
- }
-
- return paths;
+ return notePaths;
}
- getSortedNotePaths(hoistedNotePath = 'root') {
+ /**
+ * @param {string} [hoistedNoteId='root']
+ * @return {Array<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array<string>, isHidden: boolean}>}
+ */
+ getSortedNotePathRecords(hoistedNoteId = 'root') {
+ const isHoistedRoot = hoistedNoteId === 'root';
+
const notePaths = this.getAllNotePaths().map(path => ({
notePath: path,
- isInHoistedSubTree: path.includes(hoistedNotePath),
- isArchived: path.find(noteId => froca.notes[noteId].isArchived),
- isSearch: path.find(noteId => froca.notes[noteId].type === 'search'),
+ isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
+ isArchived: path.some(noteId => froca.notes[noteId].isArchived),
isHidden: path.includes('_hidden')
}));
notePaths.sort((a, b) => {
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1;
- } else if (a.isSearch !== b.isSearch) {
- return a.isSearch ? 1 : -1;
} else if (a.isArchived !== b.isArchived) {
return a.isArchived ? 1 : -1;
} else if (a.isHidden !== b.isHidden) {
@@ -392,6 +387,28 @@ class FNote {
return notePaths;
}
+ /**
+ * Returns note path considered to be the "best"
+ *
+ * @param {string} [hoistedNoteId='root']
+ * @return {string[]} array of noteIds constituting the particular note path
+ */
+ getBestNotePath(hoistedNoteId = 'root') {
+ return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
+ }
+
+ /**
+ * Returns note path considered to be the "best"
+ *
+ * @param {string} [hoistedNoteId='root']
+ * @return {string} serialized note path (e.g. 'root/a1h315/js725h')
+ */
+ getBestNotePathString(hoistedNoteId = 'root') {
+ const notePath = this.getBestNotePath(hoistedNoteId);
+
+ return notePath?.join("/");
+ }
+
/**
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
*/
@@ -403,7 +420,7 @@ class FNote {
for (const parentNote of this.getParentNotes()) {
if (parentNote.noteId === 'root') {
return false;
- } else if (parentNote.noteId === '_hidden') {
+ } else if (parentNote.noteId === '_hidden' || parentNote.type === 'search') {
continue;
}
@@ -415,6 +432,13 @@ class FNote {
return true;
}
+ /**
+ * @param {FAttribute[]} attributes
+ * @param {string} type
+ * @param {string} name
+ * @return {FAttribute[]}
+ * @private
+ */
__filterAttrs(attributes, type, name) {
this.__validateTypeName(type, name);
@@ -551,7 +575,9 @@ class FNote {
* @returns {boolean} true if note has an attribute with given type and name (including inherited)
*/
hasAttribute(type, name) {
- return !!this.getAttribute(type, name);
+ const attributes = this.getAttributes();
+
+ return attributes.some(attr => attr.name === name && attr.type === type);
}
/**
@@ -619,6 +645,20 @@ class FNote {
*/
hasLabel(name) { return this.hasAttribute(LABEL, name); }
+ /**
+ * @param {string} name - label name
+ * @returns {boolean} true if label exists (including inherited) and does not have "false" value.
+ */
+ isLabelTruthy(name) {
+ const label = this.getLabel(name);
+
+ if (!label) {
+ return false;
+ }
+
+ return label && label.value !== 'false';
+ }
+
/**
* @param {string} name - relation name
* @returns {boolean} true if relation exists (excluding inherited)
@@ -730,7 +770,14 @@ class FNote {
});
// attrs are not resorted if position changes after initial load
- promotedAttrs.sort((a, b) => a.position < b.position ? -1 : 1);
+ promotedAttrs.sort((a, b) => {
+ if (a.noteId === b.noteId) {
+ return a.position < b.position ? -1 : 1;
+ } else {
+ // inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
+ return a.noteId < b.noteId ? -1 : 1;
+ }
+ });
return promotedAttrs;
}
@@ -930,7 +977,7 @@ export default FNote;
diff --git a/docs/frontend_api/entities_fnote_complement.js.html b/docs/frontend_api/entities_fnote_complement.js.html
index 6ea35e95a..252ea6a2f 100644
--- a/docs/frontend_api/entities_fnote_complement.js.html
+++ b/docs/frontend_api/entities_fnote_complement.js.html
@@ -82,7 +82,7 @@ export default FNoteComplement;
diff --git a/docs/frontend_api/index.html b/docs/frontend_api/index.html
index 08a8eb159..6465f193a 100644
--- a/docs/frontend_api/index.html
+++ b/docs/frontend_api/index.html
@@ -56,7 +56,7 @@
diff --git a/docs/frontend_api/services_frontend_script_api.js.html b/docs/frontend_api/services_frontend_script_api.js.html
index a326810c3..679f2e5eb 100644
--- a/docs/frontend_api/services_frontend_script_api.js.html
+++ b/docs/frontend_api/services_frontend_script_api.js.html
@@ -63,36 +63,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
/** @property {dayjs} day.js library for date manipulation. See {@link https://day.js.org} for documentation */
this.dayjs = dayjs;
- /**
- * @property {RightPanelWidget}
- * @deprecated use api.RightPanelWidget instead
- */
- this.CollapsibleWidget = RightPanelWidget;
-
/** @property {RightPanelWidget} */
this.RightPanelWidget = RightPanelWidget;
/** @property {NoteContextAwareWidget} */
this.NoteContextAwareWidget = NoteContextAwareWidget;
- /**
- * @property {NoteContextAwareWidget}
- * @deprecated use NoteContextAwareWidget instead
- */
- this.TabAwareWidget = NoteContextAwareWidget;
-
- /**
- * @property {NoteContextAwareWidget}
- * @deprecated use NoteContextAwareWidget instead
- */
- this.TabCachingWidget = NoteContextAwareWidget;
-
- /**
- * @property {NoteContextAwareWidget}
- * @deprecated use NoteContextAwareWidget instead
- */
- this.NoteContextCachingWidget = NoteContextAwareWidget;
-
/** @property {BasicWidget} */
this.BasicWidget = BasicWidget;
@@ -117,7 +93,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath);
- appContext.triggerEvent('focusAndSelectTitle');
+ await appContext.triggerEvent('focusAndSelectTitle');
};
/**
@@ -134,7 +110,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
await appContext.tabManager.openContextWithNote(notePath, { activate });
if (activate) {
- appContext.triggerEvent('focusAndSelectTitle');
+ await appContext.triggerEvent('focusAndSelectTitle');
}
};
@@ -152,10 +128,10 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1];
- appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
+ await appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath});
if (activate) {
- appContext.triggerEvent('focusAndSelectTitle');
+ await appContext.triggerEvent('focusAndSelectTitle');
}
};
@@ -581,7 +557,7 @@ export default FrontendScriptApi;
diff --git a/package-lock.json b/package-lock.json
index d47b77125..56d259cbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@electron/remote": "2.0.9",
- "@excalidraw/excalidraw": "0.15.2",
+ "@excalidraw/excalidraw": "0.14.2",
"archiver": "5.3.1",
"async-mutex": "0.4.0",
"axios": "1.4.0",
@@ -459,9 +459,9 @@
}
},
"node_modules/@excalidraw/excalidraw": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz",
- "integrity": "sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==",
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz",
+ "integrity": "sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==",
"peerDependencies": {
"react": "^17.0.2 || ^18.2.0",
"react-dom": "^17.0.2 || ^18.2.0"
@@ -13330,9 +13330,9 @@
"dev": true
},
"@excalidraw/excalidraw": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz",
- "integrity": "sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw==",
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz",
+ "integrity": "sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==",
"requires": {}
},
"@gar/promisify": {
diff --git a/package.json b/package.json
index 484705430..e0991a0c9 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@electron/remote": "2.0.9",
- "@excalidraw/excalidraw": "0.15.2",
+ "@excalidraw/excalidraw": "0.14.2",
"archiver": "5.3.1",
"async-mutex": "0.4.0",
"axios": "1.4.0",
diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js
index fc93d1932..2b2aba663 100644
--- a/src/becca/becca_loader.js
+++ b/src/becca/becca_loader.js
@@ -69,18 +69,6 @@ function reload() {
require('../services/ws').reloadFrontend();
}
-function postProcessEntityUpdate(entityName, entity) {
- if (entityName === 'notes') {
- noteUpdated(entity);
- } else if (entityName === 'branches') {
- branchUpdated(entity);
- } else if (entityName === 'attributes') {
- attributeUpdated(entity);
- } else if (entityName === 'note_reordering') {
- noteReorderingUpdated(entity);
- }
-}
-
eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => {
if (!becca.loaded) {
return;
@@ -112,6 +100,25 @@ eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({entityName, en
postProcessEntityUpdate(entityName, entity);
});
+/**
+ * This gets run on entity being created or updated.
+ *
+ * @param entityName
+ * @param entityRow - can be a becca entity (change comes from this trilium instance) or just a row (from sync).
+ * Should be therefore treated as a row.
+ */
+function postProcessEntityUpdate(entityName, entityRow) {
+ if (entityName === 'notes') {
+ noteUpdated(entityRow);
+ } else if (entityName === 'branches') {
+ branchUpdated(entityRow);
+ } else if (entityName === 'attributes') {
+ attributeUpdated(entityRow);
+ } else if (entityName === 'note_reordering') {
+ noteReorderingUpdated(entityRow);
+ }
+}
+
eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED], ({entityName, entityId}) => {
if (!becca.loaded) {
return;
@@ -149,6 +156,7 @@ function branchDeleted(branchId) {
.filter(parentBranch => parentBranch.branchId !== branch.branchId);
if (childNote.parents.length > 0) {
+ // subtree notes might lose some inherited attributes
childNote.invalidateSubTree();
}
}
@@ -163,8 +171,8 @@ function branchDeleted(branchId) {
delete becca.branches[branch.branchId];
}
-function noteUpdated(entity) {
- const note = becca.notes[entity.noteId];
+function noteUpdated(entityRow) {
+ const note = becca.notes[entityRow.noteId];
if (note) {
// type / mime could have been changed, and they are present in flatTextCache
@@ -172,15 +180,19 @@ function noteUpdated(entity) {
}
}
-function branchUpdated(branch) {
- const childNote = becca.notes[branch.noteId];
+function branchUpdated(branchRow) {
+ const childNote = becca.notes[branchRow.noteId];
if (childNote) {
childNote.flatTextCache = null;
childNote.sortParents();
+
+ // notes in the subtree can get new inherited attributes
+ // this is in theory needed upon branch creation, but there's no create event for sync changes
+ childNote.invalidateSubTree();
}
- const parentNote = becca.notes[branch.parentNoteId];
+ const parentNote = becca.notes[branchRow.parentNoteId];
if (parentNote) {
parentNote.sortChildren();
@@ -222,8 +234,10 @@ function attributeDeleted(attributeId) {
}
}
-function attributeUpdated(attribute) {
- const note = becca.notes[attribute.noteId];
+/** @param {BAttribute} attributeRow */
+function attributeUpdated(attributeRow) {
+ const attribute = becca.attributes[attributeRow.attributeId];
+ const note = becca.notes[attributeRow.noteId];
if (note) {
if (attribute.isAffectingSubtree || note.isInherited()) {
diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js
index 64599cc13..3771f0f05 100644
--- a/src/becca/entities/bnote.js
+++ b/src/becca/entities/bnote.js
@@ -6,12 +6,12 @@ const sql = require('../../services/sql');
const utils = require('../../services/utils');
const dateUtils = require('../../services/date_utils');
const AbstractBeccaEntity = require("./abstract_becca_entity");
-const BRevision = require("./brevision.js");
+const BRevision = require("./brevision");
const BAttachment = require("./battachment");
const TaskContext = require("../../services/task_context");
const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc');
-const eventService = require("../../services/events.js");
+const eventService = require("../../services/events");
dayjs.extend(utc);
const LABEL = 'label';
@@ -87,7 +87,7 @@ class BNote extends AbstractBeccaEntity {
this.decrypt();
/** @type {string|null} */
- this.flatTextCache = null;
+ this.__flatTextCache = null;
return this;
}
@@ -111,7 +111,7 @@ class BNote extends AbstractBeccaEntity {
this.__attributeCache = null;
/** @type {BAttribute[]|null}
* @private */
- this.inheritableAttributeCache = null;
+ this.__inheritableAttributeCache = null;
/** @type {BAttribute[]}
* @private */
@@ -121,7 +121,7 @@ class BNote extends AbstractBeccaEntity {
/** @type {BNote[]|null}
* @private */
- this.ancestorCache = null;
+ this.__ancestorCache = null;
// following attributes are filled during searching from database
@@ -392,11 +392,11 @@ class BNote extends AbstractBeccaEntity {
}
}
- this.inheritableAttributeCache = [];
+ this.__inheritableAttributeCache = [];
for (const attr of this.__attributeCache) {
if (attr.isInheritable) {
- this.inheritableAttributeCache.push(attr);
+ this.__inheritableAttributeCache.push(attr);
}
}
}
@@ -413,11 +413,11 @@ class BNote extends AbstractBeccaEntity {
return [];
}
- if (!this.inheritableAttributeCache) {
- this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
+ if (!this.__inheritableAttributeCache) {
+ this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
}
- return this.inheritableAttributeCache;
+ return this.__inheritableAttributeCache;
}
__validateTypeName(type, name) {
@@ -751,40 +751,40 @@ class BNote extends AbstractBeccaEntity {
* @returns {string} - returns flattened textual representation of note, prefixes and attributes
*/
getFlatText() {
- if (!this.flatTextCache) {
- this.flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
+ if (!this.__flatTextCache) {
+ this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
for (const branch of this.parentBranches) {
if (branch.prefix) {
- this.flatTextCache += `${branch.prefix} `;
+ this.__flatTextCache += `${branch.prefix} `;
}
}
- this.flatTextCache += `${this.title} `;
+ this.__flatTextCache += `${this.title} `;
for (const attr of this.getAttributes()) {
// it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
- this.flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`;
+ this.__flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`;
if (attr.value) {
- this.flatTextCache += `=${attr.value}`;
+ this.__flatTextCache += `=${attr.value}`;
}
- this.flatTextCache += ' ';
+ this.__flatTextCache += ' ';
}
- this.flatTextCache = utils.normalize(this.flatTextCache);
+ this.__flatTextCache = utils.normalize(this.__flatTextCache);
}
- return this.flatTextCache;
+ return this.__flatTextCache;
}
invalidateThisCache() {
- this.flatTextCache = null;
+ this.__flatTextCache = null;
this.__attributeCache = null;
- this.inheritableAttributeCache = null;
- this.ancestorCache = null;
+ this.__inheritableAttributeCache = null;
+ this.__ancestorCache = null;
}
invalidateSubTree(path = []) {
@@ -813,24 +813,6 @@ class BNote extends AbstractBeccaEntity {
}
}
- invalidateSubtreeFlatText() {
- this.flatTextCache = null;
-
- for (const childNote of this.children) {
- childNote.invalidateSubtreeFlatText();
- }
-
- for (const targetRelation of this.targetRelations) {
- if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
- const note = targetRelation.note;
-
- if (note) {
- note.invalidateSubtreeFlatText();
- }
- }
- }
- }
-
getRelationDefinitions() {
return this.getLabels()
.filter(l => l.name.startsWith("relation:"));
@@ -1021,28 +1003,28 @@ class BNote extends AbstractBeccaEntity {
/** @returns {BNote[]} */
getAncestors() {
- if (!this.ancestorCache) {
+ if (!this.__ancestorCache) {
const noteIds = new Set();
- this.ancestorCache = [];
+ this.__ancestorCache = [];
for (const parent of this.parents) {
if (noteIds.has(parent.noteId)) {
continue;
}
- this.ancestorCache.push(parent);
+ this.__ancestorCache.push(parent);
noteIds.add(parent.noteId);
for (const ancestorNote of parent.getAncestors()) {
if (!noteIds.has(ancestorNote.noteId)) {
- this.ancestorCache.push(ancestorNote);
+ this.__ancestorCache.push(ancestorNote);
noteIds.add(ancestorNote.noteId);
}
}
}
}
- return this.ancestorCache;
+ return this.__ancestorCache;
}
/** @returns {string[]} */
@@ -1178,7 +1160,7 @@ class BNote extends AbstractBeccaEntity {
/**
* @param {string} [hoistedNoteId='root']
- * @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]}
+ * @return {Array<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array, isHidden: boolean}>}
*/
getSortedNotePathRecords(hoistedNoteId = 'root') {
const isHoistedRoot = hoistedNoteId === 'root';
@@ -1548,7 +1530,7 @@ class BNote extends AbstractBeccaEntity {
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
try {
this.title = protectedSessionService.decryptString(this.title);
- this.flatTextCache = null;
+ this.__flatTextCache = null;
this.isDecrypted = true;
}
diff --git a/src/etapi/attributes.js b/src/etapi/attributes.js
index 6886e0845..fb8b2ad99 100644
--- a/src/etapi/attributes.js
+++ b/src/etapi/attributes.js
@@ -40,19 +40,25 @@ function register(router) {
}
});
- const ALLOWED_PROPERTIES_FOR_PATCH = {
+ const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = {
'value': [v.notNull, v.isString],
'position': [v.notNull, v.isInteger]
};
+ const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = {
+ 'position': [v.notNull, v.isInteger]
+ };
+
eu.route(router, 'patch' ,'/etapi/attributes/:attributeId', (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
- if (attribute.type === 'relation') {
+ if (attribute.type === 'label') {
+ eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL);
+ } else if (attribute.type === 'relation') {
eu.getAndCheckNote(req.body.value);
- }
- eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
+ eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION);
+ }
attribute.save();
diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml
index 3c267b9a9..c5492c8e4 100644
--- a/src/etapi/etapi.openapi.yaml
+++ b/src/etapi/etapi.openapi.yaml
@@ -374,7 +374,7 @@ paths:
schema:
$ref: '#/components/schemas/Error'
patch:
- description: patch a branch identified by the branchId with changes in the body
+ description: patch a branch identified by the branchId with changes in the body. Only prefix and notePosition can be updated. If you want to update other properties, you need to delete the old branch and create a new one.
operationId: patchBranchById
requestBody:
required: true
@@ -456,7 +456,7 @@ paths:
schema:
$ref: '#/components/schemas/Error'
patch:
- description: patch a attribute identified by the attributeId with changes in the body
+ description: patch a attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
operationId: patchAttributeById
requestBody:
required: true
diff --git a/src/etapi/mappers.js b/src/etapi/mappers.js
index 26f79da01..44a5fa8f6 100644
--- a/src/etapi/mappers.js
+++ b/src/etapi/mappers.js
@@ -1,3 +1,4 @@
+/** @param {BNote} note */
function mapNoteToPojo(note) {
return {
noteId: note.noteId,
@@ -18,6 +19,7 @@ function mapNoteToPojo(note) {
};
}
+/** @param {BBranch} branch */
function mapBranchToPojo(branch) {
return {
branchId: branch.branchId,
@@ -30,6 +32,7 @@ function mapBranchToPojo(branch) {
};
}
+/** @param {BAttribute} attr */
function mapAttributeToPojo(attr) {
return {
attributeId: attr.attributeId,
diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js
index 42b1608a7..9e0716cb3 100644
--- a/src/public/app/components/note_context.js
+++ b/src/public/app/components/note_context.js
@@ -171,9 +171,12 @@ class NoteContext extends Component {
}
getPojoState() {
- if (!this.notePath && this.hoistedNoteId === 'root') {
- // keeping empty hoisted tab is esp. important for mobile (e.g., opened launcher config)
- return null;
+ if (this.hoistedNoteId !== 'root') {
+ // keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config)
+
+ if (!this.notePath && this.getSubContexts().length === 0) {
+ return null;
+ }
}
return {
diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.js
index 2f461fec7..efbafc740 100644
--- a/src/public/app/components/tab_manager.js
+++ b/src/public/app/components/tab_manager.js
@@ -477,16 +477,23 @@ export default class TabManager extends Component {
this.tabsUpdate.scheduleUpdate();
}
- noteContextReorderEvent({ntxIdsInOrder}) {
- const order = {};
- let i = 0;
-
- for (const ntxId of ntxIdsInOrder) {
- order[ntxId] = i++;
- }
+ noteContextReorderEvent({ntxIdsInOrder, oldMainNtxId, newMainNtxId}) {
+ const order = Object.fromEntries(ntxIdsInOrder.map((v, i) => [v, i]));
this.children.sort((a, b) => order[a.ntxId] < order[b.ntxId] ? -1 : 1);
+ if (oldMainNtxId && newMainNtxId) {
+ this.children.forEach(c => {
+ if (c.ntxId === newMainNtxId) {
+ // new main context has null mainNtxId
+ c.mainNtxId = null;
+ } else if (c.ntxId === oldMainNtxId || c.mainNtxId === oldMainNtxId) {
+ // old main context or subcontexts all have the new mainNtxId
+ c.mainNtxId = newMainNtxId;
+ }
+ });
+ }
+
this.tabsUpdate.scheduleUpdate();
}
diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js
index 45263eaa7..4a6f0c847 100644
--- a/src/public/app/entities/fnote.js
+++ b/src/public/app/entities/fnote.js
@@ -371,7 +371,7 @@ class FNote {
/**
* @param {string} [hoistedNoteId='root']
- * @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]}
+ * @return {Array<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array, isHidden: boolean}>}
*/
getSortedNotePathRecords(hoistedNoteId = 'root') {
const isHoistedRoot = hoistedNoteId === 'root';
@@ -431,7 +431,7 @@ class FNote {
for (const parentNote of this.getParentNotes()) {
if (parentNote.noteId === 'root') {
return false;
- } else if (parentNote.noteId === '_hidden') {
+ } else if (parentNote.noteId === '_hidden' || parentNote.type === 'search') {
continue;
}
diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js
index 927a83bd3..76a05b7e2 100644
--- a/src/public/app/layouts/desktop_layout.js
+++ b/src/public/app/layouts/desktop_layout.js
@@ -44,6 +44,7 @@ import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import FindWidget from "../widgets/find.js";
import TocWidget from "../widgets/toc.js";
+import HighlightsListWidget from "../widgets/highlights_list.js";
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
import AboutDialog from "../widgets/dialogs/about.js";
import HelpDialog from "../widgets/dialogs/help.js";
@@ -75,6 +76,7 @@ import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
import ApiLogWidget from "../widgets/api_log.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
+import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
export default class DesktopLayout {
@@ -124,6 +126,8 @@ export default class DesktopLayout {
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new SpacerWidget(0, 1))
+ .child(new MovePaneButton(true))
+ .child(new MovePaneButton(false))
.child(new ClosePaneButton())
.child(new CreatePaneButton())
)
@@ -182,6 +186,7 @@ export default class DesktopLayout {
)
.child(new RightPaneContainer()
.child(new TocWidget())
+ .child(new HighlightsListWidget())
.child(...this.customWidgets.get('right-pane'))
)
)
diff --git a/src/public/app/services/frontend_script_api.js b/src/public/app/services/frontend_script_api.js
index 9318dd8ab..efcd3f6f7 100644
--- a/src/public/app/services/frontend_script_api.js
+++ b/src/public/app/services/frontend_script_api.js
@@ -483,6 +483,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/
this.randomString = utils.randomString;
+ /**
+ * @method
+ * @param {int} size in bytes
+ * @return {string} formatted string
+ */
+ this.formatNoteSize = utils.formatNoteSize;
+
this.logMessages = {};
this.logSpacedUpdates = {};
diff --git a/src/public/app/services/note_content_renderer.js b/src/public/app/services/note_content_renderer.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js
index 5d6643507..ddf7e9fbb 100644
--- a/src/public/app/services/utils.js
+++ b/src/public/app/services/utils.js
@@ -522,6 +522,17 @@ function copyHtmlToClipboard(content) {
navigator.clipboard.write([clipboardItem]);
}
+function formatNoteSize(size) {
+ size = Math.max(Math.round(size / 1024), 1);
+
+ if (size < 1024) {
+ return `${size} KiB`;
+ }
+ else {
+ return `${Math.round(size / 102.4) / 10} MiB`;
+ }
+}
+
export default {
reloadFrontendApp,
parseDate,
@@ -567,6 +578,8 @@ export default {
isValidAttributeName,
sleep,
escapeRegExp,
+ formatNoteSize,
+ escapeRegExp,
areObjectsEqual,
copyHtmlToClipboard
};
diff --git a/src/public/app/widgets/buttons/close_pane_button.js b/src/public/app/widgets/buttons/close_pane_button.js
index 690dcac6f..220fd2cca 100644
--- a/src/public/app/widgets/buttons/close_pane_button.js
+++ b/src/public/app/widgets/buttons/close_pane_button.js
@@ -7,6 +7,10 @@ export default class ClosePaneButton extends OnClickButtonWidget {
&& this.noteContext && !!this.noteContext.mainNtxId;
}
+ async noteContextReorderEvent({ntxIdsInOrder}) {
+ this.refresh();
+ }
+
constructor() {
super();
diff --git a/src/public/app/widgets/buttons/move_pane_button.js b/src/public/app/widgets/buttons/move_pane_button.js
new file mode 100644
index 000000000..632651ca5
--- /dev/null
+++ b/src/public/app/widgets/buttons/move_pane_button.js
@@ -0,0 +1,47 @@
+import OnClickButtonWidget from "./onclick_button.js";
+import appContext from "../../components/app_context.js";
+
+export default class MovePaneButton extends OnClickButtonWidget {
+ constructor(isMovingLeft) {
+ super();
+
+ this.isMovingLeft = isMovingLeft;
+
+ this.icon(isMovingLeft ? "bx-chevron-left" : "bx-chevron-right")
+ .title(isMovingLeft ? "Move left" : "Move right")
+ .titlePlacement("bottom")
+ .onClick(async (widget, e) => {
+ e.stopPropagation();
+ widget.triggerCommand("moveThisNoteSplit", {ntxId: widget.getClosestNtxId(), isMovingLeft: this.isMovingLeft});
+ })
+ .class("icon-action");
+ }
+
+ isEnabled() {
+ if (!super.isEnabled()) {
+ return false;
+ }
+
+ if (this.isMovingLeft) {
+ // movable if the current context is not a main context, i.e. non-null mainNtxId
+ return !!this.noteContext?.mainNtxId;
+ } else {
+ const currentIndex = appContext.tabManager.noteContexts.findIndex(c => c.ntxId === this.ntxId);
+ const nextContext = appContext.tabManager.noteContexts[currentIndex + 1];
+ // movable if the next context is not null and not a main context, i.e. non-null mainNtxId
+ return !!nextContext?.mainNtxId;
+ }
+ }
+
+ async noteContextRemovedEvent() {
+ this.refresh();
+ }
+
+ async newNoteContextCreatedEvent() {
+ this.refresh();
+ }
+
+ async noteContextReorderEvent() {
+ this.refresh();
+ }
+}
diff --git a/src/public/app/widgets/containers/split_note_container.js b/src/public/app/widgets/containers/split_note_container.js
index e706c6447..ed5af8022 100644
--- a/src/public/app/widgets/containers/split_note_container.js
+++ b/src/public/app/widgets/containers/split_note_container.js
@@ -74,6 +74,50 @@ export default class SplitNoteContainer extends FlexContainer {
appContext.tabManager.removeNoteContext(ntxId);
}
+ async moveThisNoteSplitCommand({ntxId, isMovingLeft}) {
+ if (!ntxId) {
+ logError("empty ntxId!");
+ return;
+ }
+
+ const contexts = appContext.tabManager.noteContexts;
+
+ const currentIndex = contexts.findIndex(c => c.ntxId === ntxId);
+ const leftIndex = isMovingLeft ? currentIndex - 1 : currentIndex;
+
+ if (currentIndex === -1 || leftIndex < 0 || leftIndex + 1 >= contexts.length) {
+ logError(`invalid context! currentIndex: ${currentIndex}, leftIndex: ${leftIndex}, contexts.length: ${contexts.length}`);
+ return;
+ }
+
+ if (contexts[leftIndex].isEmpty() && contexts[leftIndex + 1].isEmpty()) {
+ // no op
+ return;
+ }
+
+ const ntxIds = contexts.map(c => c.ntxId);
+ const newNtxIds = [
+ ...ntxIds.slice(0, leftIndex),
+ ntxIds[leftIndex + 1],
+ ntxIds[leftIndex],
+ ...ntxIds.slice(leftIndex + 2),
+ ];
+ const isChangingMainContext = !contexts[leftIndex].mainNtxId;
+
+ this.triggerCommand("noteContextReorder", {
+ ntxIdsInOrder: newNtxIds,
+ oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null,
+ newMainNtxId: isChangingMainContext ? ntxIds[leftIndex + 1]: null,
+ });
+
+ // reorder the note context widgets
+ this.$widget.find(`[data-ntx-id="${ntxIds[leftIndex]}"]`)
+ .insertAfter(this.$widget.find(`[data-ntx-id="${ntxIds[leftIndex + 1]}"]`));
+
+ // activate context that now contains the original note
+ await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]);
+ }
+
activeContextChangedEvent() {
this.refresh();
}
diff --git a/src/public/app/widgets/highlights_list.js b/src/public/app/widgets/highlights_list.js
new file mode 100644
index 000000000..4e88e9e45
--- /dev/null
+++ b/src/public/app/widgets/highlights_list.js
@@ -0,0 +1,257 @@
+/**
+ * Widget: Show highlighted text in the right pane
+ *
+ * By design, there's no support for nonsensical or malformed constructs:
+ * - For example, if there is a formula in the middle of the highlighted text, the two ends of the formula will be regarded as two entries
+ */
+
+import attributeService from "../services/attributes.js";
+import RightPanelWidget from "./right_panel_widget.js";
+import options from "../services/options.js";
+import OnClickButtonWidget from "./buttons/onclick_button.js";
+
+const TPL = `
+
+
+
+
`;
+
+export default class HighlightsListWidget extends RightPanelWidget {
+ constructor() {
+ super();
+
+ this.closeHltButton = new CloseHltButton();
+ this.child(this.closeHltButton);
+ }
+
+ get widgetTitle() {
+ return "Highlighted Text";
+ }
+
+ isEnabled() {
+ return super.isEnabled()
+ && this.note.type === 'text'
+ && !this.noteContext.viewScope.highlightedTextTemporarilyHidden
+ && this.noteContext.viewScope.viewMode === 'default';
+ }
+
+ async doRenderBody() {
+ this.$body.empty().append($(TPL));
+ this.$highlightsList = this.$body.find('.highlists-list');
+ this.$body.find('.highlists-list-widget').append(this.closeHltButton.render());
+ }
+
+ async refreshWithNote(note) {
+ /* The reason for adding highlightedTextPreviousVisible is to record whether the previous state
+ of the highlightedText is hidden or displayed, and then let it be displayed/hidden at the initial time.
+ If there is no such value, when the right panel needs to display toc but not highlighttext,
+ every time the note content is changed, highlighttext Widget will appear and then close immediately,
+ because getHlt function will consume time */
+ if (this.noteContext.viewScope.highlightedTextPreviousVisible) {
+ this.toggleInt(true);
+ } else {
+ this.toggleInt(false);
+ }
+
+ const optionsHlt = JSON.parse(options.get('highlightedText'));
+
+ if (note.isLabelTruthy('hideHighlightWidget') || !optionsHlt) {
+ this.toggleInt(false);
+ this.triggerCommand("reEvaluateRightPaneVisibility");
+ return;
+ }
+
+ let $highlightsList = "", hltLiCount = -1;
+ // Check for type text unconditionally in case alwaysShowWidget is set
+ if (this.note.type === 'text') {
+ const {content} = await note.getNoteComplement();
+ ({$highlightsList, hltLiCount} = this.getHighlightList(content, optionsHlt));
+ }
+ this.$highlightsList.empty().append($highlightsList);
+ if (hltLiCount > 0) {
+ this.toggleInt(true);
+ this.noteContext.viewScope.highlightedTextPreviousVisible = true;
+ } else {
+ this.toggleInt(false);
+ this.noteContext.viewScope.highlightedTextPreviousVisible = false;
+ }
+
+ this.triggerCommand("reEvaluateRightPaneVisibility");
+ }
+
+ getHighlightList(content, optionsHlt) {
+ // matches a span containing background-color
+ const regex1 = /]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
+ // matches a span containing color
+ const regex2 = /]*style\s*=\s*[^>]*[^-]color:[^>]*?>[\s\S]*?<\/span>/gi;
+ // match italics
+ const regex3 = /[\s\S]*?<\/i>/gi;
+ // match bold
+ const regex4 = /[\s\S]*?<\/strong>/gi;
+ // match underline
+ const regex5 = /[\s\S]*?<\/u>/g;
+ // Possible values in optionsHlt: '["bold","italic","underline","color","bgColor"]'
+ // element priority: span>i>strong>u
+ let findSubStr = "", combinedRegexStr = "";
+ if (optionsHlt.includes("bgColor")) {
+ findSubStr += `,span[style*="background-color"]`;
+ combinedRegexStr += `|${regex1.source}`;
+ }
+ if (optionsHlt.includes("color")) {
+ findSubStr += `,span[style*="color"]`;
+ combinedRegexStr += `|${regex2.source}`;
+ }
+ if (optionsHlt.includes("italic")) {
+ findSubStr += `,i`;
+ combinedRegexStr += `|${regex3.source}`;
+ }
+ if (optionsHlt.indexOf("bold")) {
+ findSubStr += `,strong`;
+ combinedRegexStr += `|${regex4.source}`;
+ }
+ if (optionsHlt.includes("underline")) {
+ findSubStr += `,u`;
+ combinedRegexStr += `|${regex5.source}`;
+ }
+
+ findSubStr = findSubStr.substring(1)
+ combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
+ const combinedRegex = new RegExp(combinedRegexStr, 'gi');
+ const $highlightsList = $("");
+ let prevEndIndex = -1, hltLiCount = 0;
+ for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) {
+ const subHtml = match[0];
+ const startIndex = match.index;
+ const endIndex = combinedRegex.lastIndex;
+ if (prevEndIndex !== -1 && startIndex === prevEndIndex) {
+ // If the previous element is connected to this element in HTML, then concatenate them into one.
+ $highlightsList.children().last().append(subHtml);
+ } else {
+ // TODO: can't be done with $(subHtml).text()?
+ const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
+
+ if (hasText) {
+ $highlightsList.append(
+ $('')
+ .html(subHtml)
+ .on("click", () => this.jumpToHighlightedText(findSubStr, hltIndex))
+ );
+
+ hltLiCount++;
+ } else {
+ // hide li if its text content is empty
+ continue;
+ }
+ }
+ prevEndIndex = endIndex;
+ }
+ return {
+ $highlightsList,
+ hltLiCount
+ };
+ }
+
+ async jumpToHighlightedText(findSubStr, itemIndex) {
+ const isReadOnly = await this.noteContext.isReadOnly();
+ let targetElement;
+ if (isReadOnly) {
+ const $container = await this.noteContext.getContentElement();
+ targetElement = $container.find(findSubStr).filter(function () {
+ if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
+ let color = this.style.color;
+ return !($(this).prop('tagName') === "SPAN" && color === "");
+ } else {
+ return true;
+ }
+ }).filter(function () {
+ return $(this).parent(findSubStr).length === 0
+ && $(this).parent().parent(findSubStr).length === 0
+ && $(this).parent().parent().parent(findSubStr).length === 0
+ && $(this).parent().parent().parent().parent(findSubStr).length === 0;
+ })
+ } else {
+ const textEditor = await this.noteContext.getTextEditor();
+ targetElement = $(textEditor.editing.view.domRoots.values().next().value).find(findSubStr).filter(function () {
+ // When finding span[style*="color"] but not looking for span[style*="background-color"],
+ // the background-color error will be regarded as color, so it needs to be filtered
+ if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
+ let color = this.style.color;
+ return !($(this).prop('tagName') === "SPAN" && color === "");
+ } else {
+ return true;
+ }
+ }).filter(function () {
+ // Need to filter out the child elements of the element that has been found
+ return $(this).parent(findSubStr).length === 0
+ && $(this).parent().parent(findSubStr).length === 0
+ && $(this).parent().parent().parent(findSubStr).length === 0
+ && $(this).parent().parent().parent().parent(findSubStr).length === 0;
+ })
+ }
+ targetElement[itemIndex].scrollIntoView({
+ behavior: "smooth", block: "center"
+ });
+ }
+
+ async closeHltCommand() {
+ this.noteContext.viewScope.highlightedTextTemporarilyHidden = true;
+ await this.refresh();
+ this.triggerCommand('reEvaluateRightPaneVisibility');
+ }
+
+ async entitiesReloadedEvent({loadResults}) {
+ if (loadResults.isNoteContentReloaded(this.noteId)) {
+ await this.refresh();
+ } else if (loadResults.getAttributes().find(attr => attr.type === 'label'
+ && (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
+ && attributeService.isAffecting(attr, this.note))) {
+ await this.refresh();
+ }
+ }
+}
+
+class CloseHltButton extends OnClickButtonWidget {
+ constructor() {
+ super();
+
+ this.icon("bx-x")
+ .title("Close HighlightedTextWidget")
+ .titlePlacement("bottom")
+ .onClick((widget, e) => {
+ e.stopPropagation();
+
+ widget.triggerCommand("closeHlt");
+ })
+ .class("icon-action close-highlists-list");
+ }
+}
diff --git a/src/public/app/widgets/ribbon_widgets/file_properties.js b/src/public/app/widgets/ribbon_widgets/file_properties.js
index 2080e71a4..633a3a60d 100644
--- a/src/public/app/widgets/ribbon_widgets/file_properties.js
+++ b/src/public/app/widgets/ribbon_widgets/file_properties.js
@@ -136,7 +136,7 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
const blob = await this.note.getBlob();
- this.$fileSize.text(`${blob.contentLength} bytes`);
+ this.$fileSize.text(utils.formatNoteSize(blob.contentLength));
// open doesn't work for protected notes since it works through a browser which isn't in protected session
this.$openButton.toggle(!note.isProtected);
diff --git a/src/public/app/widgets/ribbon_widgets/note_info_widget.js b/src/public/app/widgets/ribbon_widgets/note_info_widget.js
index 2416ffe3d..c6911150a 100644
--- a/src/public/app/widgets/ribbon_widgets/note_info_widget.js
+++ b/src/public/app/widgets/ribbon_widgets/note_info_widget.js
@@ -106,12 +106,12 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
this.$subTreeSize.empty().append($(' '));
const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
- this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize));
+ this.$noteSize.text(utils.formatNoteSize(noteSizeResp.noteSize));
const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`);
if (subTreeResp.subTreeNoteCount > 1) {
- this.$subTreeSize.text(`(subtree size: ${utils.formatSize(subTreeResp.subTreeSize)} in ${subTreeResp.subTreeNoteCount} notes)`);
+ this.$subTreeSize.text(`(subtree size: ${utils.formatNoteSize(subTreeResp.subTreeSize)} in ${subTreeResp.subTreeNoteCount} notes)`);
}
else {
this.$subTreeSize.text("");
diff --git a/src/public/app/widgets/tab_row.js b/src/public/app/widgets/tab_row.js
index ecddc9129..17f478a47 100644
--- a/src/public/app/widgets/tab_row.js
+++ b/src/public/app/widgets/tab_row.js
@@ -609,6 +609,17 @@ export default class TabRowWidget extends BasicWidget {
this.updateTabById(noteContext.mainNtxId || noteContext.ntxId);
}
+ noteContextReorderEvent({oldMainNtxId, newMainNtxId}) {
+ if (!oldMainNtxId || !newMainNtxId) {
+ // no need to update tab row
+ return;
+ }
+
+ // update tab id for the new main context
+ this.getTabById(oldMainNtxId).attr("data-ntx-id", newMainNtxId);
+ this.updateTabById(newMainNtxId);
+ }
+
updateTabById(ntxId) {
const $tab = this.getTabById(ntxId);
diff --git a/src/public/app/widgets/title_bar_buttons.js b/src/public/app/widgets/title_bar_buttons.js
index e125e555b..89dd43601 100644
--- a/src/public/app/widgets/title_bar_buttons.js
+++ b/src/public/app/widgets/title_bar_buttons.js
@@ -30,9 +30,13 @@ const TPL = `
height: 40px;
width: 40px;
}
+ .title-bar-buttons .top-btn.active{
+ background-color:var(--accented-background-color);
+ }
+
@@ -47,10 +51,34 @@ export default class TitleBarButtonsWidget extends BasicWidget {
this.$widget = $(TPL);
this.contentSized();
+ const $topBtn = this.$widget.find(".top-btn");
const $minimizeBtn = this.$widget.find(".minimize-btn");
const $maximizeBtn = this.$widget.find(".maximize-btn");
const $closeBtn = this.$widget.find(".close-btn");
+ // When the window is restarted, the window will not be reset when it is set to the top,
+ // so get the window status and set the icon background
+ setTimeout(() => {
+ const remote = utils.dynamicRequire('@electron/remote');
+ if (remote.BrowserWindow.getFocusedWindow()?.isAlwaysOnTop()) {
+ $topBtn.addClass('active');
+ }
+ }, 1000);
+
+ $topBtn.on('click', () => {
+ $topBtn.trigger('blur');
+ const remote = utils.dynamicRequire('@electron/remote');
+ const focusedWindow = remote.BrowserWindow.getFocusedWindow();
+ const isAlwaysOnTop = focusedWindow.isAlwaysOnTop()
+ if (isAlwaysOnTop) {
+ focusedWindow.setAlwaysOnTop(false)
+ $topBtn.removeClass('active');
+ } else {
+ focusedWindow.setAlwaysOnTop(true);
+ $topBtn.addClass('active');
+ }
+ });
+
$minimizeBtn.on('click', () => {
$minimizeBtn.trigger('blur');
const remote = utils.dynamicRequire('@electron/remote');
diff --git a/src/public/app/widgets/toc.js b/src/public/app/widgets/toc.js
index 85d415be2..1c7418d05 100644
--- a/src/public/app/widgets/toc.js
+++ b/src/public/app/widgets/toc.js
@@ -38,6 +38,10 @@ const TPL = `
+`;
+
+export default class HighlightedTextOptions extends OptionsWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.$hlt = this.$widget.find("input.highlighted-text-check");
+ this.$hlt.on('change', () => {
+ const hltVals = this.$widget.find('input.highlighted-text-check[type="checkbox"]:checked').map(function () {
+ return this.value;
+ }).get();
+ this.updateOption('highlightedText', JSON.stringify(hltVals));
+ });
+ }
+
+ async optionsLoaded(options) {
+ const hltVals = JSON.parse(options.highlightedText);
+ this.$widget.find('input.highlighted-text-check[type="checkbox"]').each(function () {
+ if ($.inArray($(this).val(), hltVals) !== -1) {
+ $(this).prop("checked", true);
+ } else {
+ $(this).prop("checked", false);
+ }
+ });
+ }
+}
diff --git a/src/routes/api/options.js b/src/routes/api/options.js
index ae72275df..ce40d6e3e 100644
--- a/src/routes/api/options.js
+++ b/src/routes/api/options.js
@@ -60,6 +60,7 @@ const ALLOWED_OPTIONS = new Set([
'compressImages',
'downloadImagesAutomatically',
'minTocHeadings',
+ 'highlightedText',
'checkForUpdates',
'disableTray',
'eraseUnusedAttachmentsAfterSeconds',
diff --git a/src/services/notes.js b/src/services/notes.js
index 39c313399..a4062ac5e 100644
--- a/src/services/notes.js
+++ b/src/services/notes.js
@@ -60,11 +60,10 @@ function deriveMime(type, mime) {
* @param {BNote} childNote
*/
function copyChildAttributes(parentNote, childNote) {
- const hasAlreadyTemplate = childNote.hasRelation('template');
-
for (const attr of parentNote.getAttributes()) {
if (attr.name.startsWith("child:")) {
const name = attr.name.substr(6);
+ const hasAlreadyTemplate = childNote.hasRelation('template');
if (hasAlreadyTemplate && attr.type === 'relation' && name === 'template') {
// if the note already has a template, it means the template was chosen by the user explicitly
@@ -181,7 +180,7 @@ function createNewNote(params) {
// TODO: think about what can happen if the note already exists with the forced ID
// I guess on DB it's going to be fine, but becca references between entities
- // might get messed up (two Note instance for the same ID existing in the references)
+ // might get messed up (two note instances for the same ID existing in the references)
note = new BNote({
noteId: params.noteId, // optionally can force specific noteId
title: params.title,
@@ -202,7 +201,7 @@ function createNewNote(params) {
}
finally {
if (!isEntityEventsDisabled) {
- // re-enable entity events only if there were previously enabled
+ // re-enable entity events only if they were previously enabled
// (they can be disabled in case of import)
cls.enableEntityEvents();
}
@@ -222,22 +221,14 @@ function createNewNote(params) {
copyChildAttributes(parentNote, note);
+ eventService.emit(eventService.ENTITY_CREATED, { entityName: 'notes', entity: note });
+ eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'notes', entity: note });
triggerNoteTitleChanged(note);
-
- eventService.emit(eventService.ENTITY_CREATED, {
- entityName: 'notes',
- entity: note
- });
-
- eventService.emit(eventService.ENTITY_CREATED, {
- entityName: 'branches',
- entity: branch
- });
-
- eventService.emit(eventService.CHILD_NOTE_CREATED, {
- childNote: note,
- parentNote: parentNote
- });
+ // blobs doesn't use "created" event
+ eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'blobs', entity: note });
+ eventService.emit(eventService.ENTITY_CREATED, { entityName: 'branches', entity: branch });
+ eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'branches', entity: branch });
+ eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote });
log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`);
diff --git a/src/services/options_init.js b/src/services/options_init.js
index 0ef3e2ff0..c08ec5992 100644
--- a/src/services/options_init.js
+++ b/src/services/options_init.js
@@ -86,6 +86,7 @@ const defaultOptions = [
{ name: 'compressImages', value: 'true', isSynced: true },
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
{ name: 'minTocHeadings', value: '5', isSynced: true },
+ { name: 'highlightedText', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
{ name: 'checkForUpdates', value: 'true', isSynced: true },
{ name: 'disableTray', value: 'false', isSynced: false },
{ name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true },
diff --git a/src/share/shaca/entities/snote.js b/src/share/shaca/entities/snote.js
index 257a1dd98..3d6cc4d96 100644
--- a/src/share/shaca/entities/snote.js
+++ b/src/share/shaca/entities/snote.js
@@ -42,7 +42,7 @@ class SNote extends AbstractShacaEntity {
/** @param {SAttribute[]|null} */
this.__attributeCache = null;
/** @param {SAttribute[]|null} */
- this.inheritableAttributeCache = null;
+ this.__inheritableAttributeCache = null;
/** @param {SAttribute[]} */
this.targetRelations = [];
@@ -192,11 +192,11 @@ class SNote extends AbstractShacaEntity {
}
}
- this.inheritableAttributeCache = [];
+ this.__inheritableAttributeCache = [];
for (const attr of this.__attributeCache) {
if (attr.isInheritable) {
- this.inheritableAttributeCache.push(attr);
+ this.__inheritableAttributeCache.push(attr);
}
}
}
@@ -210,11 +210,11 @@ class SNote extends AbstractShacaEntity {
return [];
}
- if (!this.inheritableAttributeCache) {
- this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
+ if (!this.__inheritableAttributeCache) {
+ this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
}
- return this.inheritableAttributeCache;
+ return this.__inheritableAttributeCache;
}
/** @returns {boolean} */
diff --git a/test-etapi/get-inherited-attribute-cloned.http b/test-etapi/get-inherited-attribute-cloned.http
new file mode 100644
index 000000000..06c1aa976
--- /dev/null
+++ b/test-etapi/get-inherited-attribute-cloned.http
@@ -0,0 +1,116 @@
+POST {{triliumHost}}/etapi/create-note
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "parentNoteId": "root",
+ "title": "Hello parent",
+ "type": "text",
+ "content": "Hi there!"
+}
+
+> {%
+client.assert(response.status === 201);
+client.global.set("parentNoteId", response.body.note.noteId);
+client.global.set("parentBranchId", response.body.branch.branchId);
+%}
+
+### Create inheritable parent attribute
+
+POST {{triliumHost}}/etapi/attributes
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "noteId": "{{parentNoteId}}",
+ "type": "label",
+ "name": "mylabel",
+ "value": "",
+ "isInheritable": true,
+ "position": 10
+}
+
+> {%
+client.assert(response.status === 201);
+client.global.set("parentAttributeId", response.body.attributeId);
+%}
+
+### Create child note under root
+
+POST {{triliumHost}}/etapi/create-note
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "parentNoteId": "root",
+ "title": "Hello child",
+ "type": "text",
+ "content": "Hi there!"
+}
+
+> {%
+client.assert(response.status === 201);
+client.global.set("childNoteId", response.body.note.noteId);
+client.global.set("childBranchId", response.body.branch.branchId);
+%}
+
+### Create child attribute
+
+POST {{triliumHost}}/etapi/attributes
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "noteId": "{{childNoteId}}",
+ "type": "label",
+ "name": "mylabel",
+ "value": "val",
+ "isInheritable": false,
+ "position": 10
+}
+
+> {%
+client.assert(response.status === 201);
+client.global.set("childAttributeId", response.body.attributeId);
+%}
+
+### Clone child to parent
+
+POST {{triliumHost}}/etapi/branches
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "noteId": "{{childNoteId}}",
+ "parentNoteId": "{{parentNoteId}}"
+}
+
+> {%
+client.assert(response.status === 201);
+client.assert(response.body.parentNoteId == client.global.get("parentNoteId"));
+%}
+
+###
+
+GET {{triliumHost}}/etapi/notes/{{childNoteId}}
+Authorization: {{authToken}}
+
+> {%
+
+function hasAttribute(list, attributeId) {
+ for (let i = 0; i < list.length; i++) {
+ if (list[i]["attributeId"] === attributeId) {
+ return true;
+ }
+ }
+ return false;
+}
+
+client.assert(response.status === 200);
+client.assert(response.body.noteId == client.global.get("childNoteId"));
+client.assert(response.body.attributes.length == 2);
+client.assert(hasAttribute(response.body.attributes,
+ client.global.get("parentAttributeId")));
+client.assert(hasAttribute(response.body.attributes,
+ client.global.get("childAttributeId")));
+%}
diff --git a/test-etapi/get-inherited-attribute.http b/test-etapi/get-inherited-attribute.http
new file mode 100644
index 000000000..d614f419e
--- /dev/null
+++ b/test-etapi/get-inherited-attribute.http
@@ -0,0 +1,44 @@
+POST {{triliumHost}}/etapi/attributes
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "noteId": "root",
+ "type": "label",
+ "name": "mylabel",
+ "value": "val",
+ "isInheritable": true
+}
+
+> {% client.global.set("createdAttributeId", response.body.attributeId); %}
+
+###
+
+POST {{triliumHost}}/etapi/create-note
+Authorization: {{authToken}}
+Content-Type: application/json
+
+{
+ "parentNoteId": "root",
+ "title": "Hello",
+ "type": "text",
+ "content": "Hi there!"
+}
+
+> {%
+client.global.set("createdNoteId", response.body.note.noteId);
+client.global.set("createdBranchId", response.body.branch.branchId);
+%}
+
+###
+
+GET {{triliumHost}}/etapi/notes/{{createdNoteId}}
+Authorization: {{authToken}}
+
+> {%
+client.assert(response.status === 200);
+client.assert(response.body.noteId == client.global.get("createdNoteId"));
+client.assert(response.body.attributes.length == 1);
+client.assert(response.body.attributes[0].attributeId ==
+ client.global.get("createdAttributeId"));
+%}