basic backend implementation of search actions

This commit is contained in:
zadam 2021-01-20 20:31:24 +01:00
parent 7bf6ec3ff2
commit ce09e4a1eb
8 changed files with 156 additions and 54 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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