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 = `
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);