From b0d9e4111312e25be26ecbd971de6155ddddc928 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 18 May 2026 13:12:29 +0300 Subject: [PATCH 1/3] feat: add runAction method for handling custom actions in AdminForth --- adminforth/index.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/adminforth/index.ts b/adminforth/index.ts index f929f74da..473b92d92 100644 --- a/adminforth/index.ts +++ b/adminforth/index.ts @@ -29,7 +29,7 @@ import { import { AdminForthFilterOperators, AdminForthDataTypes, - AdminUser, + AdminUser, ActionCheckSource } from './types/Common.js'; import AdminForthPlugin from './basePlugin.js'; @@ -815,6 +815,85 @@ class AdminForth implements IAdminForth { return { error: null }; } + async runAction({ + resourceId, + actionId, + recordId, + adminUser, + extra = {}, + response, + tr, + }: { + resourceId: string, + actionId: string, + recordId: string | number, + adminUser: AdminUser, + extra?: Record, + response?: any, + tr?: any, + }) { + const resource = this.config.resources.find( + (res) => res.resourceId === resourceId + ); + + if (!resource) { + return { + ok: false, + error: `Resource '${resourceId}' not found`, + }; + } + + const action = resource.options.actions?.find( + (act) => act.id === actionId + ); + + if (!action) { + return { + ok: false, + error: `Action '${actionId}' not found`, + }; + } + + if (!action.action) { + return { + ok: false, + error: `Action '${actionId}' has no action handler`, + }; + } + + if (typeof action.allowed === 'function') { + const { allowedActions } = await interpretResource( + adminUser, + resource, + {}, + ActionCheckSource.CustomActionRequest, + this + ); + + const execAllowed = await action.allowed({ + adminUser, + standardAllowedActions: allowedActions, + }); + + if (!execAllowed) { + return { + ok: false, + error: `Action '${actionId}' not allowed`, + }; + } + } + + return await action.action({ + recordId: String(recordId), + adminUser, + resource, + adminforth: this, + response: response as any, + tr: tr as any, + extra, + }); + } + resource(resourceId: string): IOperationalResource { if (this.statuses.dbDiscover !== 'done') { if (this.statuses.dbDiscover === 'running') { From eded8d11de4d1ba24e8eb40c3de722f3f3d7c560 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Tue, 19 May 2026 10:42:06 +0300 Subject: [PATCH 2/3] feat: add documentation for programmatically starting actions with runAction --- .../tutorial/03-Customization/09-Actions.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md b/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md index 5eb502171..f20b9a282 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md @@ -360,3 +360,46 @@ Backend handler: read the payload via `extra`. Notes: - If you don’t emit a payload, the default behavior is used by the UI (e.g., in lists the current row context is used). When you do provide a payload, it will be forwarded to the backend as `extra` for your action handler. - You can combine default context with your own payload by merging before emitting, for example: `emit('callAction', { ...row, asListed: true })` if your component has access to the row object. + +## Start actions programmatically +You can execute resource actions manually using adminforth.runAction(). This is useful inside hooks, plugins, cron jobs, custom endpoints, or any backend automation. + +```ts title="./resources/apartments.ts" +actions: [ + { + //diff-add + id: 'testToggle listedAction', + name: 'Toggle listed', + icon: 'flowbite:eye-solid', + ... + } +] +``` +Then execute it from a hook for example: + +```ts title="./resources/apartments.ts" +hooks: { + ... + afterSave: async ({ record, adminUser, resource, adminforth }: { record: any, adminUser: AdminUser, resource: AdminForthResource, adminforth: any }) => { + + await adminforth.runAction({ + actionId: 'Toggle listed', + resourceId: resource.resourceId, + recordId: record.id, + adminUser, + }); + + return { ok: true }; + }, + }, +``` + +runAction() automatically: +- finds the resource +- finds the action +- checks permissions via allowed +- executes the action handler +- passes full action context (recordId, adminUser, extra, etc.) + +> ☝️ runAction() is not limited to hooks — you can call it anywhere you have access to the AdminForth instance. + From 1b33b65f49d1c42fc32038a77fb9aaf2bc238ab4 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Tue, 19 May 2026 11:03:54 +0300 Subject: [PATCH 3/3] fix: remove optional type from extra parameter in AdminForth class --- adminforth/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adminforth/index.ts b/adminforth/index.ts index 473b92d92..425281daf 100644 --- a/adminforth/index.ts +++ b/adminforth/index.ts @@ -828,7 +828,7 @@ class AdminForth implements IAdminForth { actionId: string, recordId: string | number, adminUser: AdminUser, - extra?: Record, + extra, response?: any, tr?: any, }) {