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=="
},
"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",

View File

@ -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",

View File

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

View File

@ -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() {

View File

@ -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',

View File

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

View File

@ -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 = `
<div class="search-definition-widget">
@ -64,9 +65,9 @@ const TPL = `
<tr>
<td>Add search option:</td>
<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>
descendant of
ancestor
</button>
<button type="button" class="btn btn-sm" data-search-option-add="fastSearch"
@ -109,16 +110,16 @@ const TPL = `
</td>
</tr>
<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.">
Descendant of: </td>
Ancestor: </td>
<td>
<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>
</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>
</tr>
<tr data-search-option-conf="fastSearch">
@ -185,17 +186,19 @@ const TPL = `
<span class="bx bx-x icon-action"></span>
</td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="3">
<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>
Search
<kbd>enter</kbd>
</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>
Search & Execute actions
</button>
@ -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() {

View File

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

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;
});
if (this.limit >= 0) {
if (this.limit > 0) {
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 {
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 = "";

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

View File

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