diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js index db40d8275..2443ff888 100644 --- a/src/public/app/services/froca.js +++ b/src/public/app/services/froca.js @@ -176,7 +176,7 @@ class Froca { return; } - const {searchResultNoteIds, highlightedTokens} = await server.get('search-note/' + note.noteId); + const {searchResultNoteIds, highlightedTokens, error} = await server.get('search-note/' + note.noteId); if (!Array.isArray(searchResultNoteIds)) { throw new Error(`Search note '${note.noteId}' failed: ${searchResultNoteIds}`); @@ -208,6 +208,8 @@ class Froca { froca.notes[note.noteId].searchResultsLoaded = true; froca.notes[note.noteId].highlightedTokens = highlightedTokens; + + return {error}; } /** @returns {NoteShort[]} */ diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.js b/src/public/app/widgets/ribbon_widgets/search_definition.js index 4b6c1d785..1cc8da73a 100644 --- a/src/public/app/widgets/ribbon_widgets/search_definition.js +++ b/src/public/app/widgets/ribbon_widgets/search_definition.js @@ -250,7 +250,11 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { async refreshResultsCommand() { try { - await froca.loadSearchNote(this.noteId); + const {error} = await froca.loadSearchNote(this.noteId); + + if (error) { + this.handleEvent('showSearchError', { error }); + } } catch (e) { toastService.showError(e.message); diff --git a/src/public/app/widgets/search_options/search_string.js b/src/public/app/widgets/search_options/search_string.js index ebfd03a95..b89510f04 100644 --- a/src/public/app/widgets/search_options/search_string.js +++ b/src/public/app/widgets/search_options/search_string.js @@ -71,6 +71,18 @@ export default class SearchString extends AbstractSearchOption { return $option; } + showSearchErrorEvent({error}) { + this.$searchString.tooltip({ + trigger: 'manual', + title: "Search error: " + error, + placement: 'bottom' + }); + + this.$searchString.tooltip("show"); + + setTimeout(() => this.$searchString.tooltip("dispose"), 4000); + } + focusOnSearchDefinitionEvent() { this.$searchString.focus(); } diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index f093c236e..24086dfd5 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -385,9 +385,10 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th font-size: var(--main-font-size) !important; } -.tooltip .arrow::before { - border-right-color: var(--main-border-color) !important; -} +.bs-tooltip-bottom .arrow::before { border-bottom-color: var(--main-border-color) !important; } +.bs-tooltip-top .arrow::before { border-top-color: var(--main-border-color) !important; } +.bs-tooltip-left .arrow::before { border-left-color: var(--main-border-color) !important; } +.bs-tooltip-right .arrow::before { border-right-color: var(--main-border-color) !important; } .note-tooltip.tooltip .arrow { display: none; diff --git a/src/services/search/expressions/and.js b/src/services/search/expressions/and.js index 7178c1e20..eacb1d0a1 100644 --- a/src/services/search/expressions/and.js +++ b/src/services/search/expressions/and.js @@ -21,9 +21,9 @@ class AndExp extends Expression { this.subExpressions = subExpressions; } - execute(inputNoteSet, executionContext) { + execute(inputNoteSet, executionContext, searchContext) { for (const subExpression of this.subExpressions) { - inputNoteSet = subExpression.execute(inputNoteSet, executionContext); + inputNoteSet = subExpression.execute(inputNoteSet, executionContext, searchContext); } return inputNoteSet; diff --git a/src/services/search/expressions/child_of.js b/src/services/search/expressions/child_of.js index 87085f120..bbfc4a6ef 100644 --- a/src/services/search/expressions/child_of.js +++ b/src/services/search/expressions/child_of.js @@ -10,14 +10,14 @@ class ChildOfExp extends Expression { this.subExpression = subExpression; } - execute(inputNoteSet, executionContext) { + execute(inputNoteSet, executionContext, searchContext) { const subInputNoteSet = new NoteSet(); for (const note of inputNoteSet.notes) { subInputNoteSet.addAll(note.parents); } - const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext); + const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext, searchContext); const resNoteSet = new NoteSet(); diff --git a/src/services/search/expressions/descendant_of.js b/src/services/search/expressions/descendant_of.js index 421e6e77f..918bd352e 100644 --- a/src/services/search/expressions/descendant_of.js +++ b/src/services/search/expressions/descendant_of.js @@ -11,9 +11,9 @@ class DescendantOfExp extends Expression { this.subExpression = subExpression; } - execute(inputNoteSet, executionContext) { + execute(inputNoteSet, executionContext, searchContext) { const subInputNoteSet = new NoteSet(Object.values(becca.notes)); - const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext); + const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext, searchContext); const subTreeNoteSet = new NoteSet(); diff --git a/src/services/search/expressions/expression.js b/src/services/search/expressions/expression.js index 1b7d00e3f..95d5df71a 100644 --- a/src/services/search/expressions/expression.js +++ b/src/services/search/expressions/expression.js @@ -8,9 +8,10 @@ class Expression { /** * @param {NoteSet} inputNoteSet * @param {object} executionContext + * @param {SearchContext} searchContext * @return {NoteSet} */ - execute(inputNoteSet, executionContext) {} + execute(inputNoteSet, executionContext, searchContext) {} } module.exports = Expression; diff --git a/src/services/search/expressions/not.js b/src/services/search/expressions/not.js index 43986504a..3117b4a01 100644 --- a/src/services/search/expressions/not.js +++ b/src/services/search/expressions/not.js @@ -9,8 +9,8 @@ class NotExp extends Expression { this.subExpression = subExpression; } - execute(inputNoteSet, executionContext) { - const subNoteSet = this.subExpression.execute(inputNoteSet, executionContext); + execute(inputNoteSet, executionContext, searchContext) { + const subNoteSet = this.subExpression.execute(inputNoteSet, executionContext, searchContext); return inputNoteSet.minus(subNoteSet); } diff --git a/src/services/search/expressions/note_content_fulltext.js b/src/services/search/expressions/note_content_fulltext.js index e6c182ed6..ae8946aab 100644 --- a/src/services/search/expressions/note_content_fulltext.js +++ b/src/services/search/expressions/note_content_fulltext.js @@ -8,7 +8,7 @@ const protectedSessionService = require('../../protected_session'); const striptags = require('striptags'); const utils = require("../../utils"); -const ALLOWED_OPERATORS = ['*=*', '=', '*=', '=*', '%=']; +const ALLOWED_OPERATORS = ['=', '!=', '*=*', '*=', '=*', '%=']; const cachedRegexes = {}; @@ -24,17 +24,19 @@ class NoteContentFulltextExp extends Expression { constructor(operator, {tokens, raw, flatText}) { super(); - if (!ALLOWED_OPERATORS.includes(operator)) { - throw new Error(`Note content can be searched only with operators: ` + ALLOWED_OPERATORS.join(", ") + `, operator ${operator} given.`); - } - this.operator = operator; this.tokens = tokens; this.raw = !!raw; this.flatText = !!flatText; } - execute(inputNoteSet) { + execute(inputNoteSet, executionContext, searchContext) { + if (!ALLOWED_OPERATORS.includes(this.operator)) { + searchContext.addError(`Note content can be searched only with operators: ` + ALLOWED_OPERATORS.join(", ") + `, operator ${this.operator} given.`); + + return inputNoteSet; + } + const resultNoteSet = new NoteSet(); const sql = require('../../sql'); @@ -66,6 +68,7 @@ class NoteContentFulltextExp extends Expression { const [token] = this.tokens; if ((this.operator === '=' && token === content) + || (this.operator === '!=' && token !== content) || (this.operator === '*=' && content.endsWith(token)) || (this.operator === '=*' && content.startsWith(token)) || (this.operator === '*=*' && content.includes(token)) diff --git a/src/services/search/expressions/order_by_and_limit.js b/src/services/search/expressions/order_by_and_limit.js index af774da46..45f64e47b 100644 --- a/src/services/search/expressions/order_by_and_limit.js +++ b/src/services/search/expressions/order_by_and_limit.js @@ -20,8 +20,8 @@ class OrderByAndLimitExp extends Expression { this.subExpression = null; // it's expected to be set after construction } - execute(inputNoteSet, executionContext) { - let {notes} = this.subExpression.execute(inputNoteSet, executionContext); + execute(inputNoteSet, executionContext, searchContext) { + let {notes} = this.subExpression.execute(inputNoteSet, executionContext, searchContext); notes.sort((a, b) => { for (const {valueExtractor, smaller, larger} of this.orderDefinitions) { diff --git a/src/services/search/expressions/parent_of.js b/src/services/search/expressions/parent_of.js index 6fb2ebdce..9a474ed40 100644 --- a/src/services/search/expressions/parent_of.js +++ b/src/services/search/expressions/parent_of.js @@ -10,14 +10,14 @@ class ParentOfExp extends Expression { this.subExpression = subExpression; } - execute(inputNoteSet, executionContext) { + execute(inputNoteSet, executionContext, searchContext) { const subInputNoteSet = new NoteSet(); for (const note of inputNoteSet.notes) { subInputNoteSet.addAll(note.children); } - const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext); + const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext, searchContext); const resNoteSet = new NoteSet(); diff --git a/src/services/search/expressions/relation_where.js b/src/services/search/expressions/relation_where.js index f230de282..ee9f26081 100644 --- a/src/services/search/expressions/relation_where.js +++ b/src/services/search/expressions/relation_where.js @@ -12,7 +12,7 @@ class RelationWhereExp extends Expression { this.subExpression = subExpression; } - execute(inputNoteSet, executionContext) { + execute(inputNoteSet, executionContext, searchContext) { const candidateNoteSet = new NoteSet(); for (const attr of becca.findAttributes('relation', this.relationName)) { @@ -20,7 +20,7 @@ class RelationWhereExp extends Expression { if (inputNoteSet.hasNoteId(note.noteId) && attr.targetNote) { const subInputNoteSet = new NoteSet([attr.targetNote]); - const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext); + const subResNoteSet = this.subExpression.execute(subInputNoteSet, executionContext, searchContext); if (subResNoteSet.hasNote(attr.targetNote)) { if (attr.isInheritable) { diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index 2f72e0b1b..eb3ba5a2d 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -18,6 +18,7 @@ const AncestorExp = require("../expressions/ancestor"); const buildComparator = require('./build_comparator'); const ValueExtractor = require('../value_extractor'); const utils = require("../../utils"); +const TrueExp = require("../expressions/true.js"); function getFulltext(tokens, searchContext) { tokens = tokens.map(t => utils.removeDiacritic(t.token)); @@ -417,11 +418,22 @@ function getExpression(tokens, searchContext, level = 0) { } function parse({fulltextTokens, expressionTokens, searchContext}) { + let expression; + + try { + expression = getExpression(expressionTokens, searchContext); + } + catch (e) { + searchContext.addError(e.message); + + expression = new TrueExp(); + } + let exp = AndExp.of([ searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", "=", "false"), (searchContext.ancestorNoteId && searchContext.ancestorNoteId !== 'root') ? new AncestorExp(searchContext.ancestorNoteId, searchContext.ancestorDepth) : null, getFulltext(fulltextTokens, searchContext), - getExpression(expressionTokens, searchContext) + expression ]); if (searchContext.orderBy && searchContext.orderBy !== 'relevancy') { diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index dee2816c6..f8ddd9bd6 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -17,6 +17,7 @@ function searchFromNote(note) { const searchScript = note.getRelationValue('searchScript'); const searchString = note.getLabelValue('searchString'); + let error = null; if (searchScript) { searchResultNoteIds = searchFromRelation(note, 'searchScript'); @@ -38,13 +39,15 @@ function searchFromNote(note) { .map(sr => sr.noteId); highlightedTokens = searchContext.highlightedTokens; + error = searchContext.getError(); } // we won't return search note's own noteId // also don't allow root since that would force infinite cycle return { searchResultNoteIds: searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId)), - highlightedTokens + highlightedTokens, + error: error }; } @@ -148,7 +151,7 @@ function findResultsWithExpression(expression, searchContext) { noteIdToNotePath: {} }; - const noteSet = expression.execute(allNoteSet, executionContext); + const noteSet = expression.execute(allNoteSet, executionContext, searchContext); const searchResults = noteSet.notes .map(note => { @@ -197,7 +200,15 @@ function findResultsWithExpression(expression, searchContext) { function parseQueryToExpression(query, searchContext) { const {fulltextTokens, expressionTokens} = lex(query); - const structuredExpressionTokens = handleParens(expressionTokens); + let structuredExpressionTokens; + + try { + structuredExpressionTokens = handleParens(expressionTokens); + } + catch (e) { + structuredExpressionTokens = []; + searchContext.addError(e.message); + } const expression = parse({ fulltextTokens,