From d7e46263beed735654f1a8ac5818739a7dd71771 Mon Sep 17 00:00:00 2001 From: zadam Date: Mon, 18 Jan 2021 22:52:07 +0100 Subject: [PATCH] UI fixes to search definition + backend support --- package-lock.json | 6 +- package.json | 2 +- src/public/app/services/date_notes.js | 2 +- .../app/services/dialog_command_executor.js | 6 +- src/public/app/services/note_autocomplete.js | 4 +- src/public/app/widgets/note_list.js | 20 ++++++ src/public/app/widgets/search_definition.js | 65 ++++++++++++------- src/routes/api/search.js | 12 ++-- src/services/search/expressions/ancestor.js | 28 ++++++++ .../search/expressions/order_by_and_limit.js | 2 +- src/services/search/expressions/sub_tree.js | 28 -------- src/services/search/search_context.js | 8 ++- src/services/search/services/parse.js | 23 +++++-- src/services/search/services/search.js | 4 +- 14 files changed, 134 insertions(+), 76 deletions(-) create mode 100644 src/services/search/expressions/ancestor.js delete mode 100644 src/services/search/expressions/sub_tree.js diff --git a/package-lock.json b/package-lock.json index 586f3cc72..b3a30d031 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4122,9 +4122,9 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "helmet": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.3.1.tgz", - "integrity": "sha512-WsafDyKsIexB0+pUNkq3rL1rB5GVAghR68TP8ssM9DPEMzfBiluEQlVzJ/FEj6Vq2Ag3CNuxf7aYMjXrN0X49Q==" + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.4.1.tgz", + "integrity": "sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw==" }, "hosted-git-info": { "version": "2.8.5", diff --git a/package.json b/package.json index 735632269..47dcdfc3b 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "express": "4.17.1", "express-session": "1.17.1", "fs-extra": "9.0.1", - "helmet": "4.3.1", + "helmet": "4.4.1", "html": "1.0.0", "html2plaintext": "2.1.2", "http-proxy-agent": "4.0.1", diff --git a/src/public/app/services/date_notes.js b/src/public/app/services/date_notes.js index 42a9a46bd..7b46497c7 100644 --- a/src/public/app/services/date_notes.js +++ b/src/public/app/services/date_notes.js @@ -47,7 +47,7 @@ async function createSearchNote(opts = {}) { const note = await server.post('search-note'); const attrsToUpdate = [ - opts.subTreeNoteId ? { type: 'label', name: 'subTreeNoteId', value: opts.subTreeNoteId } : undefined, + opts.ancestor ? { type: 'relation', name: 'ancestor', value: opts.ancestorNoteId } : undefined, opts.searchString ? { type: 'label', name: 'searchString', value: opts.searchString } : undefined ].filter(attr => !!attr); diff --git a/src/public/app/services/dialog_command_executor.js b/src/public/app/services/dialog_command_executor.js index f422d8df3..2b149f5db 100644 --- a/src/public/app/services/dialog_command_executor.js +++ b/src/public/app/services/dialog_command_executor.js @@ -67,8 +67,8 @@ export default class DialogCommandExecutor extends Component { appContext.triggerCommand('focusOnDetail', {tabId: tabContext.tabId}); } - async searchNotesCommand({searchString, subTreeNoteId}) { - const searchNote = await dateNoteService.createSearchNote({searchString, subTreeNoteId}); + async searchNotesCommand({searchString, ancestorNoteId}) { + const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId}); const tabContext = await appContext.tabManager.openTabWithNote(searchNote.noteId, true); @@ -78,7 +78,7 @@ export default class DialogCommandExecutor extends Component { async searchInSubtreeCommand({notePath}) { const noteId = treeService.getNoteIdFromNotePath(notePath); - this.searchNotesCommand({subTreeNoteId: noteId}); + this.searchNotesCommand({ancestorNoteId: noteId}); } showBackendLogCommand() { diff --git a/src/public/app/services/note_autocomplete.js b/src/public/app/services/note_autocomplete.js index 6d847a1cd..4afcf07dd 100644 --- a/src/public/app/services/note_autocomplete.js +++ b/src/public/app/services/note_autocomplete.js @@ -23,6 +23,8 @@ async function autocompleteSourceForCKEditor(queryText) { highlightedNotePathTitle: row.highlightedNotePathTitle } })); + }, { + allowCreatingNotes: true }); }); } @@ -34,7 +36,7 @@ async function autocompleteSource(term, cb, options = {}) { + '?query=' + encodeURIComponent(term) + '&activeNoteId=' + activeNoteId); - if (term.trim().length >= 1) { + if (term.trim().length >= 1 && options.allowCreatingNotes) { results = [ { action: 'create-note', diff --git a/src/public/app/widgets/note_list.js b/src/public/app/widgets/note_list.js index 29eb17fdd..2a050bade 100644 --- a/src/public/app/widgets/note_list.js +++ b/src/public/app/widgets/note_list.js @@ -48,6 +48,10 @@ export default class NoteListWidget extends TabAwareWidget { } checkRenderStatus() { + // console.log("this.isIntersecting", this.isIntersecting); + // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId); + // console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId); + if (this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) { @@ -62,6 +66,11 @@ export default class NoteListWidget extends TabAwareWidget { await noteListRenderer.renderList(); } + /** + * We have this event so that we evaluate intersection only after note detail is loaded. + * If it's evaluated before note detail then it's clearly intersected (visible) although after note detail load + * it is not intersected (visible) anymore. + */ noteDetailRefreshedEvent({tabId}) { if (!this.isTab(tabId)) { return; @@ -72,6 +81,17 @@ export default class NoteListWidget extends TabAwareWidget { setTimeout(() => this.checkRenderStatus(), 100); } + searchRefreshedEvent({tabId}) { + if (!this.isTab(tabId)) { + return; + } + + this.noteIdRefreshed = this.noteId; + this.shownNoteId = null; + + this.checkRenderStatus(); + } + autoBookDisabledEvent({tabContext}) { if (this.isTab(tabContext.tabId)) { this.refresh(); diff --git a/src/public/app/widgets/search_definition.js b/src/public/app/widgets/search_definition.js index b85405ee4..e7db8f0c1 100644 --- a/src/public/app/widgets/search_definition.js +++ b/src/public/app/widgets/search_definition.js @@ -4,6 +4,7 @@ import server from "../services/server.js"; import TabAwareWidget from "./tab_aware_widget.js"; import treeCache from "../services/tree_cache.js"; import ws from "../services/ws.js"; +import utils from "../services/utils.js"; const TPL = `
@@ -64,9 +65,9 @@ const TPL = ` Add search option: - - @@ -257,6 +260,11 @@ export default class SearchDefinitionWidget extends TabAwareWidget { this.$searchString = this.$widget.find(".search-string"); this.$searchString.on('input', () => this.searchStringSU.scheduleUpdate()); + utils.bindElShortcut(this.$searchString, 'return', async () => { + await this.searchStringSU.updateNowIfNecessary(); + + this.refreshResults(); + }); this.searchStringSU = new SpacedUpdate(async () => { const searchString = this.$searchString.val(); @@ -270,13 +278,13 @@ export default class SearchDefinitionWidget extends TabAwareWidget { } }, 1000); - this.$descendantOf = this.$widget.find('.descendant-of'); - noteAutocompleteService.initNoteAutocomplete(this.$descendantOf); + this.$ancestor = this.$widget.find('.ancestor'); + noteAutocompleteService.initNoteAutocomplete(this.$ancestor); - this.$descendantOf.on('autocomplete:closed', async () => { - const descendantOfNoteId = this.$descendantOf.getSelectedNoteId(); + this.$ancestor.on('autocomplete:closed', async () => { + const ancestorOfNoteId = this.$ancestor.getSelectedNoteId(); - await this.setAttribute('relation', 'descendantOf', descendantOfNoteId); + await this.setAttribute('relation', 'ancestor', ancestorOfNoteId); }); this.$widget.on('click', '[data-search-option-add]', async event => { @@ -292,8 +300,8 @@ export default class SearchDefinitionWidget extends TabAwareWidget { else if (searchOption === 'includeArchivedNotes') { await this.setAttribute('label', 'includeArchivedNotes'); } - else if (searchOption === 'descendantOf') { - await this.setAttribute('relation', 'descendantOf', 'root'); + else if (searchOption === 'ancestor') { + await this.setAttribute('relation', 'ancestor', 'root'); } this.refresh(); @@ -364,6 +372,11 @@ export default class SearchDefinitionWidget extends TabAwareWidget { this.refresh(); }); + + this.$searchButton = this.$widget.find('.search-button'); + this.$searchButton.on('click', () => this.refreshResults()); + + this.$searchAndExecuteButton = this.$widget.find('.search-and-execute-button'); } async setAttribute(type, name, value = '') { @@ -374,25 +387,27 @@ export default class SearchDefinitionWidget extends TabAwareWidget { async refreshResults() { await treeCache.reloadNotes([this.noteId]); + + this.triggerEvent('searchRefreshed', {tabId: this.tabContext.tabId}); } async refreshWithNote(note) { this.$component.show(); this.$searchString.val(this.note.getLabelValue('searchString')); - for (const attrName of ['includeArchivedNotes', 'descendantOf', 'fastSearch', 'orderBy']) { + for (const attrName of ['includeArchivedNotes', 'ancestor', 'fastSearch', 'orderBy']) { const has = note.hasLabel(attrName) || note.hasRelation(attrName); this.$widget.find(`[data-search-option-add='${attrName}'`).toggle(!has); this.$widget.find(`[data-search-option-conf='${attrName}'`).toggle(has); } - const descendantOfNoteId = this.note.getRelationValue('descendantOf'); - const descendantOfNote = descendantOfNoteId ? await treeCache.getNote(descendantOfNoteId, true) : null; + const ancestorNoteId = this.note.getRelationValue('ancestor'); + const ancestorNote = ancestorNoteId ? await treeCache.getNote(ancestorNoteId, true) : null; - this.$descendantOf - .val(descendantOfNote ? descendantOfNote.title : "") - .setSelectedNotePath(descendantOfNoteId); + this.$ancestor + .val(ancestorNote ? ancestorNote.title : "") + .setSelectedNotePath(ancestorNoteId); if (note.hasLabel('orderBy')) { this.$orderBy.val(note.getLabelValue('orderBy')); @@ -401,7 +416,9 @@ export default class SearchDefinitionWidget extends TabAwareWidget { this.$actionOptions.empty(); - for (const actionAttr of this.note.getLabels('action')) { + const actionLabels = this.note.getLabels('action'); + + for (const actionAttr of actionLabels) { let actionDef; try { @@ -418,7 +435,9 @@ export default class SearchDefinitionWidget extends TabAwareWidget { this.$actionOptions.append($actionConf); } - this.refreshResults(); // important specifically when this search note was not yet refreshed + this.$searchAndExecuteButton.css('visibility', actionLabels.length > 0 ? 'visible' : 'hidden'); + + //this.refreshResults(); // important specifically when this search note was not yet refreshed } focusOnSearchDefinitionEvent() { diff --git a/src/routes/api/search.js b/src/routes/api/search.js index 373d7e2f4..f22504857 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -34,9 +34,11 @@ async function searchFromNote(req) { } else if (searchString) { const searchContext = new SearchContext({ - includeNoteContent: note.getLabelValue('includeNoteContent') === 'true', - subTreeNoteId: note.getLabelValue('subTreeNoteId'), - excludeArchived: true, + fastSearch: note.hasLabel('fastSearch'), + ancestorNoteId: note.getRelationValue('ancestor'), + includeArchivedNotes: note.hasLabel('includeArchivedNotes'), + orderBy: note.getLabelValue('orderBy'), + orderDirection: note.getLabelValue('orderDirection'), fuzzyAttributeSearch: false }); @@ -107,8 +109,8 @@ function getRelatedNotes(req) { const attr = req.body; const searchSettings = { - includeNoteContent: false, - excludeArchived: true, + fastSearch: true, + includeArchivedNotes: false, fuzzyAttributeSearch: false }; diff --git a/src/services/search/expressions/ancestor.js b/src/services/search/expressions/ancestor.js new file mode 100644 index 000000000..360b71a40 --- /dev/null +++ b/src/services/search/expressions/ancestor.js @@ -0,0 +1,28 @@ +"use strict"; + +const Expression = require('./expression'); +const NoteSet = require('../note_set'); +const log = require('../../log'); +const noteCache = require('../../note_cache/note_cache'); + +class AncestorExp extends Expression { + constructor(ancestorNoteId) { + super(); + + this.ancestorNoteId = ancestorNoteId; + } + + execute(inputNoteSet, executionContext) { + const ancestorNote = noteCache.notes[this.ancestorNoteId]; + + if (!ancestorNote) { + log.error(`Subtree note '${this.ancestorNoteId}' was not not found.`); + + return new NoteSet([]); + } + + return new NoteSet(ancestorNote.subtreeNotes).intersection(inputNoteSet); + } +} + +module.exports = AncestorExp; diff --git a/src/services/search/expressions/order_by_and_limit.js b/src/services/search/expressions/order_by_and_limit.js index 758e77c38..28f5cc6b5 100644 --- a/src/services/search/expressions/order_by_and_limit.js +++ b/src/services/search/expressions/order_by_and_limit.js @@ -44,7 +44,7 @@ class OrderByAndLimitExp extends Expression { return 0; }); - if (this.limit >= 0) { + if (this.limit > 0) { notes = notes.slice(0, this.limit); } diff --git a/src/services/search/expressions/sub_tree.js b/src/services/search/expressions/sub_tree.js deleted file mode 100644 index fc6e58e1f..000000000 --- a/src/services/search/expressions/sub_tree.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; - -const Expression = require('./expression'); -const NoteSet = require('../note_set'); -const log = require('../../log'); -const noteCache = require('../../note_cache/note_cache'); - -class SubTreeExp extends Expression { - constructor(subTreeNoteId) { - super(); - - this.subTreeNoteId = subTreeNoteId; - } - - execute(inputNoteSet, executionContext) { - const subTreeNote = noteCache.notes[this.subTreeNoteId]; - - if (!subTreeNote) { - log.error(`Subtree note '${this.subTreeNoteId}' was not not found.`); - - return new NoteSet([]); - } - - return new NoteSet(subTreeNote.subtreeNotes).intersection(inputNoteSet); - } -} - -module.exports = SubTreeExp; diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js index 976adbfa8..afc6d33ad 100644 --- a/src/services/search/search_context.js +++ b/src/services/search/search_context.js @@ -2,9 +2,11 @@ class SearchContext { constructor(params = {}) { - this.includeNoteContent = !!params.includeNoteContent; - this.subTreeNoteId = params.subTreeNoteId; - this.excludeArchived = !!params.excludeArchived; + this.fastSearch = !!params.fastSearch; + this.ancestorNoteId = params.ancestorNoteId; + this.includeArchivedNotes = !!params.includeArchivedNotes; + this.orderBy = params.orderBy; + this.orderDirection = params.orderDirection; this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch; this.highlightedTokens = []; this.originalQuery = ""; diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index fe87a3ff6..11bdd8935 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -15,7 +15,7 @@ const NoteCacheFlatTextExp = require('../expressions/note_cache_flat_text.js'); const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js'); const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js'); const OrderByAndLimitExp = require('../expressions/order_by_and_limit.js'); -const SubTreeExp = require("../expressions/sub_tree.js"); +const AncestorExp = require("../expressions/ancestor.js"); const buildComparator = require('./build_comparator.js'); const ValueExtractor = require('../value_extractor.js'); @@ -28,7 +28,7 @@ function getFulltext(tokens, searchContext) { return null; } - if (searchContext.includeNoteContent) { + if (!searchContext.fastSearch) { return new OrExp([ new NoteCacheFlatTextExp(tokens), new NoteContentProtectedFulltextExp('*=*', tokens), @@ -408,12 +408,25 @@ function getExpression(tokens, searchContext, level = 0) { } function parse({fulltextTokens, expressionTokens, searchContext}) { - return AndExp.of([ - searchContext.excludeArchived ? new PropertyComparisonExp("isarchived", buildComparator("=", "false")) : null, - searchContext.subTreeNoteId ? new SubTreeExp(searchContext.subTreeNoteId) : null, + let exp = AndExp.of([ + searchContext.includeArchivedNotes ? null : new PropertyComparisonExp("isarchived", buildComparator("=", "false")), + searchContext.ancestorNoteId ? new AncestorExp(searchContext.ancestorNoteId) : null, getFulltext(fulltextTokens, searchContext), getExpression(expressionTokens, searchContext) ]); + + if (searchContext.orderBy && searchContext.orderBy !== 'relevancy') { + const filterExp = exp; + + exp = new OrderByAndLimitExp([{ + valueExtractor: new ValueExtractor(['note', searchContext.orderBy]), + direction: searchContext.orderDirection + }], 0); + + exp.subExpression = filterExp; + } + + return exp; } module.exports = parse; diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index 6f3afdec6..80db63ea0 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -111,8 +111,8 @@ function searchTrimmedNotes(query, searchContext) { function searchNotesForAutocomplete(query) { const searchContext = new SearchContext({ - includeNoteContent: false, - excludeArchived: true, + fastSearch: true, + includeArchivedNotes: false, fuzzyAttributeSearch: true });