UI fixes to search definition + backend support

This commit is contained in:
zadam 2021-01-18 22:52:07 +01:00
parent 3fa2535862
commit d7e46263be
14 changed files with 134 additions and 76 deletions

6
package-lock.json generated
View File

@ -4122,9 +4122,9 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
}, },
"helmet": { "helmet": {
"version": "4.3.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.3.1.tgz", "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.4.1.tgz",
"integrity": "sha512-WsafDyKsIexB0+pUNkq3rL1rB5GVAghR68TP8ssM9DPEMzfBiluEQlVzJ/FEj6Vq2Ag3CNuxf7aYMjXrN0X49Q==" "integrity": "sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw=="
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.5", "version": "2.8.5",

View File

@ -41,7 +41,7 @@
"express": "4.17.1", "express": "4.17.1",
"express-session": "1.17.1", "express-session": "1.17.1",
"fs-extra": "9.0.1", "fs-extra": "9.0.1",
"helmet": "4.3.1", "helmet": "4.4.1",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.2", "html2plaintext": "2.1.2",
"http-proxy-agent": "4.0.1", "http-proxy-agent": "4.0.1",

View File

@ -47,7 +47,7 @@ async function createSearchNote(opts = {}) {
const note = await server.post('search-note'); const note = await server.post('search-note');
const attrsToUpdate = [ 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 opts.searchString ? { type: 'label', name: 'searchString', value: opts.searchString } : undefined
].filter(attr => !!attr); ].filter(attr => !!attr);

View File

@ -67,8 +67,8 @@ export default class DialogCommandExecutor extends Component {
appContext.triggerCommand('focusOnDetail', {tabId: tabContext.tabId}); appContext.triggerCommand('focusOnDetail', {tabId: tabContext.tabId});
} }
async searchNotesCommand({searchString, subTreeNoteId}) { async searchNotesCommand({searchString, ancestorNoteId}) {
const searchNote = await dateNoteService.createSearchNote({searchString, subTreeNoteId}); const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId});
const tabContext = await appContext.tabManager.openTabWithNote(searchNote.noteId, true); const tabContext = await appContext.tabManager.openTabWithNote(searchNote.noteId, true);
@ -78,7 +78,7 @@ export default class DialogCommandExecutor extends Component {
async searchInSubtreeCommand({notePath}) { async searchInSubtreeCommand({notePath}) {
const noteId = treeService.getNoteIdFromNotePath(notePath); const noteId = treeService.getNoteIdFromNotePath(notePath);
this.searchNotesCommand({subTreeNoteId: noteId}); this.searchNotesCommand({ancestorNoteId: noteId});
} }
showBackendLogCommand() { showBackendLogCommand() {

View File

@ -23,6 +23,8 @@ async function autocompleteSourceForCKEditor(queryText) {
highlightedNotePathTitle: row.highlightedNotePathTitle highlightedNotePathTitle: row.highlightedNotePathTitle
} }
})); }));
}, {
allowCreatingNotes: true
}); });
}); });
} }
@ -34,7 +36,7 @@ async function autocompleteSource(term, cb, options = {}) {
+ '?query=' + encodeURIComponent(term) + '?query=' + encodeURIComponent(term)
+ '&activeNoteId=' + activeNoteId); + '&activeNoteId=' + activeNoteId);
if (term.trim().length >= 1) { if (term.trim().length >= 1 && options.allowCreatingNotes) {
results = [ results = [
{ {
action: 'create-note', action: 'create-note',

View File

@ -48,6 +48,10 @@ export default class NoteListWidget extends TabAwareWidget {
} }
checkRenderStatus() { 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 if (this.isIntersecting
&& this.noteIdRefreshed === this.noteId && this.noteIdRefreshed === this.noteId
&& this.shownNoteId !== this.noteId) { && this.shownNoteId !== this.noteId) {
@ -62,6 +66,11 @@ export default class NoteListWidget extends TabAwareWidget {
await noteListRenderer.renderList(); 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}) { noteDetailRefreshedEvent({tabId}) {
if (!this.isTab(tabId)) { if (!this.isTab(tabId)) {
return; return;
@ -72,6 +81,17 @@ export default class NoteListWidget extends TabAwareWidget {
setTimeout(() => this.checkRenderStatus(), 100); setTimeout(() => this.checkRenderStatus(), 100);
} }
searchRefreshedEvent({tabId}) {
if (!this.isTab(tabId)) {
return;
}
this.noteIdRefreshed = this.noteId;
this.shownNoteId = null;
this.checkRenderStatus();
}
autoBookDisabledEvent({tabContext}) { autoBookDisabledEvent({tabContext}) {
if (this.isTab(tabContext.tabId)) { if (this.isTab(tabContext.tabId)) {
this.refresh(); this.refresh();

View File

@ -4,6 +4,7 @@ import server from "../services/server.js";
import TabAwareWidget from "./tab_aware_widget.js"; import TabAwareWidget from "./tab_aware_widget.js";
import treeCache from "../services/tree_cache.js"; import treeCache from "../services/tree_cache.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
import utils from "../services/utils.js";
const TPL = ` const TPL = `
<div class="search-definition-widget"> <div class="search-definition-widget">
@ -64,9 +65,9 @@ const TPL = `
<tr> <tr>
<td>Add search option:</td> <td>Add search option:</td>
<td colspan="2" class="add-search-option"> <td colspan="2" class="add-search-option">
<button type="button" class="btn btn-sm" data-search-option-add="descendantOf"> <button type="button" class="btn btn-sm" data-search-option-add="ancestor">
<span class="bx bx-filter-alt"></span> <span class="bx bx-filter-alt"></span>
descendant of ancestor
</button> </button>
<button type="button" class="btn btn-sm" data-search-option-add="fastSearch" <button type="button" class="btn btn-sm" data-search-option-add="fastSearch"
@ -109,16 +110,16 @@ const TPL = `
</td> </td>
</tr> </tr>
<tbody class="search-options"> <tbody class="search-options">
<tr data-search-option-conf="descendantOf"> <tr data-search-option-conf="ancestor">
<td title="Matched notes must be within subtree of given note."> <td title="Matched notes must be within subtree of given note.">
Descendant of: </td> Ancestor: </td>
<td> <td>
<div class="input-group"> <div class="input-group">
<input class="descendant-of form-control" placeholder="search for note by its name"> <input class="ancestor form-control" placeholder="search for note by its name">
</div> </div>
</td> </td>
<td> <td>
<span class="bx bx-x icon-action" data-search-option-del="descendantOf"></span> <span class="bx bx-x icon-action" data-search-option-del="ancestor"></span>
</td> </td>
</tr> </tr>
<tr data-search-option-conf="fastSearch"> <tr data-search-option-conf="fastSearch">
@ -185,17 +186,19 @@ const TPL = `
<span class="bx bx-x icon-action"></span> <span class="bx bx-x icon-action"></span>
</td> </td>
</tr> </tr>
</tbody>
<tbody>
<tr> <tr>
<td colspan="3"> <td colspan="3">
<div style="display: flex; justify-content: space-evenly"> <div style="display: flex; justify-content: space-evenly">
<button type="button" class="btn btn-sm"> <button type="button" class="btn btn-sm search-button">
<span class="bx bx-search"></span> <span class="bx bx-search"></span>
Search Search
<kbd>enter</kbd> <kbd>enter</kbd>
</button> </button>
<button type="button" class="btn btn-sm"> <button type="button" class="btn btn-sm search-and-execute-button">
<span class="bx bxs-zap"></span> <span class="bx bxs-zap"></span>
Search & Execute actions Search & Execute actions
</button> </button>
@ -257,6 +260,11 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
this.$searchString = this.$widget.find(".search-string"); this.$searchString = this.$widget.find(".search-string");
this.$searchString.on('input', () => this.searchStringSU.scheduleUpdate()); this.$searchString.on('input', () => this.searchStringSU.scheduleUpdate());
utils.bindElShortcut(this.$searchString, 'return', async () => {
await this.searchStringSU.updateNowIfNecessary();
this.refreshResults();
});
this.searchStringSU = new SpacedUpdate(async () => { this.searchStringSU = new SpacedUpdate(async () => {
const searchString = this.$searchString.val(); const searchString = this.$searchString.val();
@ -270,13 +278,13 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
} }
}, 1000); }, 1000);
this.$descendantOf = this.$widget.find('.descendant-of'); this.$ancestor = this.$widget.find('.ancestor');
noteAutocompleteService.initNoteAutocomplete(this.$descendantOf); noteAutocompleteService.initNoteAutocomplete(this.$ancestor);
this.$descendantOf.on('autocomplete:closed', async () => { this.$ancestor.on('autocomplete:closed', async () => {
const descendantOfNoteId = this.$descendantOf.getSelectedNoteId(); 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 => { this.$widget.on('click', '[data-search-option-add]', async event => {
@ -292,8 +300,8 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
else if (searchOption === 'includeArchivedNotes') { else if (searchOption === 'includeArchivedNotes') {
await this.setAttribute('label', 'includeArchivedNotes'); await this.setAttribute('label', 'includeArchivedNotes');
} }
else if (searchOption === 'descendantOf') { else if (searchOption === 'ancestor') {
await this.setAttribute('relation', 'descendantOf', 'root'); await this.setAttribute('relation', 'ancestor', 'root');
} }
this.refresh(); this.refresh();
@ -364,6 +372,11 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
this.refresh(); 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 = '') { async setAttribute(type, name, value = '') {
@ -374,25 +387,27 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
async refreshResults() { async refreshResults() {
await treeCache.reloadNotes([this.noteId]); await treeCache.reloadNotes([this.noteId]);
this.triggerEvent('searchRefreshed', {tabId: this.tabContext.tabId});
} }
async refreshWithNote(note) { async refreshWithNote(note) {
this.$component.show(); this.$component.show();
this.$searchString.val(this.note.getLabelValue('searchString')); 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); const has = note.hasLabel(attrName) || note.hasRelation(attrName);
this.$widget.find(`[data-search-option-add='${attrName}'`).toggle(!has); this.$widget.find(`[data-search-option-add='${attrName}'`).toggle(!has);
this.$widget.find(`[data-search-option-conf='${attrName}'`).toggle(has); this.$widget.find(`[data-search-option-conf='${attrName}'`).toggle(has);
} }
const descendantOfNoteId = this.note.getRelationValue('descendantOf'); const ancestorNoteId = this.note.getRelationValue('ancestor');
const descendantOfNote = descendantOfNoteId ? await treeCache.getNote(descendantOfNoteId, true) : null; const ancestorNote = ancestorNoteId ? await treeCache.getNote(ancestorNoteId, true) : null;
this.$descendantOf this.$ancestor
.val(descendantOfNote ? descendantOfNote.title : "") .val(ancestorNote ? ancestorNote.title : "")
.setSelectedNotePath(descendantOfNoteId); .setSelectedNotePath(ancestorNoteId);
if (note.hasLabel('orderBy')) { if (note.hasLabel('orderBy')) {
this.$orderBy.val(note.getLabelValue('orderBy')); this.$orderBy.val(note.getLabelValue('orderBy'));
@ -401,7 +416,9 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
this.$actionOptions.empty(); this.$actionOptions.empty();
for (const actionAttr of this.note.getLabels('action')) { const actionLabels = this.note.getLabels('action');
for (const actionAttr of actionLabels) {
let actionDef; let actionDef;
try { try {
@ -418,7 +435,9 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
this.$actionOptions.append($actionConf); 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() { focusOnSearchDefinitionEvent() {

View File

@ -34,9 +34,11 @@ async function searchFromNote(req) {
} }
else if (searchString) { else if (searchString) {
const searchContext = new SearchContext({ const searchContext = new SearchContext({
includeNoteContent: note.getLabelValue('includeNoteContent') === 'true', fastSearch: note.hasLabel('fastSearch'),
subTreeNoteId: note.getLabelValue('subTreeNoteId'), ancestorNoteId: note.getRelationValue('ancestor'),
excludeArchived: true, includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
orderBy: note.getLabelValue('orderBy'),
orderDirection: note.getLabelValue('orderDirection'),
fuzzyAttributeSearch: false fuzzyAttributeSearch: false
}); });
@ -107,8 +109,8 @@ function getRelatedNotes(req) {
const attr = req.body; const attr = req.body;
const searchSettings = { const searchSettings = {
includeNoteContent: false, fastSearch: true,
excludeArchived: true, includeArchivedNotes: false,
fuzzyAttributeSearch: false fuzzyAttributeSearch: false
}; };

View File

@ -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;

View File

@ -44,7 +44,7 @@ class OrderByAndLimitExp extends Expression {
return 0; return 0;
}); });
if (this.limit >= 0) { if (this.limit > 0) {
notes = notes.slice(0, this.limit); notes = notes.slice(0, this.limit);
} }

View File

@ -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;

View File

@ -2,9 +2,11 @@
class SearchContext { class SearchContext {
constructor(params = {}) { constructor(params = {}) {
this.includeNoteContent = !!params.includeNoteContent; this.fastSearch = !!params.fastSearch;
this.subTreeNoteId = params.subTreeNoteId; this.ancestorNoteId = params.ancestorNoteId;
this.excludeArchived = !!params.excludeArchived; this.includeArchivedNotes = !!params.includeArchivedNotes;
this.orderBy = params.orderBy;
this.orderDirection = params.orderDirection;
this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch; this.fuzzyAttributeSearch = !!params.fuzzyAttributeSearch;
this.highlightedTokens = []; this.highlightedTokens = [];
this.originalQuery = ""; this.originalQuery = "";

View File

@ -15,7 +15,7 @@ const NoteCacheFlatTextExp = require('../expressions/note_cache_flat_text.js');
const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js'); const NoteContentProtectedFulltextExp = require('../expressions/note_content_protected_fulltext.js');
const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js'); const NoteContentUnprotectedFulltextExp = require('../expressions/note_content_unprotected_fulltext.js');
const OrderByAndLimitExp = require('../expressions/order_by_and_limit.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 buildComparator = require('./build_comparator.js');
const ValueExtractor = require('../value_extractor.js'); const ValueExtractor = require('../value_extractor.js');
@ -28,7 +28,7 @@ function getFulltext(tokens, searchContext) {
return null; return null;
} }
if (searchContext.includeNoteContent) { if (!searchContext.fastSearch) {
return new OrExp([ return new OrExp([
new NoteCacheFlatTextExp(tokens), new NoteCacheFlatTextExp(tokens),
new NoteContentProtectedFulltextExp('*=*', tokens), new NoteContentProtectedFulltextExp('*=*', tokens),
@ -408,12 +408,25 @@ function getExpression(tokens, searchContext, level = 0) {
} }
function parse({fulltextTokens, expressionTokens, searchContext}) { function parse({fulltextTokens, expressionTokens, searchContext}) {
return AndExp.of([ let exp = AndExp.of([
searchContext.excludeArchived ? new PropertyComparisonExp("isarchived", buildComparator("=", "false")) : null, searchContext.includeArchivedNotes ? null : new PropertyComparisonExp("isarchived", buildComparator("=", "false")),
searchContext.subTreeNoteId ? new SubTreeExp(searchContext.subTreeNoteId) : null, searchContext.ancestorNoteId ? new AncestorExp(searchContext.ancestorNoteId) : null,
getFulltext(fulltextTokens, searchContext), getFulltext(fulltextTokens, searchContext),
getExpression(expressionTokens, 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; module.exports = parse;

View File

@ -111,8 +111,8 @@ function searchTrimmedNotes(query, searchContext) {
function searchNotesForAutocomplete(query) { function searchNotesForAutocomplete(query) {
const searchContext = new SearchContext({ const searchContext = new SearchContext({
includeNoteContent: false, fastSearch: true,
excludeArchived: true, includeArchivedNotes: false,
fuzzyAttributeSearch: true fuzzyAttributeSearch: true
}); });