| [ Web Proxy ] |
| Viewing: https://adminforth.dev/docs/tutorial/Customization/Actions/ | [Back] [Original] |
You might need to give admin users a feature to perform some action on a single record. Actions can be displayed as buttons in the list view and/or in the three-dots menu.
Here's how to add a custom action:
{
resourceId: 'aparts',
options: {
actions: [
{
name: 'Auto submit', // Display name of the action
icon: 'lucide:play', // Icon to display (using Lucide icons)
// Control who can see/use this action
allowed: ({ adminUser, standardAllowedActions }) => {
return true; // Allow everyone
},
// Handler function when action is triggered
action: async ({ recordId, adminUser }) => {
logger.info("auto submit", recordId, adminUser);
return {
ok: true,
successMessage: "Auto submitted"
};
},
// Configure where the action appears
showIn: {
list: false, // Show in list view
listThreeDotsMenu: true, // Show in three dots menu in list view
showButton: true, // Show as a button
showThreeDotsMenu: true, // Show in three-dots menu
}
}
]
}
}
name: Display name of the actionicon: Icon to show (using Lucide icon set)allowed: Function to control access to the actionaction: Handler function that executes when action is triggered for a single recordbulkHandler: Handler function that executes when the action is triggered for multiple records at once (see Dedicated bulk handler)showIn: Controls where the action appears
list: whether to show as an icon button per row in the list viewlistThreeDotsMenu: whether to show in the three-dots menu per row in the list viewshowButton: whether to show as a button on the show viewshowThreeDotsMenu: whether to show in the three-dots menu of the show viewbulkButton: whether to show as a bulk action button when rows are selectedactionWhen showIn.bulkButton is true and only action (not bulkHandler) is defined, AdminForth automatically calls your action function once per selected record using Promise.all. This is convenient for simple cases but means N separate handler invocations run in parallel:
{
name: 'Auto submit',
action: async ({ recordId }) => {
// Called once per selected record when used as a bulk button
await doSomething(recordId);
return { ok: true, successMessage: 'Done' };
},
showIn: {
bulkButton: true, // triggers Promise.all over selected records
showButton: true,
}
}
If your operation can be expressed more efficiently as a single batched query (e.g., a single UPDATE WHERE id IN ()), define bulkHandler instead. AdminForth will call it once with all selected record IDs:
{
name: 'Auto submit',
// bulkHandler receives all recordIds in one call use it for batched operations
bulkHandler: async ({ recordIds, adminforth, resource }) => {
await doSomethingBatch(recordIds);
return { ok: true, successMessage: `Processed ${recordIds.length} records` };
},
// You can still keep `action` for the single-record show/edit buttons
action: async ({ recordId }) => {
await doSomething(recordId);
return { ok: true, successMessage: 'Done' };
},
showIn: {
bulkButton: true,
showButton: true,
}
}
When both
actionandbulkHandlerare defined, AdminForth usesbulkHandlerfor bulk operations andactionfor single-record operations. When onlyactionis defined andbulkButtonis enabled, AdminForth falls back toPromise.allover individualactioncalls.
| Option | Type | Description |
|---|---|---|
showIn.bulkButton | boolean | Show as a bulk action button in the list toolbar. |
bulkHandler | async ({ recordIds, adminUser, adminforth, resource, response, tr }) => { ok, error?, message? } | Called with all selected IDs at once. Falls back to calling action per record in parallel if omitted. |
bulkConfirmationMessage | string | Confirmation dialog text shown before the bulk action executes. |
bulkSuccessMessage | string | Success message shown after the bulk operation. Defaults to "N out of M items processed successfully". |
You can control who can use an action through the allowed function. This function receives:
{
options: {
actions: [
{
name: 'Auto submit',
allowed: async ({ adminUser, standardAllowedActions }) => {
if (adminUser.dbUser.role !== 'superadmin') {
return false;
}
return true;
},
// ... other configuration
}
]
}
}
The allowed function receives:
adminUser: The current admin user objectstandardAllowedActions: Standard permissions for the current userReturn:
true to allow accessfalse to deny accessreturn 'Only superadmins can perform this action'Here is how it looks:
[alt text]
Instead of defining an action handler, you can specify a url that the user will be redirected to when clicking the action button:
{
name: 'View details',
icon: 'lucide:eye',
url: '/resource/aparts', // URL to redirect to
showIn: {
list: true,
listThreeDotsMenu: false,
showButton: true,
showThreeDotsMenu: true,
}
}
Note: You cannot specify both
actionandurlfor the same action - only one should be used.
The URL can be:
To open the URL in a new tab, append target=_blank as a query parameter. If the URL already has query parameters, use &target=_blank; otherwise use ?target=_blank:
{
name: 'View on Google',
icon: 'lucide:external-link',
url: 'https://google.com/search?q=apartment&target=_blank',
showIn: {
list: true,
showButton: true
}
}
Example to generate dynamic URL:
{
name: 'View on Google',
icon: 'lucide:external-link',
url: async ({record, recordId, adminUser, resource }) => `https://google.com/search?q=Apartment ${record.title}`,
showIn: {
list: true,
showButton: true
}
}
Note: Though url function might be async we recommend to omit long awaits, or ideally don't use them at all, cause slow execution of this hook might be a subject of bottleneck for resource pages rendering. For built actions the async functions would be called in parallel to optimize loading speed.
Using url prop described above is recommended way to implementing URL navigation from actions (internal or external), because URLs are rendered into direct anchour tag and support all anchour features (like Open in new tab).
However, rearely you might also like to decide whether to redirect only after performing some logic (conditionally). This way is not recommended for most of cases, because it is not compatible with action native features (we can't know URL before executing action body):
{
name: 'View on Google',
icon: 'lucide:external-link',
action: async ({ recordId }) => {
if (await testSomething(recordId)) {
return { ok: true, redirectUrl: 'https://google.com/search?q=apartment' };
};
return { ok: true, successMessage: 'Done' };
},
showIn: {
list: true,
showButton: true
}
}
If you want to style an action's button/icon without changing its behavior, attach a custom UI wrapper via customComponent.
The file points to your SFC in the custom folder (alias @@/), and meta lets you pass lightweight styling options (e.g., border color, radius).
Below we wrap a "Mark as listed" action.
{
resourceId: 'aparts',
options: {
actions: [
{
name: 'Mark as listed',
icon: 'lucide:eye',
// UI wrapper for the built-in action button
customComponent: {
file: '@@/ActionBorder.vue', // SFC path in your custom folder
meta: { color: '#94a3b8', radius: 10 }
},
showIn: { list: false, listThreeDotsMenu: true, showButton: true, showThreeDotsMenu: true },
action: async ({ recordId }) => {
await admin.resource('aparts').update(recordId, { listed: 1 });
return { ok: true, successMessage: 'Marked as listed' };
}
},
]
}
}
Use this minimal wrapper component to add a border/rounding around the default action UI while keeping the action logic intact.
Keep the <slot /> (that's where AdminForth renders the default button) and emit callAction (optionally with a payload) to trigger the handler when the wrapper is clicked.
<template>
<!-- Keep the slot: AdminForth renders the default action button/icon here -->
<!-- Emit `callAction` (optionally with a payload) to trigger the action when the wrapper is clicked -->
<!-- Example: provide `meta.extra` to send custom data. In list views we merge with `row` so recordId context is kept. -->
<div :style="styleObj" @click="click({ ...props.row, ...(props.meta?.extra ?? {}) })">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
// meta can style the wrapper and optionally carry extra payload for the action
meta?: { color?: string; radius?: number; padding?: number; extra?: any };
// When used in list view, the table passes current row
row?: any;
// When used in show/edit views, the page passes current record
record?: any;
}>();
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
const styleObj = computed(() => ({
display: 'inline-block',
border: `1px solid ${props.meta?.color ?? '#e5e7eb'}`,
borderRadius: (props.meta?.radius ?? 8) + 'px',
padding: (props.meta?.padding ?? 2) + 'px',
}));
function click(payload: any) {
emit('callAction', { ...props.row, ...(props.meta?.extra ?? {}) })
}
//we need to define this expose, because padding is added by adminforth wrapper and to trigger click on this padding we use this expose
defineExpose({
click
});
</script>
You can pass arbitrary data from your custom UI wrapper to the backend action by emitting callAction with a payload. That payload will be available on the server under the extra argument of your action handler.
Frontend examples:
<template>
<!-- Two buttons that pass different flags to the action -->
<button @click="emit('callAction', { asListed: true })" class="mr-2">Mark as listed</button>
<button @click="emit('callAction', { asListed: false })">Mark as unlisted</button>
<!-- Or keep the default slot button and wrap it: -->
<div @click="emit('callAction', { asListed: true })">
<slot />
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
</script>
Backend handler: read the payload via extra.
{
resourceId: 'aparts',
options: {
actions: [
{
name: 'Toggle listed',
icon: 'lucide:eye',
showIn: { list: false, listThreeDotsMenu: false, showButton: true, showThreeDotsMenu: true },
// The payload from emit('callAction', { asListed: true|false }) arrives here as `extra`
customComponent: {
file: '@@/ActionToggleListed.vue'
},
action: async ({ recordId, extra }) => {
const asListed = extra?.asListed === true;
// Example update (use your own data layer):
await admin.resource('aparts').update(recordId, { listed: asListed });
return { ok: true, successMessage: `Set listed=${asListed}` };
}
}
]
}
}
Notes:
extra for your action handler.emit('callAction', { ...row, asListed: true }) if your component has access to the row object.You can execute resource actions manually using adminforth.runAction(). This is useful inside hooks, plugins, cron jobs, custom endpoints, or any backend automation.
actions: [
{
id: 'testToggle listedAction',
name: 'Toggle listed',
icon: 'lucide:eye',
...
}
]
Then execute it from a hook for example:
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:
runAction() is not limited to hooks you can call it anywhere you have access to the AdminForth instance.
| Web Proxy Viewer | New URL | Original Page |