diff --git a/docs/backend_api/AbstractBeccaEntity.html b/docs/backend_api/AbstractBeccaEntity.html index e13c799bc9..328d08f0c2 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 8f6af88c62..29eb98f903 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 ae6e4e0141..f75738ee7f 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 1fab948698..2c6eb82187 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 d38f35c0fe..b1d3e2b608 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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
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 0951fa14d2..3a9ccf0d3e 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 b5990cb927..849051190c 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 b36b38298e..cdda77c51b 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 12800f1610..ba6ffbc2b3 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 4036de3f66..b64387b2d2 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 83fff96e55..d9715c60b0 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 b4fa7f5674..b2261711d5 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 92ab19328e..3298435224 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 97889071a5..0c729d838e 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 c140f1d107..ea663e51f8 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 ab2c22f519..0f2856fdcb 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 2e2f5ccb1a..02f6858fb0 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 55d778a7e6..f9f7919b5f 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 6517bc0d48..2605d65303 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 44f40bed62..bffce4bcd6 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 ec2ac56e69..783bf8301f 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 641c3a7372..901dc598c1 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 284bc72d5a..4ec45ea97e 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 cff8d1e8fa..cf7e9782c0 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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeAttributesDefaultDescription
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:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
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 8c1bb3d940..1cac29fba6 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 e866a8473e..5e94a32c20 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:
- - - - - - - - - - - - - - - - -

CollapsibleWidget

- - - - - - - - - - -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeDescription
- - -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:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeDescription
- - -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:
- - - - - - - -
- - - - - - - - -

TabAwareWidget

- - - - - - - - - - -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeDescription
- - -NoteContextAwareWidget - - - -
- - - - -
- - - - - - - - - - - - - - - - -
Deprecated:
  • use NoteContextAwareWidget instead
- - - - - - - - - - - -
Source:
-
- - - - - - - -
- - - - - - - - -

TabCachingWidget

- - - - - - - - - - -
Properties:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeDescription
- - -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 dffe972bcd..a9cb49a5f4 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 2b7ced98da..deae06846d 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 a31f615a28..84616e2cbf 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 6ea35e95aa..252ea6a2fb 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 08a8eb1598..6465f193ac 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 a326810c3e..679f2e5eb1 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 d47b771256..56d259cbcd 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 4847054300..e0991a0c98 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 fc93d19324..2b2aba663f 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 64599cc139..3771f0f05f 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 6886e08454..fb8b2ad999 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 3c267b9a93..c5492c8e4e 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 26f79da01e..44a5fa8f65 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 42b1608a77..9e0716cb34 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 2f461fec79..efbafc7403 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 45263eaa77..4a6f0c8474 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 927a83bd34..76a05b7e2d 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 9318dd8ab6..efcd3f6f7e 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 0000000000..e69de29bb2 diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js index 5d66435073..ddf7e9fbb9 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 690dcac6f9..220fd2cca2 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 0000000000..632651ca5f --- /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 e706c64472..ed5af80224 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 0000000000..4e88e9e453 --- /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( + $('
  1. ') + .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 2080e71a42..633a3a60d4 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 2416ffe3d2..c6911150a4 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 ecddc9129f..17f478a477 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 e125e555b2..89dd436017 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 85d415be22..1c7418d05f 100644 --- a/src/public/app/widgets/toc.js +++ b/src/public/app/widgets/toc.js @@ -38,6 +38,10 @@ const TPL = `
    .toc li { cursor: pointer; + text-align: justify; + text-justify: distribute; + word-wrap: break-word; + hyphens: auto; } .toc li:hover { @@ -80,6 +84,16 @@ export default class TocWidget extends RightPanelWidget { } async refreshWithNote(note) { + /*The reason for adding tocPreviousVisible is to record whether the previous state of the toc 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 highlighttext but not toc, every time the note content is changed, + * toc will appear and then close immediately, because getToc(html) function will consume time*/ + if (this.noteContext.viewScope.tocPreviousVisible ==true){ + this.toggleInt(true); + }else{ + this.toggleInt(false); + } + const tocLabel = note.getLabel('toc'); if (tocLabel?.value === 'hide') { @@ -96,10 +110,13 @@ export default class TocWidget extends RightPanelWidget { } this.$toc.html($toc); - this.toggleInt( - ["", "show"].includes(tocLabel?.value) - || headingCount >= options.getInt('minTocHeadings') - ); + if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt('minTocHeadings')){ + this.toggleInt(true); + this.noteContext.viewScope.tocPreviousVisible=true; + }else{ + this.toggleInt(false); + this.noteContext.viewScope.tocPreviousVisible=false; + } this.triggerCommand("reEvaluateRightPaneVisibility"); } diff --git a/src/public/app/widgets/type_widgets/content_widget.js b/src/public/app/widgets/type_widgets/content_widget.js index 7dad884044..9c075ef469 100644 --- a/src/public/app/widgets/type_widgets/content_widget.js +++ b/src/public/app/widgets/type_widgets/content_widget.js @@ -7,6 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js"; import KeyboardShortcutsOptions from "./options/shortcuts.js"; import HeadingStyleOptions from "./options/text_notes/heading_style.js"; import TableOfContentsOptions from "./options/text_notes/table_of_contents.js"; +import HighlightedTextOptions from "./options/text_notes/highlighted_text.js"; import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js"; import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js"; import WrapLinesOptions from "./options/code_notes/wrap_lines.js"; @@ -62,6 +63,7 @@ const CONTENT_WIDGETS = { _optionsTextNotes: [ HeadingStyleOptions, TableOfContentsOptions, + HighlightedTextOptions, TextAutoReadOnlySizeOptions ], _optionsCodeNotes: [ diff --git a/src/public/app/widgets/type_widgets/options/shortcuts.js b/src/public/app/widgets/type_widgets/options/shortcuts.js index a2c89437b5..b37975b086 100644 --- a/src/public/app/widgets/type_widgets/options/shortcuts.js +++ b/src/public/app/widgets/type_widgets/options/shortcuts.js @@ -116,11 +116,11 @@ export default class KeyboardShortcutsOptions extends OptionsWidget { return; } - $table.find('input.form-control').each(function() { - const defaultShortcuts = this.$widget.find(this).attr('data-default-keyboard-shortcuts'); + $table.find('input.form-control').each((_index, el) => { + const defaultShortcuts = this.$widget.find(el).attr('data-default-keyboard-shortcuts'); - if (this.$widget.find(this).val() !== defaultShortcuts) { - this.$widget.find(this) + if (this.$widget.find(el).val() !== defaultShortcuts) { + this.$widget.find(el) .val(defaultShortcuts) .trigger('change'); } diff --git a/src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js b/src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js new file mode 100644 index 0000000000..72d0ccd49b --- /dev/null +++ b/src/public/app/widgets/type_widgets/options/text_notes/highlighted_text.js @@ -0,0 +1,40 @@ +import OptionsWidget from "../options_widget.js"; + +const TPL = ` +
    +

    Highlighted Text

    + +

    You can customize the highlighted text displayed in the right panel:

    + +
    + + + + + +
    +`; + +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 ae72275df6..ce40d6e3e5 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 39c313399f..a4062ac5e9 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 0ef3e2ff0e..c08ec59921 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 257a1dd984..3d6cc4d964 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 0000000000..06c1aa976d --- /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 0000000000..d614f419e9 --- /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")); +%}