diff --git a/src/services/note_cache.js b/src/services/note_cache.js
index e4208d430..88f934c56 100644
--- a/src/services/note_cache.js
+++ b/src/services/note_cache.js
@@ -27,6 +27,8 @@ class Note {
this.title = row.title;
/** @param {boolean} */
this.isProtected = !!row.isProtected;
+ /** @param {boolean} */
+ this.isDecrypted = false;
/** @param {Note[]} */
this.parents = [];
/** @param {Note[]} */
@@ -82,6 +84,16 @@ class Note {
get isArchived() {
return this.hasAttribute('label', 'archived');
}
+
+ get hasInheritableOwnedArchivedLabel() {
+ return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
+ }
+
+ // will sort the parents so that non-archived are first and archived at the end
+ // this is done so that non-archived paths are always explored as first when searching for note path
+ resortParents() {
+ this.parents.sort((a, b) => a.hasInheritableOwnedArchivedLabel ? 1 : -1);
+ }
}
class Branch {
@@ -94,6 +106,8 @@ class Branch {
this.parentNoteId = row.parentNoteId;
/** @param {string} */
this.prefix = row.prefix;
+
+ childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
}
/** @return {Note} */
@@ -172,12 +186,6 @@ let loadedPromiseResolve;
/** Is resolved after the initial load */
let loadedPromise = new Promise(res => loadedPromiseResolve = res);
-let noteTitles = {};
-let protectedNoteTitles = {};
-let noteIds;
-const childToParent = {};
-let archived = {};
-
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
let prefixes = {};
@@ -236,8 +244,6 @@ async function load() {
childNote.parents.push(parentNote);
parentNote.children.push(childNote);
-
- childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`] = branch;
}
if (protectedSessionService.isProtectedSessionAvailable()) {
@@ -254,12 +260,31 @@ async function load() {
async function decryptProtectedNotes() {
for (const note of notes) {
- if (note.isProtected) {
+ if (note.isProtected && !note.isDecrypted) {
note.title = protectedSessionService.decryptString(note.title);
+
+ note.isDecrypted = true;
}
}
}
+function formatAttribute(attr) {
+ if (attr.type === 'relation') {
+ return '@' + utils.escapeHtml(attr.name) + "=…";
+ }
+ else if (attr.type === 'label') {
+ let label = '#' + utils.escapeHtml(attr.name);
+
+ if (attr.value) {
+ const val = /[^\w_-]/.test(attr.value) ? '"' + attr.value + '"' : attr.value;
+
+ label += '=' + utils.escapeHtml(val);
+ }
+
+ return label;
+ }
+}
+
function highlightResults(results, allTokens) {
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
// which would make the resulting HTML string invalid.
@@ -274,7 +299,7 @@ function highlightResults(results, allTokens) {
for (const attr of note.attributes) {
if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
- result.pathTitle += ` @${attr.name}=${attr.value}`;
+ result.pathTitle += ` ${formatAttribute(attr)}`;
}
}
@@ -296,6 +321,44 @@ function highlightResults(results, allTokens) {
}
}
+/**
+ * Returns noteIds which have at least one matching tokens
+ *
+ * @param tokens
+ * @return {Set}
+ */
+function getCandidateNotes(tokens) {
+ const candidateNoteIds = new Set();
+
+ for (const token of tokens) {
+ for (const noteId in fulltext) {
+ if (!fulltext[noteId].includes(token)) {
+ continue;
+ }
+
+ candidateNoteIds.add(noteId);
+ const note = notes[noteId];
+ const inheritableAttrs = note.ownedAttributes.filter(attr => attr.isInheritable);
+
+ searchingAttrs:
+ // for matching inheritable attributes, include the whole note subtree to the candidates
+ for (const attr of inheritableAttrs) {
+ const lcName = attr.name.toLowerCase();
+ const lcValue = attr.value.toLowerCase();
+
+ for (const token of tokens) {
+ if (lcName.includes(token) || lcValue.includes(token)) {
+ note.addSubTreeNoteIdsTo(candidateNoteIds);
+
+ break searchingAttrs;
+ }
+ }
+ }
+ }
+ }
+ return candidateNoteIds;
+}
+
async function findNotes(query) {
if (!query.length) {
return [];
@@ -307,41 +370,14 @@ async function findNotes(query) {
.split(/[ -]/)
.filter(token => token !== '/'); // '/' is used as separator
- const tokens = allTokens.slice();
+ const candidateNoteIds = getCandidateNotes(allTokens);
- const matchedNoteIds = new Set();
-
- for (const token of tokens) {
- for (const noteId in fulltext) {
- if (!fulltext[noteId].includes(token)) {
- continue;
- }
-
- matchedNoteIds.add(noteId);
- const note = notes[noteId];
- const inheritableAttrs = note.ownedAttributes.filter(attr => attr.isInheritable);
-
- searchingAttrs:
- for (const attr of inheritableAttrs) {
- const lcName = attr.name.toLowerCase();
- const lcValue = attr.value.toLowerCase();
-
- for (const token of tokens) {
- if (lcName.includes(token) || lcValue.includes(token)) {
- note.addSubTreeNoteIdsTo(matchedNoteIds);
-
- break searchingAttrs;
- }
- }
- }
- }
- }
-//console.log(matchedNoteIds);
// now we have set of noteIds which match at least one token
let results = [];
+ const tokens = allTokens.slice();
- for (const noteId of matchedNoteIds) {
+ for (const noteId of candidateNoteIds) {
const note = notes[noteId];
// autocomplete should be able to find notes by their noteIds as well (only leafs)
@@ -476,14 +512,17 @@ function search(note, tokens, path, results) {
function isNotePathArchived(notePath) {
const noteId = notePath[notePath.length - 1];
+ const note = notes[noteId];
- if (archived[noteId] !== undefined) {
+ if (note.isArchived) {
return true;
}
for (let i = 0; i < notePath.length - 1; i++) {
+ const note = notes[notePath[i]];
+
// this is going through parents so archived must be inheritable
- if (archived[notePath[i]] === 1) {
+ if (note.hasInheritableOwnedArchivedLabel) {
return true;
}
}
@@ -550,7 +589,7 @@ function getNoteTitle(childNoteId, parentNoteId) {
const branch = parentNote ? getBranch(childNote.noteId, parentNote.noteId) : null;
- return ((branch && branch.prefix) ? (branch.prefix + ' - ') : '') + title;
+ return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
}
function getNoteTitleArrayForPath(path) {
@@ -639,11 +678,11 @@ function getNotePath(noteId) {
}
}
-function evaluateSimilarity(text1, text2, noteId, results) {
- let coeff = stringSimilarity.compareTwoStrings(text1, text2);
+function evaluateSimilarity(text, note, results) {
+ let coeff = stringSimilarity.compareTwoStrings(text, note.title);
if (coeff > 0.4) {
- const notePath = getSomePath(noteId);
+ const notePath = getSomePath(note);
// this takes care of note hoisting
if (!notePath) {
@@ -654,7 +693,7 @@ function evaluateSimilarity(text1, text2, noteId, results) {
coeff -= 0.2; // archived penalization
}
- results.push({coeff, notePath, noteId});
+ results.push({coeff, notePath, noteId: note.noteId});
}
}
@@ -668,11 +707,16 @@ function setImmediatePromise() {
});
}
-async function evaluateSimilarityDict(title, dict, results) {
+async function findSimilarNotes(title) {
+ const results = [];
let i = 0;
- for (const noteId in dict) {
- evaluateSimilarity(title, dict[noteId], noteId, results);
+ for (const note of Object.values(notes)) {
+ if (note.isProtected && !note.isDecrypted) {
+ continue;
+ }
+
+ evaluateSimilarity(title, note, results);
i++;
@@ -680,16 +724,6 @@ async function evaluateSimilarityDict(title, dict, results) {
await setImmediatePromise();
}
}
-}
-
-async function findSimilarNotes(title) {
- const results = [];
-
- await evaluateSimilarityDict(title, noteTitles, results);
-
- if (protectedSessionService.isProtectedSessionAvailable()) {
- await evaluateSimilarityDict(title, protectedNoteTitles, results);
- }
results.sort((a, b) => a.coeff > b.coeff ? -1 : 1);
@@ -704,80 +738,84 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED
}
if (entityName === 'notes') {
- const note = entity;
+ const {noteId} = entity;
- if (note.isDeleted) {
- delete noteTitles[note.noteId];
- delete childToParent[note.noteId];
+ if (entity.isDeleted) {
+ delete notes[noteId];
+ }
+ else if (noteId in notes) {
+ // we can assume we have protected session since we managed to update
+ notes[noteId].title = entity.title;
+ notes[noteId].isDecrypted = true;
}
else {
- if (note.isProtected) {
- // we can assume we have protected session since we managed to update
- // removing from the maps is important when switching between protected & unprotected
- protectedNoteTitles[note.noteId] = note.title;
- delete noteTitles[note.noteId];
- }
- else {
- noteTitles[note.noteId] = note.title;
- delete protectedNoteTitles[note.noteId];
- }
+ notes[noteId] = new Note(entity);
}
}
else if (entityName === 'branches') {
- const branch = entity;
+ const {branchId, noteId, parentNoteId} = entity;
- if (branch.isDeleted) {
- if (branch.noteId in childToParent) {
- childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId);
+ if (entity.isDeleted) {
+ const childNote = notes[noteId];
+
+ if (childNote) {
+ childNote.parents = childNote.parents.filter(parent => parent.noteId !== parentNoteId);
}
- delete prefixes[branch.noteId + '-' + branch.parentNoteId];
- delete childParentToBranchId[branch.noteId + '-' + branch.parentNoteId];
+ const parentNote = notes[parentNoteId];
+
+ if (parentNote) {
+ childNote.children = childNote.children.filter(child => child.noteId !== noteId);
+ }
+
+ delete childParentToBranch[`${noteId}-${parentNoteId}`];
+ delete branches[branchId];
+ }
+ else if (branchId in branches) {
+ // only relevant thing which can change in a branch is prefix
+ branches[branchId].prefix = entity.prefix;
}
else {
- if (branch.prefix) {
- prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
+ branches[branchId] = new Branch(entity);
+
+ const note = notes[entity.noteId];
+
+ if (note) {
+ note.resortParents();
}
-
- childToParent[branch.noteId] = childToParent[branch.noteId] || [];
-
- if (!childToParent[branch.noteId].includes(branch.parentNoteId)) {
- childToParent[branch.noteId].push(branch.parentNoteId);
- }
-
- resortChildToParent(branch.noteId);
-
- childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
}
}
else if (entityName === 'attributes') {
- const attribute = entity;
+ const {attributeId, noteId} = entity;
- if (attribute.type === 'label' && attribute.name === 'archived') {
- // we're not using label object directly, since there might be other non-deleted archived label
- const archivedLabel = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label'
- AND name = 'archived' AND noteId = ?`, [attribute.noteId]);
+ if (entity.isDeleted) {
+ const note = notes[noteId];
- if (archivedLabel) {
- archived[attribute.noteId] = archivedLabel.isInheritable ? 1 : 0;
+ if (note) {
+ note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attributeId);
}
- else {
- delete archived[attribute.noteId];
+
+ delete attributes[entity.attributeId];
+ }
+ else if (attributeId in attributes) {
+ const attr = attributes[attributeId];
+
+ // attr name cannot change
+ attr.value = entity.value;
+ attr.isInheritable = entity.isInheritable;
+ }
+ else {
+ attributes[attributeId] = new Attribute(entity);
+
+ const note = notes[noteId];
+
+ if (note) {
+ note.ownedAttributes.push(attributes[attributeId]);
}
}
}
});
-// will sort the childs so that non-archived are first and archived at the end
-// this is done so that non-archived paths are always explored as first when searching for note path
-function resortChildToParent(noteId) {
- if (!(noteId in childToParent)) {
- return;
- }
-
- childToParent[noteId].sort((a, b) => archived[a] === 1 ? 1 : -1);
-}
-
/**
* @param noteId
* @returns {boolean} - true if note exists (is not deleted) and is available in current note hoisting