Custom Actions
Custom Actions
Add your own buttons next to the built-in ones: publish a draft, archive the selected rows, sync a resource with another system.
Bulk delete, CSV/JSON export and CSV import are built in — see Features.
Where they live
An action's handler is a function, and a function in nuxt.config cannot reach the running app. So actions
are defined in their own file, which is bundled with the admin: app/admin.actions.ts (the app's source
directory — admin.actions.ts next to app.vue).
export default defineAdminActions({
articles: {
publish: {
label: 'Publish',
icon: 'i-heroicons-rocket-launch',
type: 'single',
location: ['row', 'detail'],
permission: 'update',
confirm: article => `Publish "${article.title}"?`,
handler: async (article, ctx) => {
await $fetch(ctx.path(article.id), { method: 'PATCH', body: { published: true } })
await ctx.refresh()
ctx.toast.success(`"${article.title}" is live`)
},
},
},
})
The keys are resource names, then action names. defineAdminActions is auto-imported.
To keep the file somewhere else, point the module at it:
autoAdmin: {
actions: '~/admin/actions', // resolved like an import
}
The file is found when the app starts: after creating it, restart the dev server.
resources.<name>.actions in nuxt.config fails the build with a pointer to this file — earlier versions
documented it there, where it was never applied.Action types
Single — one record
publish: {
label: 'Publish',
type: 'single',
location: ['row', 'detail'],
handler: async (article, ctx) => { /* … */ },
}
location: 'row'— in each row's menu, after View / Edit / Deletelocation: 'detail'— on the record: the detail page and the view modal
The handler gets the record.
Bulk — the selected records
unpublish: {
label: 'Unpublish',
icon: 'i-heroicons-eye-slash',
type: 'bulk',
location: 'toolbar',
confirm: articles => `Unpublish ${articles.length} articles?`,
handler: async (articles, ctx) => {
await Promise.all(articles.map(a =>
$fetch(ctx.path(a.id), { method: 'PATCH', body: { published: false } }),
))
await ctx.refresh()
ctx.toast.success(`${articles.length} unpublished`)
},
}
A button on the list toolbar while rows are selected — Unpublish (2). The handler gets the selected rows of
the current page. Rows become selectable for a bulk action even when the caller may not delete them (bulk
delete still only takes the rows they may delete). features.bulkActions: false turns selection off, custom
bulk actions included.
Page-level — the resource
syncFromCrm: {
label: 'Sync from CRM',
icon: 'i-heroicons-arrow-path',
type: 'page-level',
location: 'toolbar',
handler: async (_, ctx) => {
await $fetch('/api/crm/sync', { method: 'POST' })
await ctx.refresh()
ctx.toast.success('Synced')
},
}
A button next to "Create New". The handler gets undefined.
Options
| Option | |
|---|---|
label | Button / menu text |
icon | Icon name, e.g. i-heroicons-rocket-launch |
type | 'single' · 'bulk' · 'page-level' |
location | 'row' · 'detail' (or both, as an array) for single; bulk and page-level are always on the toolbar |
permission | Who may use it — see below |
confirm | Ask first: a message, or a function of the record(s) |
variant | Nuxt UI button variant: solid · outline · soft · subtle · ghost · link |
color | Nuxt UI color: primary · secondary · success · info · warning · error · neutral |
handler | (item, ctx) => void | Promise<void> |
Permissions
An operation is checked against the API's own answer — the same one the built-in buttons use:
permission: 'update', // 'create' | 'read' | 'update' | 'delete'
For a row or a record, update and delete are that row's answer (its objectLevel rule included), so the
action is disabled exactly where Edit is.
Or a function, for anything else. It runs while the admin renders, so it may call your own composables — your auth state, for example — and it must answer synchronously:
permission: ({ item }) => useUserSession().user.value?.role === 'admin' && !item?.published,
A refused action is shown disabled or hidden, per permissions.unauthorizedButtons.
permission decides which buttons the admin offers. What the handler's requests may do is still decided by
nuxt-auto-api on the server.The context
interface ActionContext {
resource: string // 'articles'
refresh: () => Promise<void> // refetch every list and record of this resource
toast: {
success: (message: string) => void
error: (message: string, error?: unknown) => void // the API's message as the description
}
path: (...segments: Array<string | number>) => string // ctx.path(7) → '/api/articles/7' (honours the API prefix)
}
A handler that throws gets an error toast ("Publish failed" with the API's message); report your own errors
with ctx.toast.error when you want a better one.
Next Steps
- Composables - Available composables for actions
- Permissions - Detailed permission handling
M2M Relationships
This guide covers how M2M relationships work in the auto-admin interface.
Composables
Auto-imported composables available in your admin components and custom pages.