Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Order Field Laravel Package

michielkempen/nova-order-field

A Laravel Nova field that adds drag-and-drop ordering to resource index tables, letting editors reorder records and persist their position via a dedicated order column. Simple to install and configure.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisites you should confirm before writing code

  • Laravel Nova installed and working in the app (this field is inert outside Nova).
  • An integer order-style column on the target model — nullable-free, backfilled, normal index, no unique constraint.
  • The package's composer.json constraints checked against your actual Nova/Laravel/PHP matrix. The last release is January 2022, so run this check first; it is the whole adoption decision in one command:
composer require michielkempen/nova-order-field --dry-run
composer show michielkempen/nova-order-field

Minimal setup

  1. Install and pin the exact version so a silent composer update cannot move it:
composer require michielkempen/nova-order-field:^1.0
  1. Add the column and backfill it to a clean 1..n sequence in the same deploy:
Schema::table('faqs', function (Blueprint $table) {
    $table->unsignedInteger('order')->default(0)->after('id');
    $table->index('order');          // normal index only — no ->unique()
});
DB::transaction(function () {
    Faq::query()->orderBy('created_at')->orderBy('id')->get()
        ->each(fn ($faq, $i) => $faq->forceFill(['order' => $i + 1])->saveQuietly());
});
  1. Make the model sortable (the package follows the spatie/eloquent-sortable convention — verify the trait name against its README/composer.json):
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;

class Faq extends Model implements Sortable
{
    use SortableTrait;

    public $sortable = [
        'order_column_name'  => 'order',
        'sort_when_creating' => true,   // new rows append to the end
    ];
}
  1. Register the field on the resource and hide it from forms:
use Michielkempen\NovaOrderField\OrderField;

public function fields(Request $request)
{
    return [
        ID::make()->sortable(),
        Text::make('Question')->rules('required'),
        OrderField::make('Order')->onlyOnIndex(),
    ];
}
  1. Set the resource's default sort to the order column so a reload shows the persisted sequence:
public static $defaultSort = ['order', 'asc'];   // Nova 4+ syntax; check yours

Where to look first

  • The resource's fields() method — that is where the whole feature lives.
  • The package's resources/js bundle — read it before trusting it. You want to know whether a drag fires one PATCH per row or one bulk request, and whether it is wrapped in a transaction.
  • The Nova request in your browser's network tab on the first drag — confirm the endpoint, the payload, and the HTTP status. This is the fastest way to learn the real persistence behaviour.

First use case

Pick one non-critical, flat, human-curated list — FAQs, hero slides, navigation items, categories — where editors currently type numbers into a "sort weight" field. Reorder it once end-to-end (drag → save → hard refresh → confirm), then move on. Do not start with a resource whose order drives a customer-facing API payload.

Implementation Patterns

The day-to-day loop

Editors drag on the index, the field persists, and the index reloads in the new order. Your job is keeping three things in sync: the order column, the resource's default sort, and whatever consumes the order downstream (queries, caches, API serialisers).

// Consumer side — always order explicitly, never rely on insertion order
$faqs = Faq::query()->orderBy('order')->get();

Companion read-only field for forms

The drag handle is index-only, so give editors a visible-but-inert value on detail/edit screens rather than letting them hand-edit raw numbers and desynchronise the sequence:

Number::make('Order')
    ->onlyOnDetail()
    ->help('Drag rows on the index to reorder.'),

Scoped ordering (per parent, per tenant)

If ordering is per-parent (faqs under a category, nav items under a menu), scope the sort query on the model so a new row appends within its own group:

public function buildSortQuery()
{
    return static::query()->where('category_id', $this->category_id);
}

Then make sure the Nova index is actually filtered to that parent (via a HasMany relationship panel or a lens). A drag on an unfiltered index writes global order values and quietly scrambles per-parent sequences — test this specific case.

Bulk-creation paths

Importers, seeders, and duplicate actions all need to respect the sequence. Prefer going through the sortable trait's create path over mass insert():

foreach ($rows as $row) {
    Faq::create($row);   // sort_when_creating appends
}

If you must bulk-insert, assign order explicitly as max(order) + n inside a transaction, and re-run your normalisation command afterwards.

Normalisation as a recurring safety net

Ship the backfill as a real artisan command, not a one-off tinker session. You will run it again after an import, after a soft-delete purge, or after an editor reports "the drags look wrong":

// php artisan order:normalize faqs
Faq::query()->orderBy('order')->orderBy('id')->get()
    ->each(fn ($m, $i) => $m->forceFill(['order' => $i + 1])->saveQuietly());

Cache invalidation is yours, not the package's

The field writes to the DB and stops there. If your front end caches a menu or slider payload, hang the invalidation off the model:

protected static function booted()
{
    static::saved(fn () => Cache::forget('nav.items'));
    static::deleted(fn () => Cache::forget('nav.items'));
}

Testing pattern

Cover the thing that actually breaks — persistence — with a feature test that hits the same endpoint the drag hits, plus one browser-level drag if you have Dusk or Pest browser tests available. Assert the reloaded order, not the response body.

Gotchas and Tips

Compatibility is the risk, not the feature. January 2022 against Nova's major-version cadence means Nova 4/5 asset and Inertia/Vue changes are the likely breakage point. If the field renders blank or the handle does nothing, check npm/nova.mix asset compilation and the package's registered script before debugging your own code. Budget a fork as a pre-approved option: the surface is small enough that updating the JS bundle for the current Nova major is a day, not a quarter.

Never put a unique index on the order column. Drag implementations commonly write intermediate states — two rows at order = 3 for a moment — and a unique constraint turns a drag into a 500. Enforce uniqueness in application code if the domain truly needs it.

Duplicates, nulls, and gaps cause unstable drags. Before rollout, run SELECT \order`, COUNT() FROM faqs GROUP BY `order` HAVING COUNT() > 1andWHERE `order` IS NULL OR `order` = 0`. A dirty sequence is the most common cause of "it jumped to the wrong place" reports that look like a package bug.

Pagination and filters are the sharp edge. Verify what a drag on page 2 of a filtered index actually does to the global sequence. Test it explicitly with: no filter, a search term, a sort by another column, and a page-2 drag. Note the result in your internal docs, because you will be asked.

Soft deletes leave holes. If the index excludes trashed rows, editors see 1, 2, 4, 5 and drags behave oddly. Normalise after restores and force-deletes, or accept the gaps and make sure your consuming orderBy does not care.

Authorization lives on the reorder endpoint, so test it as a restricted user. Log in as someone without update permission on the resource and confirm a drag is rejected server-side — not just hidden in the UI. If you run multi-tenant, confirm the endpoint scopes to the current tenant; a reorder request that ignores tenant scope is a cross-tenant write.

Silent save failures are the worst failure mode. The UI may show the new order while the DB kept the old one. Make "drag, hard refresh, confirm" part of your smoke test, and check the network tab for non-2xx responses the first time you integrate.

Concurrent editors. Two editors dragging the same list at the same time will race. Unless the package locks the sequence, treat the last write as winning and keep the list short enough that collisions are rare — or serialise reorders through an application-owned endpoint if the list is business-critical.

Check fillable/guarded. If the reorder writes through Eloquent mass assignment and order is guarded, saves fail silently. Add it to $fillable (or the trait's own assignment path) and verify.

Set the default sort, or the index lies on reload. The persisted order is worthless if Nova re-sorts by created_at on page load. This is the single most common "the package doesn't work" report and it is a one-line resource fix.

Keep it out of transactional code paths. Do not drive customer-facing ordering logic through an admin field. The field captures editor intent; ordering semantics, publishing state, and scoping belong in application services so they remain testable and independent of Nova.

Document the exit. Pin the version, add the package to your Nova upgrade checklist with a named owner, and write a short note covering the order-column convention, the normalisation command, and the manual fallback (a numeric field) if the drag handle breaks after an upgrade. That note is the difference between a UI regression and an incident.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource