mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 19:49:01 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			178 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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<T> = (action: T, note: BNote) => void;
 | |
| 
 | |
| type ActionHandlerMap = {
 | |
|     [K in keyof ActionHandlers]: ActionHandler<BulkActionData<K>>;
 | |
| };
 | |
| 
 | |
| 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<string>) {
 | |
|     const actions = getActions(note);
 | |
|     return executeActions(actions, noteIds);
 | |
| }
 | |
| 
 | |
| function executeActions(actions: BulkAction[], noteIds: string[] | Set<string>) {
 | |
|     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
 | |
| };
 | 
