diff --git a/src/public/app/dialogs/add_link.js b/src/public/app/dialogs/add_link.js
index 43f67df85..47270c164 100644
--- a/src/public/app/dialogs/add_link.js
+++ b/src/public/app/dialogs/add_link.js
@@ -75,7 +75,7 @@ function updateTitleFormGroupVisibility() {
}
$form.on('submit', () => {
- const notePath = $autoComplete.getSelectedPath();
+ const notePath = $autoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -89,4 +89,4 @@ $form.on('submit', () => {
}
return false;
-});
\ No newline at end of file
+});
diff --git a/src/public/app/dialogs/attributes.js b/src/public/app/dialogs/attributes.js
index 2cf844420..e4264114c 100644
--- a/src/public/app/dialogs/attributes.js
+++ b/src/public/app/dialogs/attributes.js
@@ -269,7 +269,7 @@ function initKoPlugins() {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
noteAutocompleteService.initNoteAutocomplete($(element));
- $(element).setSelectedPath(bindingContext.$data.selectedPath);
+ $(element).setSelectedNotePath(bindingContext.$data.selectedPath);
$(element).on('autocomplete:selected', function (event, suggestion, dataset) {
bindingContext.$data.selectedPath = $(element).val().trim() ? suggestion.path : '';
diff --git a/src/public/app/dialogs/clone_to.js b/src/public/app/dialogs/clone_to.js
index eab144aac..14e231aae 100644
--- a/src/public/app/dialogs/clone_to.js
+++ b/src/public/app/dialogs/clone_to.js
@@ -52,7 +52,7 @@ async function cloneNotesTo(notePath) {
}
$form.on('submit', () => {
- const notePath = $noteAutoComplete.getSelectedPath();
+ const notePath = $noteAutoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -64,4 +64,4 @@ $form.on('submit', () => {
}
return false;
-});
\ No newline at end of file
+});
diff --git a/src/public/app/dialogs/include_note.js b/src/public/app/dialogs/include_note.js
index 71d3cb220..1aa2b149e 100644
--- a/src/public/app/dialogs/include_note.js
+++ b/src/public/app/dialogs/include_note.js
@@ -38,7 +38,7 @@ async function includeNote(notePath) {
}
$form.on('submit', () => {
- const notePath = $autoComplete.getSelectedPath();
+ const notePath = $autoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -50,4 +50,4 @@ $form.on('submit', () => {
}
return false;
-});
\ No newline at end of file
+});
diff --git a/src/public/app/dialogs/move_to.js b/src/public/app/dialogs/move_to.js
index 4dbc6bcca..afd9ceb5f 100644
--- a/src/public/app/dialogs/move_to.js
+++ b/src/public/app/dialogs/move_to.js
@@ -41,7 +41,7 @@ async function moveNotesTo(parentNoteId) {
}
$form.on('submit', () => {
- const notePath = $noteAutoComplete.getSelectedPath();
+ const notePath = $noteAutoComplete.getSelectedNotePath();
if (notePath) {
$dialog.modal('hide');
@@ -55,4 +55,4 @@ $form.on('submit', () => {
}
return false;
-});
\ No newline at end of file
+});
diff --git a/src/public/app/services/note_autocomplete.js b/src/public/app/services/note_autocomplete.js
index 4f6b6a575..c9ae9be8d 100644
--- a/src/public/app/services/note_autocomplete.js
+++ b/src/public/app/services/note_autocomplete.js
@@ -3,7 +3,7 @@ import appContext from "./app_context.js";
import utils from './utils.js';
// this key needs to have this value so it's hit by the tooltip
-const SELECTED_PATH_KEY = "data-note-path";
+const SELECTED_NOTE_PATH_KEY = "data-note-path";
async function autocompleteSource(term, cb) {
const result = await server.get('autocomplete'
@@ -12,8 +12,8 @@ async function autocompleteSource(term, cb) {
if (result.length === 0) {
result.push({
- pathTitle: "No results",
- path: ""
+ notePathTitle: "No results",
+ notePath: ""
});
}
@@ -25,7 +25,7 @@ function clearText($el) {
return;
}
- $el.setSelectedPath("");
+ $el.setSelectedNotePath("");
$el.autocomplete("val", "").trigger('change');
}
@@ -34,7 +34,7 @@ function showRecentNotes($el) {
return;
}
- $el.setSelectedPath("");
+ $el.setSelectedNotePath("");
$el.autocomplete("val", "");
$el.trigger('focus');
}
@@ -91,10 +91,10 @@ function initNoteAutocomplete($el, options) {
}, [
{
source: autocompleteSource,
- displayKey: 'pathTitle',
+ displayKey: 'notePathTitle',
templates: {
suggestion: function(suggestion) {
- return suggestion.highlightedTitle;
+ return suggestion.highlightedNotePathTitle;
}
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
@@ -102,7 +102,7 @@ function initNoteAutocomplete($el, options) {
}
]);
- $el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedPath(suggestion.path));
+ $el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedNotePath(suggestion.notePath));
$el.on('autocomplete:closed', () => {
if (!$el.val().trim()) {
clearText($el);
@@ -113,24 +113,24 @@ function initNoteAutocomplete($el, options) {
}
function init() {
- $.fn.getSelectedPath = function () {
+ $.fn.getSelectedNotePath = function () {
if (!$(this).val().trim()) {
return "";
} else {
- return $(this).attr(SELECTED_PATH_KEY);
+ return $(this).attr(SELECTED_NOTE_PATH_KEY);
}
};
- $.fn.setSelectedPath = function (path) {
- path = path || "";
+ $.fn.setSelectedNotePath = function (notePath) {
+ notePath = notePath || "";
- $(this).attr(SELECTED_PATH_KEY, path);
+ $(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
- .toggleClass("disabled", !path.trim())
- .attr(SELECTED_PATH_KEY, path); // we also set attr here so tooltip can be displayed
+ .toggleClass("disabled", !notePath.trim())
+ .attr(SELECTED_NOTE_PATH_KEY, notePath); // we also set attr here so tooltip can be displayed
};
}
@@ -139,4 +139,4 @@ export default {
initNoteAutocomplete,
showRecentNotes,
init
-}
\ No newline at end of file
+}
diff --git a/src/public/app/widgets/promoted_attributes.js b/src/public/app/widgets/promoted_attributes.js
index 58e025d07..859ca2291 100644
--- a/src/public/app/widgets/promoted_attributes.js
+++ b/src/public/app/widgets/promoted_attributes.js
@@ -200,7 +200,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
this.promotedAttributeChanged(event);
});
- $input.setSelectedPath(valueAttr.value);
+ $input.setSelectedNotePath(valueAttr.value);
}
else {
ws.logError("Unknown attribute type=" + valueAttr.type);
@@ -250,7 +250,7 @@ export default class PromotedAttributesWidget extends TabAwareWidget {
value = $attr.is(':checked') ? "true" : "false";
}
else if ($attr.prop("attribute-type") === "relation") {
- const selectedPath = $attr.getSelectedPath();
+ const selectedPath = $attr.getSelectedNotePath();
value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : "";
}
diff --git a/src/routes/api/autocomplete.js b/src/routes/api/autocomplete.js
index a8255b3eb..f5c416eb0 100644
--- a/src/routes/api/autocomplete.js
+++ b/src/routes/api/autocomplete.js
@@ -18,7 +18,7 @@ async function getAutocomplete(req) {
results = await getRecentNotes(activeNoteId);
}
else {
- results = await noteCacheService.findNotesWithFulltext(query);
+ results = await noteCacheService.findNotesForAutocomplete(query);
}
const msTaken = Date.now() - timestampStarted;
@@ -57,10 +57,9 @@ async function getRecentNotes(activeNoteId) {
const title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
return {
- path: rn.notePath,
- pathTitle: title,
- highlightedTitle: title,
- noteTitle: noteCacheService.getNoteTitleFromPath(rn.notePath)
+ notePath: rn.notePath,
+ notePathTitle: title,
+ highlightedNotePathTitle: utils.escapeHtml(title)
};
});
}
diff --git a/src/services/note_cache.js b/src/services/note_cache.js
index e52619f3d..b35c449e8 100644
--- a/src/services/note_cache.js
+++ b/src/services/note_cache.js
@@ -371,6 +371,8 @@ async function load() {
branches = await getMappedRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`,
row => new Branch(row));
+ attributeIndex = [];
+
attributes = await getMappedRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`,
row => new Attribute(row));
@@ -378,17 +380,7 @@ async function load() {
loadedPromiseResolve();
}
-const expression = {
- operator: 'and',
- operands: [
- {
- operator: 'exists',
- fieldName: 'hokus'
- }
- ]
-};
-
-class AndOp {
+class AndExp {
constructor(subExpressions) {
this.subExpressions = subExpressions;
}
@@ -402,7 +394,7 @@ class AndOp {
}
}
-class OrOp {
+class OrExp {
constructor(subExpressions) {
this.subExpressions = subExpressions;
}
@@ -441,7 +433,7 @@ class NoteSet {
}
}
-class ExistsOp {
+class ExistsExp {
constructor(attributeType, attributeName) {
this.attributeType = attributeType;
this.attributeName = attributeName;
@@ -469,7 +461,7 @@ class ExistsOp {
}
}
-class EqualsOp {
+class EqualsExp {
constructor(attributeType, attributeName, attributeValue) {
this.attributeType = attributeType;
this.attributeName = attributeName;
@@ -498,7 +490,7 @@ class EqualsOp {
}
}
-class NoteContentFulltextOp {
+class NoteContentFulltextExp {
constructor(tokens) {
this.tokens = tokens;
}
@@ -525,7 +517,7 @@ class NoteContentFulltextOp {
}
}
-class NoteCacheFulltextOp {
+class NoteCacheFulltextExp {
constructor(tokens) {
this.tokens = tokens;
}
@@ -569,7 +561,7 @@ class NoteCacheFulltextOp {
}
if (foundTokens.length > 0) {
- const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
+ const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
this.searchDownThePath(parentNote, remainingTokens, [note.noteId], resultNoteSet, searchContext);
}
@@ -651,6 +643,21 @@ class NoteCacheFulltextOp {
}
}
+class SearchResult {
+ constructor(notePathArray) {
+ this.notePathArray = notePathArray;
+ this.notePathTitle = getNoteTitleForPath(notePathArray);
+ }
+
+ get notePath() {
+ return this.notePathArray.join('/');
+ }
+
+ get noteId() {
+ return this.notePathArray[this.notePathArray.length - 1];
+ }
+}
+
async function findNotesWithExpression(expression) {
const hoistedNote = notes[hoistedNoteService.getHoistedNoteId()];
@@ -664,10 +671,27 @@ async function findNotesWithExpression(expression) {
noteIdToNotePath: {}
};
- expression.execute(allNoteSet, searchContext);
+ const noteSet = await expression.execute(allNoteSet, searchContext);
+
+ let searchResults = noteSet.notes
+ .map(note => searchContext.noteIdToNotePath[note.noteId] || getSomePath(note))
+ .filter(notePathArray => notePathArray.includes(hoistedNoteService.getHoistedNoteId()))
+ .map(notePathArray => new SearchResult(notePathArray));
+
+ // sort results by depth of the note. This is based on the assumption that more important results
+ // are closer to the note root.
+ searchResults.sort((a, b) => {
+ if (a.notePathArray.length === b.notePathArray.length) {
+ return a.notePathTitle < b.notePathTitle ? -1 : 1;
+ }
+
+ return a.notePathArray.length < b.notePathArray.length ? -1 : 1;
+ });
+
+ return searchResults;
}
-async function findNotesWithFulltext(query, searchInContent) {
+async function findNotesForAutocomplete(query) {
if (!query.trim().length) {
return [];
}
@@ -678,74 +702,54 @@ async function findNotesWithFulltext(query, searchInContent) {
.split(/[ -]/)
.filter(token => token !== '/'); // '/' is used as separator
- const cacheResults = findInNoteCache(tokens);
+ const expression = new NoteCacheFulltextExp(tokens);
- const contentResults = searchInContent ? await findInNoteContent(tokens) : [];
+ let searchResults = await findNotesWithExpression(expression);
- let results = cacheResults.concat(contentResults);
+ searchResults = searchResults.slice(0, 200);
- if (hoistedNoteService.getHoistedNoteId() !== 'root') {
- results = results.filter(res => res.pathArray.includes(hoistedNoteService.getHoistedNoteId()));
- }
-
- // sort results by depth of the note. This is based on the assumption that more important results
- // are closer to the note root.
- results.sort((a, b) => {
- if (a.pathArray.length === b.pathArray.length) {
- return a.title < b.title ? -1 : 1;
- }
-
- return a.pathArray.length < b.pathArray.length ? -1 : 1;
- });
-
- const apiResults = results.slice(0, 200).map(res => {
- const notePath = res.pathArray.join('/');
+ highlightSearchResults(searchResults, tokens);
+ return searchResults.map(result => {
return {
- noteId: res.noteId,
- branchId: res.branchId,
- path: notePath,
- pathTitle: res.titleArray.join(' / '),
- noteTitle: getNoteTitleFromPath(notePath)
- };
+ notePath: result.notePath,
+ notePathTitle: result.notePathTitle,
+ highlightedNotePathTitle: result.highlightedNotePathTitle
+ }
});
-
- highlightResults(apiResults, tokens);
-
- return apiResults;
}
-function highlightResults(results, allTokens) {
+function highlightSearchResults(searchResults, tokens) {
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
// which would make the resulting HTML string invalid.
// { and } are used for marking and tag (to avoid matches on single 'b' character)
- allTokens = allTokens.map(token => token.replace('/[<\{\}]/g', ''));
+ tokens = tokens.map(token => token.replace('/[<\{\}]/g', ''));
// sort by the longest so we first highlight longest matches
- allTokens.sort((a, b) => a.length > b.length ? -1 : 1);
+ tokens.sort((a, b) => a.length > b.length ? -1 : 1);
- for (const result of results) {
+ for (const result of searchResults) {
const note = notes[result.noteId];
+ result.highlightedNotePathTitle = result.notePathTitle;
+
for (const attr of note.attributes) {
- if (allTokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
- result.pathTitle += ` ${formatAttribute(attr)}`;
+ if (tokens.find(token => attr.name.includes(token) || attr.value.includes(token))) {
+ result.highlightedNotePathTitle += ` ${formatAttribute(attr)}`;
}
}
-
- result.highlightedTitle = result.pathTitle;
}
- for (const token of allTokens) {
+ for (const token of tokens) {
const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi");
- for (const result of results) {
- result.highlightedTitle = result.highlightedTitle.replace(tokenRegex, "{$1}");
+ for (const result of searchResults) {
+ result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(tokenRegex, "{$1}");
}
}
- for (const result of results) {
- result.highlightedTitle = result.highlightedTitle
+ for (const result of searchResults) {
+ result.highlightedNotePathTitle = result.highlightedNotePathTitle
.replace(/{/g, "")
.replace(/}/g, "");
}
@@ -839,17 +843,6 @@ function isInAncestor(noteId, ancestorNoteId) {
return false;
}
-function getNoteTitleFromPath(notePath) {
- const pathArr = notePath.split("/");
-
- if (pathArr.length === 1) {
- return getNoteTitle(pathArr[0], 'root');
- }
- else {
- return getNoteTitle(pathArr[pathArr.length - 1], pathArr[pathArr.length - 2]);
- }
-}
-
function getNoteTitle(childNoteId, parentNoteId) {
const childNote = notes[childNoteId];
const parentNote = notes[parentNoteId];
@@ -868,17 +861,17 @@ function getNoteTitle(childNoteId, parentNoteId) {
return ((branch && branch.prefix) ? `${branch.prefix} - ` : '') + title;
}
-function getNoteTitleArrayForPath(path) {
+function getNoteTitleArrayForPath(notePathArray) {
const titles = [];
- if (path[0] === hoistedNoteService.getHoistedNoteId() && path.length === 1) {
+ if (notePathArray[0] === hoistedNoteService.getHoistedNoteId() && notePathArray.length === 1) {
return [ getNoteTitle(hoistedNoteService.getHoistedNoteId()) ];
}
let parentNoteId = 'root';
let hoistedNotePassed = false;
- for (const noteId of path) {
+ for (const noteId of notePathArray) {
// start collecting path segment titles only after hoisted note
if (hoistedNotePassed) {
const title = getNoteTitle(noteId, parentNoteId);
@@ -896,8 +889,8 @@ function getNoteTitleArrayForPath(path) {
return titles;
}
-function getNoteTitleForPath(path) {
- const titles = getNoteTitleArrayForPath(path);
+function getNoteTitleForPath(notePathArray) {
+ const titles = getNoteTitleArrayForPath(notePathArray);
return titles.join(' / ');
}
@@ -1153,10 +1146,9 @@ sqlInit.dbReady.then(() => utils.stopWatch("Note cache load", load));
module.exports = {
loadedPromise,
- findNotesWithFulltext,
+ findNotesForAutocomplete,
getNotePath,
getNoteTitleForPath,
- getNoteTitleFromPath,
isAvailable,
isArchived,
isInAncestor,