gobrightspot/nova-detached-actions
Nova Detached Actions for Laravel Nova lets you pull actions out of the dropdown menu and render them as standalone buttons or links on resource index and detail pages, so frequently used workflows stay visible and are always just one click away for your users.
Confirm you are on the right Nova major before writing a line of code. This package's last release was 2020-10-11 and the upstream repo is archived. That places it in the Nova 2/3 era (PHP 7.4/8.0, Laravel 6–8). Run composer show laravel/nova first. If you are on Nova 4/5, stop — the correct move is to reimplement the toolbar partial in your own app or fork the package, not to install and hope.
Install and register.
composer require gobrightspot/nova-detached-actions
Register the tool the standard Nova way — either in config/nova.php under tools, or in your NovaServiceProvider::tools(). Then publish assets if the service provider does not auto-register them:
php artisan vendor:publish --provider="Brightspot\NovaDetachedActions\ToolServiceProvider"
Read four files, in this order. They are the entire API surface and you will need all of them eventually:
ToolServiceProvider — shows which Blade partial is overridden and which asset tag is published.Laravel\Nova\Actions\Action or just a marker. Either way, the action you write is a normal Nova action.First use case: a read-only export button. Pick the lowest-risk action you already own. Register an existing ExportAction as detached on a resource's index screen, load the page, and confirm two things: the button renders next to Create, and clicking it opens the action's modal with its fields. Prove the toolbar and the modal lifecycle before you attach anything that writes, imports, or deletes. Do not start with a cache rebuild or an import — if the action fires unexpectedly with zero models, you want it to be harmless.
Sanity-check the asset pipeline immediately. Detached buttons are rendered by compiled JS/CSS shipped inside the package. On a fresh install, click the button on day one. The classic silent failure here is buttons that render but do nothing when clicked — no PHP exception, no obvious console error. Catching that in the first ten minutes saves an afternoon of debugging later.
Reuse existing actions; do not write parallel ones. The package changes where the trigger lives, not what the action does. A row-selection ExportAction you already wrote — with its fields(), handle(), authorizedToSee(), and confirmation modal — works unchanged as a detached action. This is the single biggest velocity win, and the single biggest debt avoidance: you keep one action class, one set of field definitions, one permission story.
Registration is declarative and order-sensitive. Register detached actions where you register your normal actions (a resource-level registration method, or globally from the provider). From release 1.1.1: the first registered action appears closest to the Create button. This is not alphabetical and not configurable — it is array order. Treat ordering as a deliberate UI decision and comment the array so the next developer doesn't "tidy" it into something confusing.
Overflow is automatic, but it is a 2020 UX decision. Release 1.1.1 added a "more" menu: past a certain count, the remainder collapse behind a menu. You get this free, but the breakpoint behaviour was tuned for 2020-era viewports. Test at your actual admin resolutions — a modern widescreen may show more buttons inline than you expect, or worse, an awkward halfway state. If a screen needs more than a handful of commands, reconsider whether the toolbar is the right home for all of them.
Use Heroicons for the common verbs. 1.1.1 added support for 104 Heroicons, so a detached action can carry an icon rather than a label. Reserve icons for the canonical verbs — download, upload, refresh, cog, trash — and let the overflow menu carry the rest as text. Icons-only toolbars are elegant at three buttons and inscrutable at eight.
Lean on Nova's authorization chain. Detached actions run through the normal action authorization path: authorizedToSee on the action, canRun against the request, and canSee on inline fields. Writing the action the way you already do inherits the entire permission model for free. This is the strongest reason to use the package over a hand-rolled toolbar card — you are not inventing a second permissions surface.
Queue anything that isn't instant. "Detached" means "no row selection," not "asynchronous." An export that takes 40 seconds still occupies the request and will time out. Implement ShouldQueue on the action, or dispatch a job from handle() explicitly, for imports, exports, and index rebuilds. Write the result somewhere durable — a log table, a notification, an output file on disk — because the package provides no run history whatsoever.
Guard handle() against an empty model collection — every single time. This is the most common implementation bug in the wild. Actions written for row selection assume $models holds the selected records. On a detached invocation, that collection is empty or absent. The first three lines of handle() should decide explicitly whether empty input means "operate on everything" (an export, a full rebuild) or "invalid invocation, abort" (an archive, a bulk status change). Never leave it implicit.
Keep two action sets genuinely separate. Row-scoped actions — archive this record, resend this invoice, assign this ticket — stay behind checkboxes. Resource-wide commands — rebuild search index, export all, run nightly cleanup — go detached. Only register the same class in both places if its handle() genuinely behaves correctly for both "zero models" and "n models," and even then, think twice: a button that silently means "everything" is a footgun on a table with 400k rows.
Test authorization as a first-class case. Detached actions bypass the checkbox gate, which means a registration mistake exposes a maintenance command to every user who can view the resource. One test is enough: a user lacking the policy ability must neither see the button nor be able to execute the action via a direct request. That test is your entire security story for this feature; write it before the second action, not after the tenth.
You are adopting a fork, not consuming a dependency. The repository is archived, has zero dependents, and no maintainer is coming back. Vendor it: copy the source into an internal namespace, pin it via a path or VCS repository in composer.json, tag it, and name an owner. The MIT license makes this free. Consuming it straight from Packagist with no fork is the one path with no recovery when something breaks.
Asset drift is the primary failure mode. The package ships pre-compiled JS/CSS flavoured for Nova's old asset pipeline. When those assets go stale — after a Nova update, a build change, or a publishing mishap — the buttons still render, but clicking them does nothing. No PHP exception. No error you'll notice in a log. If a detached button appears but the modal never opens, check the published assets and the service provider's publishes() array before you check your action class.
Detached actions receive no models. Say it again. An action written against $models->first() or $models->each(...) will blow up or, worse, silently no-op. If an action is shared between row-select and detached contexts, that branch belongs at the very top of handle(), commented.
Ordering surprises are array order, full stop. If buttons land somewhere unexpected, read the registration array — do not hunt for a config key that doesn't exist. The first entry is nearest Create, always.
Icon names are Heroicons v0.x-era strings. The 104 icons supported in 1.1.1 are a frozen set from 2020. A modern heroicon-o-arrow-down-tray will not resolve. Pull the supported list out of the package source and treat it as an enum; don't trust a current Heroicons gallery.
Verify standalone() semantics against source. Nova itself grew standalone-action concepts around the same era. There is a real chance you are depending on the package's rendering and Nova's flag in ways that interact unpredictably across Nova versions. Ten minutes in the service provider and the toolbar partial settles which mechanism is actually doing the work.
There is no auditing, retrying, scheduling, or run history. The package is a trigger and nothing more. If a stakeholder asks "did the import run, and what did it do," you need a queue, a log table, and probably a notification — none of which come from here. Say that out loud before anyone builds an ops workflow on top of a button.
Extension points are the source itself. There is no plugin system. The toolbar Blade partial and the action registration method are the two seams. Override the partial in your app's view namespace to restyle it; patch the fork for behavioural changes. Keep patches small, dated, and documented in a PATCHES.md so the next Nova upgrade is a diff rather than an archaeology project.
Budget the maintenance honestly. Near-zero recurring cost until Nova, PHP, or Laravel moves — then you are the maintainer, on the clock. Put a review on the calendar alongside every framework upgrade. Discovering this at upgrade time, mid-migration, is the expensive version of the same conversation.
How can I help you explore Laravel packages today?