import log from "./log.js"; import becca from "../becca/becca.js"; import cloningService from "./cloning.js"; import branchService from "./branches.js"; import { randomString } from "./utils.js"; import eraseService from "./erase.js"; import type BNote from "../becca/entities/bnote.js"; import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons"; type ActionHandler = (action: T, note: BNote) => void; type ActionHandlerMap = { [K in keyof ActionHandlers]: ActionHandler>; }; const ACTION_HANDLERS: ActionHandlerMap = { addLabel: (action, note) => { note.addLabel(action.labelName, action.labelValue); }, addRelation: (action, note) => { note.addRelation(action.relationName, action.targetNoteId); }, deleteNote: (action, note) => { const deleteId = `searchbulkaction-${randomString(10)}`; note.deleteNote(deleteId); }, deleteRevisions: (action, note) => { const revisionIds = note .getRevisions() .map((rev) => rev.revisionId) .filter((rev) => !!rev) as string[]; eraseService.eraseRevisions(revisionIds); }, deleteLabel: (action, note) => { for (const label of note.getOwnedLabels(action.labelName)) { label.markAsDeleted(); } }, deleteRelation: (action, note) => { for (const relation of note.getOwnedRelations(action.relationName)) { relation.markAsDeleted(); } }, renameNote: (action, note) => { // "officially" injected value: // - note const newTitle = eval(`\`${action.newTitle}\``); if (note.title !== newTitle) { note.title = newTitle; note.save(); } }, renameLabel: (action, note) => { for (const label of note.getOwnedLabels(action.oldLabelName)) { // attribute name is immutable, renaming means delete old + create new const newLabel = label.createClone("label", action.newLabelName, label.value); newLabel.save(); label.markAsDeleted(); } }, renameRelation: (action, note) => { for (const relation of note.getOwnedRelations(action.oldRelationName)) { // attribute name is immutable, renaming means delete old + create new const newRelation = relation.createClone("relation", action.newRelationName, relation.value); newRelation.save(); relation.markAsDeleted(); } }, updateLabelValue: (action, note) => { for (const label of note.getOwnedLabels(action.labelName)) { label.value = action.labelValue; label.save(); } }, updateRelationTarget: (action, note) => { for (const relation of note.getOwnedRelations(action.relationName)) { relation.value = action.targetNoteId; relation.save(); } }, moveNote: (action, note) => { const targetParentNote = becca.getNote(action.targetParentNoteId); if (!targetParentNote) { log.info(`Cannot execute moveNote because note ${action.targetParentNoteId} doesn't exist.`); return; } let res; if (note.getParentBranches().length > 1) { res = cloningService.cloneNoteToParentNote(note.noteId, action.targetParentNoteId); } else { res = branchService.moveBranchToNote(note.getParentBranches()[0], action.targetParentNoteId); } if ("success" in res && !res.success) { log.info(`Moving/cloning note ${note.noteId} to ${action.targetParentNoteId} failed with error ${JSON.stringify(res)}`); } }, executeScript: (action, note) => { if (!action.script || !action.script.trim()) { log.info("Ignoring executeScript since the script is empty."); return; } const scriptFunc = new Function("note", action.script); scriptFunc(note); note.save(); } } as const; function getActions(note: BNote) { 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 as BulkAction; }) .filter((a) => !!a); } /** * Executes the bulk actions defined in the note against the provided search result note IDs. * @param note the note containing the bulk actions, read from the `action` label. * @param noteIds the IDs of the notes to apply the actions to. */ function executeActionsFromNote(note: BNote, noteIds: string[] | Set) { const actions = getActions(note); return executeActions(actions, noteIds); } function executeActions(actions: BulkAction[], noteIds: string[] | Set) { for (const resultNoteId of noteIds) { const resultNote = becca.getNote(resultNoteId); if (!resultNote) { continue; } for (const action of actions) { try { log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`); const handler = ACTION_HANDLERS[action.name] as (a: typeof action, n: BNote) => void; handler(action, resultNote); } catch (e: any) { log.error(`ExecuteScript search action failed with ${e.message}`); } } } } export default { executeActions, executeActionsFromNote };