mirror of
https://github.com/zadam/trilium.git
synced 2025-06-05 01:18:44 +02:00
UI fixes to search definition + backend support
This commit is contained in:
parent
3fa2535862
commit
d7e46263be
6
package-lock.json
generated
6
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
28
src/services/search/expressions/ancestor.js
Normal file
28
src/services/search/expressions/ancestor.js
Normal 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;
|
@ -44,7 +44,7 @@ class OrderByAndLimitExp extends Expression {
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (this.limit >= 0) {
|
||||
if (this.limit > 0) {
|
||||
notes = notes.slice(0, this.limit);
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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 = "";
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user