mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
basic backend implementation of search actions
This commit is contained in:
parent
7bf6ec3ff2
commit
ce09e4a1eb
@ -10,7 +10,10 @@ const TPL = `
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 15px;" class="text-nowrap">Label name:</div>
|
||||
|
||||
<input type="text" class="form-control label-name"/>
|
||||
<input type="text"
|
||||
class="form-control label-name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -10,7 +10,10 @@ const TPL = `
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 15px;" class="text-nowrap">Relation name:</div>
|
||||
|
||||
<input type="text" class="form-control relation-name"/>
|
||||
<input type="text"
|
||||
class="form-control relation-name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -11,11 +11,19 @@ const TPL = `
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 15px;">From:</div>
|
||||
|
||||
<input type="text" class="form-control old-label-name" placeholder="old name"/>
|
||||
<input type="text"
|
||||
class="form-control old-label-name"
|
||||
placeholder="old name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
|
||||
<div style="margin-right: 15px; margin-left: 15px;">To:</div>
|
||||
|
||||
<input type="text" class="form-control new-label-name" placeholder="new name"/>
|
||||
<input type="text"
|
||||
class="form-control new-label-name"
|
||||
placeholder="new name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -11,7 +11,11 @@ const TPL = `
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 15px;" class="text-nowrap">Set label</div>
|
||||
|
||||
<input type="text" class="form-control label-name" placeholder="label name"/>
|
||||
<input type="text"
|
||||
class="form-control label-name"
|
||||
placeholder="label name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
|
||||
<div style="margin-right: 15px; margin-left: 15px;" class="text-nowrap">to value</div>
|
||||
|
||||
|
@ -11,7 +11,11 @@ const TPL = `
|
||||
<div style="display: flex; align-items: center">
|
||||
<div style="margin-right: 15px;" class="text-nowrap">Set relation</div>
|
||||
|
||||
<input type="text" class="form-control relation-name" placeholder="relation name"/>
|
||||
<input type="text"
|
||||
class="form-control relation-name"
|
||||
placeholder="relation name"
|
||||
pattern="[\\p{L}\\p{N}_:]+"
|
||||
title="Alphanumeric characters, underscore and colon are allowed characters."/>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 10px;">
|
||||
<div style="margin-right: 15px;" class="text-nowrap">target to note</div>
|
||||
@ -25,7 +29,7 @@ const TPL = `
|
||||
</tr>`;
|
||||
|
||||
export default class SetRelationTargetSearchAction extends AbstractAction {
|
||||
static get actionName() { return "setLabelValue"; }
|
||||
static get actionName() { return "setRelationTarget"; }
|
||||
|
||||
doRender() {
|
||||
const $action = $(TPL);
|
||||
|
@ -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 toastService from "../services/toast.js";
|
||||
import utils from "../services/utils.js";
|
||||
import DeleteNoteSearchAction from "./search_actions/delete_note.js";
|
||||
import DeleteLabelSearchAction from "./search_actions/delete_label.js";
|
||||
@ -14,14 +15,7 @@ import SetRelationTargetSearchAction from "./search_actions/set_relation_target.
|
||||
|
||||
const TPL = `
|
||||
<div class="search-definition-widget">
|
||||
<style>
|
||||
.note-detail-search {
|
||||
padding: 7px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
<style>
|
||||
.search-setting-table {
|
||||
margin-top: 7px;
|
||||
margin-bottom: 7px;
|
||||
@ -38,6 +32,10 @@ const TPL = `
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.search-definition-widget input:invalid {
|
||||
border: 3px solid red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="search-settings">
|
||||
@ -350,6 +348,7 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
|
||||
this.$searchButton.on('click', () => this.refreshResults());
|
||||
|
||||
this.$searchAndExecuteButton = this.$widget.find('.search-and-execute-button');
|
||||
this.$searchAndExecuteButton.on('click', () => this.searchAndExecute());
|
||||
}
|
||||
|
||||
async setAttribute(type, name, value = '') {
|
||||
@ -423,4 +422,12 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
|
||||
getContent() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async searchAndExecute() {
|
||||
await server.post(`search-and-execute-note/${this.noteId}`);
|
||||
|
||||
this.refreshResults();
|
||||
|
||||
toastService.showMessage('Actions have been executed.', 3000);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,41 @@ const log = require('../../services/log');
|
||||
const scriptService = require('../../services/script');
|
||||
const searchService = require('../../services/search/services/search');
|
||||
|
||||
async function search(note) {
|
||||
let searchResultNoteIds;
|
||||
|
||||
try {
|
||||
const searchScript = note.getRelationValue('searchScript');
|
||||
const searchString = note.getLabelValue('searchString');
|
||||
|
||||
if (searchScript) {
|
||||
searchResultNoteIds = await searchFromRelation(note, 'searchScript');
|
||||
} else if (searchString) {
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: note.hasLabel('fastSearch'),
|
||||
ancestorNoteId: note.getRelationValue('ancestor'),
|
||||
includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
|
||||
orderBy: note.getLabelValue('orderBy'),
|
||||
orderDirection: note.getLabelValue('orderDirection'),
|
||||
fuzzyAttributeSearch: false
|
||||
});
|
||||
|
||||
searchResultNoteIds = searchService.findNotesWithQuery(searchString, searchContext)
|
||||
.map(sr => sr.noteId);
|
||||
} else {
|
||||
searchResultNoteIds = [];
|
||||
}
|
||||
|
||||
// we won't return search note's own noteId
|
||||
// also don't allow root since that would force infinite cycle
|
||||
return searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId));
|
||||
} catch (e) {
|
||||
log.error(`Search failed for note ${note.noteId}: ` + e.message + ": " + e.stack);
|
||||
|
||||
throw new Error("Search failed, see logs for details.");
|
||||
}
|
||||
}
|
||||
|
||||
async function searchFromNote(req) {
|
||||
const note = repository.getNote(req.params.noteId);
|
||||
|
||||
@ -19,55 +54,91 @@ async function searchFromNote(req) {
|
||||
}
|
||||
|
||||
if (note.type !== 'search') {
|
||||
return [400, `Note ${req.params.noteId} is not search note.`]
|
||||
return [400, `Note ${req.params.noteId} is not a search note.`]
|
||||
}
|
||||
|
||||
let searchString;
|
||||
let searchResultNoteIds;
|
||||
|
||||
try {
|
||||
const searchScript = note.getRelationValue('searchScript');
|
||||
searchString = note.getLabelValue('searchString');
|
||||
|
||||
if (searchScript) {
|
||||
searchResultNoteIds = await searchFromRelation(note, 'searchScript');
|
||||
}
|
||||
else if (searchString) {
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: note.hasLabel('fastSearch'),
|
||||
ancestorNoteId: note.getRelationValue('ancestor'),
|
||||
includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
|
||||
orderBy: note.getLabelValue('orderBy'),
|
||||
orderDirection: note.getLabelValue('orderDirection'),
|
||||
fuzzyAttributeSearch: false
|
||||
});
|
||||
|
||||
searchResultNoteIds = searchService.findNotesWithQuery(searchString, searchContext)
|
||||
.map(sr => sr.noteId);
|
||||
}
|
||||
else {
|
||||
searchResultNoteIds = [];
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
log.error(`Search failed for note ${note.noteId}: ` + e.message + ": " + e.stack);
|
||||
|
||||
throw new Error("Search failed, see logs for details.");
|
||||
}
|
||||
|
||||
// we won't return search note's own noteId
|
||||
// also don't allow root since that would force infinite cycle
|
||||
searchResultNoteIds = searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId));
|
||||
let searchResultNoteIds = await search(note);
|
||||
|
||||
if (searchResultNoteIds.length > 200) {
|
||||
searchResultNoteIds = searchResultNoteIds.slice(0, 200);
|
||||
}
|
||||
|
||||
console.log(`Search with query "${searchString}" with results: ${searchResultNoteIds}`);
|
||||
|
||||
return searchResultNoteIds;
|
||||
}
|
||||
|
||||
const ACTION_HANDLERS = {
|
||||
deleteNote: (action, note) => {
|
||||
note.isDeleted;
|
||||
note.save();
|
||||
},
|
||||
renameLabel: (action, note) => {
|
||||
for (const label of note.getOwnedLabels(action.oldLabelName)) {
|
||||
label.name = action.newLabelName;
|
||||
label.save();
|
||||
}
|
||||
},
|
||||
setLabelValue: (action, note) => {
|
||||
note.setLabel(action.labelName, action.labelValue);
|
||||
}
|
||||
};
|
||||
|
||||
function getActions(note) {
|
||||
return note.getLabels('action')
|
||||
.map(actionLabel => {
|
||||
let action;
|
||||
|
||||
try {
|
||||
action = JSON.parse(actionLabel.value);
|
||||
} catch (e) {
|
||||
log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(action.name in ACTION_HANDLERS)) {
|
||||
log.error(`Cannot find '${action.name}' search action handler, skipping.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return action;
|
||||
})
|
||||
.filter(a => !!a);
|
||||
}
|
||||
|
||||
async function searchAndExecute(req) {
|
||||
const note = repository.getNote(req.params.noteId);
|
||||
|
||||
if (!note) {
|
||||
return [404, `Note ${req.params.noteId} has not been found.`];
|
||||
}
|
||||
|
||||
if (note.isDeleted) {
|
||||
// this can be triggered from recent changes and it's harmless to return empty list rather than fail
|
||||
return [];
|
||||
}
|
||||
|
||||
if (note.type !== 'search') {
|
||||
return [400, `Note ${req.params.noteId} is not a search note.`]
|
||||
}
|
||||
|
||||
const searchResultNoteIds = await search(note);
|
||||
|
||||
const actions = getActions(note);
|
||||
|
||||
for (const resultNoteId of searchResultNoteIds) {
|
||||
const resultNote = repository.getNote(resultNoteId);
|
||||
|
||||
if (!resultNote || resultNote.isDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const action of actions) {
|
||||
ACTION_HANDLERS[action.name](action, resultNote);
|
||||
|
||||
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function searchFromRelation(note, relationName) {
|
||||
const scriptNote = note.getRelationTarget(relationName);
|
||||
|
||||
@ -180,5 +251,6 @@ function formatValue(val) {
|
||||
|
||||
module.exports = {
|
||||
searchFromNote,
|
||||
searchAndExecute,
|
||||
getRelatedNotes
|
||||
};
|
||||
|
@ -257,6 +257,7 @@ function register(app) {
|
||||
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
|
||||
|
||||
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
|
||||
apiRoute(POST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute);
|
||||
apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes);
|
||||
|
||||
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
||||
|
Loading…
x
Reference in New Issue
Block a user