From ce09e4a1ebb6ff77b757d5660f709e21229c8f7f Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 20 Jan 2021 20:31:24 +0100 Subject: [PATCH] basic backend implementation of search actions --- .../widgets/search_actions/delete_label.js | 5 +- .../widgets/search_actions/delete_relation.js | 5 +- .../widgets/search_actions/rename_label.js | 12 +- .../widgets/search_actions/set_label_value.js | 6 +- .../search_actions/set_relation_target.js | 8 +- src/public/app/widgets/search_definition.js | 23 ++- src/routes/api/search.js | 150 +++++++++++++----- src/routes/routes.js | 1 + 8 files changed, 156 insertions(+), 54 deletions(-) diff --git a/src/public/app/widgets/search_actions/delete_label.js b/src/public/app/widgets/search_actions/delete_label.js index 65081a857..1a06fa859 100644 --- a/src/public/app/widgets/search_actions/delete_label.js +++ b/src/public/app/widgets/search_actions/delete_label.js @@ -10,7 +10,10 @@ const TPL = `
Label name:
- +
diff --git a/src/public/app/widgets/search_actions/delete_relation.js b/src/public/app/widgets/search_actions/delete_relation.js index 5b3886c8b..22876f43d 100644 --- a/src/public/app/widgets/search_actions/delete_relation.js +++ b/src/public/app/widgets/search_actions/delete_relation.js @@ -10,7 +10,10 @@ const TPL = `
Relation name:
- +
diff --git a/src/public/app/widgets/search_actions/rename_label.js b/src/public/app/widgets/search_actions/rename_label.js index f9824fa00..1c8fe10fd 100644 --- a/src/public/app/widgets/search_actions/rename_label.js +++ b/src/public/app/widgets/search_actions/rename_label.js @@ -11,11 +11,19 @@ const TPL = `
From:
- +
To:
- +
diff --git a/src/public/app/widgets/search_actions/set_label_value.js b/src/public/app/widgets/search_actions/set_label_value.js index 64a8cf5cd..8f2fae2d1 100644 --- a/src/public/app/widgets/search_actions/set_label_value.js +++ b/src/public/app/widgets/search_actions/set_label_value.js @@ -11,7 +11,11 @@ const TPL = `
Set label
- +
to value
diff --git a/src/public/app/widgets/search_actions/set_relation_target.js b/src/public/app/widgets/search_actions/set_relation_target.js index e0ee7ba29..11ca9f085 100644 --- a/src/public/app/widgets/search_actions/set_relation_target.js +++ b/src/public/app/widgets/search_actions/set_relation_target.js @@ -11,7 +11,11 @@ const TPL = `
Set relation
- +
target to note
@@ -25,7 +29,7 @@ const TPL = ` `; export default class SetRelationTargetSearchAction extends AbstractAction { - static get actionName() { return "setLabelValue"; } + static get actionName() { return "setRelationTarget"; } doRender() { const $action = $(TPL); diff --git a/src/public/app/widgets/search_definition.js b/src/public/app/widgets/search_definition.js index de3b1367c..01ec82f65 100644 --- a/src/public/app/widgets/search_definition.js +++ b/src/public/app/widgets/search_definition.js @@ -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 = `
-
@@ -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); + } } diff --git a/src/routes/api/search.js b/src/routes/api/search.js index f22504857..ddd9cdc73 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -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 }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 850428f5c..7694f6655 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -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);