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="display: flex; align-items: center">
|
||||||
<div style="margin-right: 15px;" class="text-nowrap">Label name:</div>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -10,7 +10,10 @@ const TPL = `
|
|||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<div style="margin-right: 15px;" class="text-nowrap">Relation name:</div>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -11,11 +11,19 @@ const TPL = `
|
|||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<div style="margin-right: 15px;">From:</div>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -11,7 +11,11 @@ const TPL = `
|
|||||||
<div style="display: flex; align-items: center">
|
<div style="display: flex; align-items: center">
|
||||||
<div style="margin-right: 15px;" class="text-nowrap">Set label</div>
|
<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>
|
<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="display: flex; align-items: center">
|
||||||
<div style="margin-right: 15px;" class="text-nowrap">Set relation</div>
|
<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>
|
||||||
<div style="display: flex; align-items: center; margin-top: 10px;">
|
<div style="display: flex; align-items: center; margin-top: 10px;">
|
||||||
<div style="margin-right: 15px;" class="text-nowrap">target to note</div>
|
<div style="margin-right: 15px;" class="text-nowrap">target to note</div>
|
||||||
@ -25,7 +29,7 @@ const TPL = `
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
|
|
||||||
export default class SetRelationTargetSearchAction extends AbstractAction {
|
export default class SetRelationTargetSearchAction extends AbstractAction {
|
||||||
static get actionName() { return "setLabelValue"; }
|
static get actionName() { return "setRelationTarget"; }
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
const $action = $(TPL);
|
const $action = $(TPL);
|
||||||
|
@ -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 toastService from "../services/toast.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import DeleteNoteSearchAction from "./search_actions/delete_note.js";
|
import DeleteNoteSearchAction from "./search_actions/delete_note.js";
|
||||||
import DeleteLabelSearchAction from "./search_actions/delete_label.js";
|
import DeleteLabelSearchAction from "./search_actions/delete_label.js";
|
||||||
@ -14,14 +15,7 @@ import SetRelationTargetSearchAction from "./search_actions/set_relation_target.
|
|||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="search-definition-widget">
|
<div class="search-definition-widget">
|
||||||
<style>
|
<style>
|
||||||
.note-detail-search {
|
|
||||||
padding: 7px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-setting-table {
|
.search-setting-table {
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
@ -38,6 +32,10 @@ const TPL = `
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-definition-widget input:invalid {
|
||||||
|
border: 3px solid red;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="search-settings">
|
<div class="search-settings">
|
||||||
@ -350,6 +348,7 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
|
|||||||
this.$searchButton.on('click', () => this.refreshResults());
|
this.$searchButton.on('click', () => this.refreshResults());
|
||||||
|
|
||||||
this.$searchAndExecuteButton = this.$widget.find('.search-and-execute-button');
|
this.$searchAndExecuteButton = this.$widget.find('.search-and-execute-button');
|
||||||
|
this.$searchAndExecuteButton.on('click', () => this.searchAndExecute());
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAttribute(type, name, value = '') {
|
async setAttribute(type, name, value = '') {
|
||||||
@ -423,4 +422,12 @@ export default class SearchDefinitionWidget extends TabAwareWidget {
|
|||||||
getContent() {
|
getContent() {
|
||||||
return '';
|
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 scriptService = require('../../services/script');
|
||||||
const searchService = require('../../services/search/services/search');
|
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) {
|
async function searchFromNote(req) {
|
||||||
const note = repository.getNote(req.params.noteId);
|
const note = repository.getNote(req.params.noteId);
|
||||||
|
|
||||||
@ -19,55 +54,91 @@ async function searchFromNote(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (note.type !== 'search') {
|
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 = await search(note);
|
||||||
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));
|
|
||||||
|
|
||||||
if (searchResultNoteIds.length > 200) {
|
if (searchResultNoteIds.length > 200) {
|
||||||
searchResultNoteIds = searchResultNoteIds.slice(0, 200);
|
searchResultNoteIds = searchResultNoteIds.slice(0, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Search with query "${searchString}" with results: ${searchResultNoteIds}`);
|
|
||||||
|
|
||||||
return 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) {
|
async function searchFromRelation(note, relationName) {
|
||||||
const scriptNote = note.getRelationTarget(relationName);
|
const scriptNote = note.getRelationTarget(relationName);
|
||||||
|
|
||||||
@ -180,5 +251,6 @@ function formatValue(val) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
searchFromNote,
|
searchFromNote,
|
||||||
|
searchAndExecute,
|
||||||
getRelatedNotes
|
getRelatedNotes
|
||||||
};
|
};
|
||||||
|
@ -257,6 +257,7 @@ function register(app) {
|
|||||||
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
|
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
|
||||||
|
|
||||||
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
|
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);
|
apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes);
|
||||||
|
|
||||||
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user