kirschbaum-development/nova-inline-select
Nova Inline Select lets you edit select fields directly from Laravel Nova's index and detail views, no extra clicks or forms. Add inline dropdown editing to any resource for faster, smoother workflows with a simple field configuration.
Step 1 — pin the version against your Nova major, deliberately. The 3.0.0 release notes are one line: "Nova v5 Support." That is a semver-major bump whose only stated change is framework compatibility, so treat ^3.0 as the Nova 5 track and ^2.0 as the frozen Nova 4 track. Don't let composer require resolve this implicitly while your laravel/nova constraint is also loose:
# Nova 5
composer require kirschbaum-development/nova-inline-select:^3.0
# Nova 4 — pin the old line explicitly
composer require kirschbaum-development/nova-inline-select:^2.0
Commit the constraint to composer.json and note the chosen track in your README. This is the single decision most likely to bite you six months later during a Nova upgrade.
Step 2 — verify the namespace from disk, not from a blog post. The vendor slug and the PHP namespace don't line up the way you'd guess:
head -20 vendor/kirschbaum-development/nova-inline-select/src/*.php
The class is conventionally KirschbaumDevelopment\NovaInlineSelect\NovaInlineSelect and it extends Laravel\Nova\Fields\Select — but confirm the header before pasting the import. A stale FQCN gives you a "class not found" on your first field and tells you nothing about whether the package installed correctly.
Step 3 — first use case: one enum-ish field on one index screen. Pick something high-frequency and trivially reversible — a status, a stage, a priority. Drop it in next to your existing fields:
use KirschbaumDevelopment\NovaInlineSelect\NovaInlineSelect;
public function fields(NovaRequest $request)
{
return [
NovaInlineSelect::make('status', 'Status')
->options([
'draft' => 'Draft',
'review' => 'In Review',
'published' => 'Published',
])
->displayUsingLabels()
->onlyOnIndex()
->sortable()
->filterable(),
// ...the rest of your fields
];
}
Because it is a Select, everything you already know — options(), displayUsing(), displayUsingLabels(), sortable(), filterable() — carries over. The migration is a one-line class swap; that's the whole appeal.
Step 4 — smoke test in this exact order before you demo it:
updated_at on the row moved. If it didn't, your write never reached the model and the UI is lying.Step 5 — know where to look when it breaks. The surface area is tiny, so triage is a table lookup:
| Symptom | First place to look |
|---|---|
| Renders as plain text, no dropdown | dist/js/field.js present; run php artisan nova:publish |
| Click does nothing | Network tab — the PATCH /nova-api/... request and its status |
| 403 on save | Policy / canSee / authorizedToUpdate on the resource |
| 422 on save | Server-side validation rules — yours, not the component's |
| 200 but value reverts | $fillable, casts, or an observer writing the value back |
| Wrong label shown | ->options() key/value shape, or missing displayUsingLabels() |
Split the field by context — this is the pattern that makes the package safe to adopt. Use the inline field on the index for the fast path, and a plain Nova Select on forms where you want the full validation and help-text lifecycle:
// Index: fast, low-ceremony
NovaInlineSelect::make('status')
->options(Status::class)
->displayUsingLabels()
->onlyOnIndex(),
// Forms: deliberate, fully validated
Select::make('status')
->options(Status::class)
->rules('required', 'in:draft,review,published')
->onlyOnForms(),
Bind options to your domain enum, never a duplicated array. Since it's a Select underneath, class-based options work, so the dropdown tracks the enum instead of drifting from it:
NovaInlineSelect::make('status')->options(Status::class)->displayUsingLabels()
If your enum doesn't expose Nova-compatible labels, map explicitly:
->options(collect(Status::cases())->mapWithKeys(fn ($c) => [$c->value => $c->label()]))
Gate visibility per row, because the index is now an update surface. An inline edit skips the detail screen — which is usually where your policy actually gets exercised. If a user can see the cell, assume they can click it:
NovaInlineSelect::make('status')
->options(Status::class)
->canSee(fn (NovaRequest $request) => $request->user()->can('update', $this->resource))
Put the real guard on the model, not on the field. Field-level rules are a UI affordance; a model observer or DB constraint is the guard that also covers tinker, imports, and future API writes:
protected static function booted(): void
{
static::updating(function (Order $order) {
if ($order->isDirty('status') && ! $order->status->canTransitionTo(...)) {
throw ValidationException::withMessages(['status' => 'Invalid transition.']);
}
});
}
Log audit data in an observer, not from Nova events. If "who changed this, and when" matters, use updated plus getOriginal() / getChanges(), then verify with a real inline click that the observer fires exactly once. Don't assume ResourceUpdated semantics carry over to an inline save — prove it.
Design the screen as a queue, not a table. The highest-value shape is an index scoped by Nova filters to an operational queue — unassigned tickets, pending fulfilment, leads by stage — where the only action is advancing one scalar field. Anything that needs a confirmation dialog, a reason code, or a side effect stays a Nova action.
Keep option sets to roughly 3–8 entries. A 200-row owner or category dropdown in a table cell is unusable; that's a searchable BelongsTo on the detail form, or an action with a picker.
Sequence adoption. Pilot on one low-risk resource with a documented rollback (delete the field line, composer remove). Expand only after you've confirmed writes flow through your observers and your policy. The whole rollout is cheap partly because reversal is one commit.
Version coupling is the headline risk. 3.0.0 signals Nova 5 support and nothing else — no stated evidence that Nova 4 remains supported in 3.x, and no evidence the save lifecycle was revisited. Record your track in the README and re-check the constraint during any Nova upgrade.
Do not trust field ->rules() as your safety net on the index. Nova field rules are exercised during the form submission path, and an inline index edit may not take that path. Test empirically: submit a deliberately invalid value from the index and see whether it persists. If it does, move the rule into a model observer, a request-level check, or the database.
Model events and side effects can double-fire or silently skip. If updated sends a webhook, reindexes search, or writes an audit row, test each model with a real inline click before rollout. Once an index becomes a rapid-fire editing surface, consider debouncing or rate-limiting expensive observers.
Concurrency is last-write-wins, and the index doesn't refresh itself. Two ops staff working the same queue will overwrite each other. Cheap mitigations: show updated_at on the index, mark stale rows readonly, or keep the field off resources where a conflicting edit is expensive.
Debug the request, not the component. A "nothing happens" report resolves fastest in the Network tab: 403 → policy / canSee; 422 → server validation; 200 with unchanged data → $fillable, casts, or an observer rewriting the value.
displayUsingLabels() needs keyed options. A bare list like ['draft', 'published'] won't render labels the way you expect. Use an associative array or an enum.
Nullable columns need an explicit blank entry. Without one, users can never clear the field. Add '' => '—' and handle the empty string in your observer so clearing is a defined transition rather than an accidental null.
Re-publish and rebuild assets after every Nova or package bump. Nova 5 changed asset conventions: run php artisan nova:publish and your asset build, then test against a production build. A missing or stale dist/js/field.js frequently only shows up when assets are versioned — hence the classic "works locally, plain text in prod" report.
Asset/API drift, not logic, is what actually breaks. The blast radius is one field class plus one Vue component. When it stops rendering after an upgrade, diff the dist/ output and the Nova field API signature between versions before debugging your own resource code.
Extension points are thin — fork without guilt. There's no service provider to hook, no config file, and no event surface beyond Nova's. If you need per-row dependent options, multi-selects, or custom rendering, you're writing a Nova field anyway; at this code size, forking under your own namespace (MIT permits it) is a realistic and cheap fallback. That forkability is also the mitigation for the low star count and thin dependents list.
Stay off anything audit- or money-sensitive. Inline edits bypass reason codes, approvals, and confirmation dialogs — fine for reversible operational fields, wrong for billing state, refunds, permissions, or compliance statuses. Build those deliberately on the detail form.
How can I help you explore Laravel packages today?