diff --git a/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md b/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md index 5eb50217..f20b9a28 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. + diff --git a/adminforth/index.ts b/adminforth/index.ts index f929f74d..425281da 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, + 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') {