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 Field Sortable Laravel Package

naxon/nova-field-sortable

naxon/nova-field-sortable is a Laravel Nova field for drag-and-drop sorting of resource records in index views. It lets editors reorder menus, categories, and other lists directly in Nova, with simple setup and an order column to store manual positions. Useful for any model that needs a custom displ...

View on GitHub
Deep Wiki
Context7

Getting Started

Version-check before you touch the keyboard. This package last shipped in October 2018, so it is Nova 1.x/2.x era: Vue 2 components, Laravel Mix-compiled assets, Nova::script() registration. Run composer show laravel/nova first. On Nova 3 you may get it working with a nudge. On Nova 4/5 (Vue 3 + Vite) it will not mount, and the failure is silent — see Gotchas. If you're on 4+, you are not installing a package, you are reading a reference implementation and writing your own.

Install by vendoring source, not by trusting Packagist. With no upstream commits since 2018 and zero dependents, treat this as code you adopt rather than a dependency you consume:

"repositories": [{ "type": "vcs", "url": "[email protected]:you/nova-field-sortable.git" }],
"require": { "naxon/nova-field-sortable": "dev-main" }

Pin the commit in composer.lock and commit that lock file. Then, in order:

  1. Migration — an unsigned integer column with an index and a default, so existing rows don't fail the NOT NULL constraint:
Schema::table('faqs', function (Blueprint $table) {
    $table->unsignedInteger('order')->default(0)->index();
});
  1. Backfill — assign sequential values to existing rows in an order you can defend (created_at is the usual fallback).
  2. New rows go to the end — otherwise every create lands at position 0 and the list looks random:
protected static function booted()
{
    static::creating(fn ($model) => $model->order = (int) static::max('order') + 1);
}
  1. Register the field index-only; it's a drag handle, not an editable input:
Sortable::make('Order')->onlyOnIndex(),
  1. Make the index match the stored order — without this the drag appears to do nothing on refresh:
public static function indexQuery(NovaRequest $request, $query)
{
    return $query->orderBy('order');
}

Read these four things before trusting it, in this order: src/Sortable.php (the actual column name and any config methods — the README may not match), the route/controller behind the reorder XHR (this is the entire security surface), resources/js/components/IndexField.vue (which sortable library, and how it computes the payload indices), and composer.json (the laravel/nova constraint you'll have to edit).

First use case: one flat, editorially-owned list — FAQs, footer links, categories, pricing tiers — with tens of rows and one non-technical editor. Drag one row, refresh, confirm the column changed. If that works in an afternoon, continue; if it doesn't, stop and reconsider before you've built dependencies on it.

Implementation Patterns

Flat lists only. The field sorts the whole table and knows nothing about parent_id. If you're ordering chapters within a course, a global order column is wrong from the first drag. Either scope the index query and the reorder payload by parent, or make the uniqueness constraint composite and enforce it:

$table->unique(['course_id', 'order']);

Never combine it with spatie/eloquent-sortable. Both want to own the order column; spatie reindexes on save()/delete(), this field reindexes on drop. You get double writes and off-by-one drift that is miserable to debug. Pick one writer. Either spatie owns the column and the field is pure UI calling into it, or plain integers with this field owning every write.

Contiguous integers are fine at tens of rows and wrong at thousands. The default strategy rewrites N rows per drop. If a list can plausibly grow past a few hundred rows, decide on fractional ranks up front so you don't migrate mid-flight:

$rank = ($prev->rank + $next->rank) / 2;

One row updated per drag, and most of the lost-update problem disappears for free.

You own authorization. A user who can view a Nova resource is not automatically allowed to reorder it. Find the reorder route and put a policy in front of it — an endpoint taking an array of IDs is textbook IDOR:

$ids = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);

$records = Faq::findMany($ids);
$records->each(fn ($r) => Gate::authorize('update', $r));

Wrap the reindex in a transaction with lockForUpdate(), and assert the payload is exactly the expected set — not an arbitrary assortment of IDs from another table.

Pagination and filters are the classic trap. Drag on page 2 with a search applied, and a naive "reindex the visible rows by their visible indices" will scramble every hidden row. Either disable the handle when filters or pagination are active, or send the full ordered ID set and reindex server-side inside the transaction. Pick one and write it into the resource, don't leave it to chance.

Deletion leaves gaps — let them. Compacting on delete is a full reindex you didn't need, and it collides with soft deletes if you query without withTrashed(). Gaps are harmless; compact on demand if a human actually complains.

Test it as three features, not one unit. A happy-path POST that asserts the column; a policy-denial that asserts 403; and an index render that asserts the drag handle HTML is present. That third one is your canary for a Nova upgrade silently blanking the field:

$this->actingAs($editor)
    ->postJson('/nova-vendor/naxon/field-sortable/reorder', ['ids' => [$b->id, $a->id]])
    ->assertOk();

$this->assertEquals(1, $b->fresh()->order);

Gotchas and Tips

Check for a hardcoded column name before you name your schema. Look in src/Sortable.php for a literal order or position. If a config method exists (->sortColumn(...) or similar), use it rather than renaming your column to fit a 2018 package's taste.

Nova's built-in sorting probably already covers you. ->sortable() on an index field plus $defaultSort gets you a stable, zero-maintenance ordering. Drag-and-drop earns its keep only when the order is genuinely editorial — a curator's judgment not derivable from any column. "By name," "by created_at," and "by priority score" are not manual-sort problems.

The Composer constraint will fight you in CI. laravel/nova lives on a private repo, so add credentials as secrets before this resolves at all:

composer config http-basic.nova.laravel.com "$NOVA_USER" "$NOVA_PASS"

Don't commit an auth.json.

After a Nova upgrade, the failure mode is silence. The field registers, mounts, renders nothing, throws no error, and the index looks normal minus the handle. If the drag handle vanishes, open the network tab and look for a 404 on the compiled asset in public/vendor/ — that's Nova::script() failing quietly. Put "verify drag handle renders" on the Nova-major upgrade checklist and back it with the render smoke test above.

Stale assets after deploy. The JS is published into public/vendor/ at build time. Run php artisan nova:publish in your release pipeline and assert the file exists, or you'll ship a backend change against last week's frontend and waste an afternoon.

Debugging a drop, in order. Watch the XHR:

  • Nothing fires → it's the Vue component or its registration.
  • Fires 200, order doesn't stick → $fillable/mass-assignment, or a saving model hook overwriting the column.
  • Fires 419 → CSRF against the Nova route.
  • Fires 200 and the DB changed but the UI reverted → your indexQuery isn't ordering by the column.

Extension points that actually exist: the field class (column/handle config), the Vue component (drag behavior), and the route/controller (persistence). Scoping, hierarchy, concurrency, and fractional ranks are all yours to write — budget accordingly.

Name an owner and write down the divergence. No upstream commits since 2018, no issue triage, 58 stars, zero dependents. Add a short README note describing what your fork changed and why. Otherwise the next developer assumes it's a maintained dependency and tries to composer update it into a broken state.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment