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

outl1ne/nova-sortable

Laravel Nova package that adds drag-and-drop reordering to resource index and HasMany views, powered by Spatie's eloquent-sortable. Supports pivot tables, move-to-start/end arrows, per-resource sorting control, and localization.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements check first. Nova ^5.0 and PHP >=8.1. This package will not work on Nova 3/4 — if you're mid-upgrade, park it until Nova is upgraded.

Install and wire it up in four steps.

  1. composer require outl1ne/nova-sortable
  2. Add the order column and backfill it in one migration:
Schema::table('slides', function (Blueprint $table) {
    $table->integer('sort_order')->default(0)->index();
});
DB::statement('UPDATE slides SET sort_order = id');
  1. Make the model sortable (this is Spatie's eloquent-sortable, so the package only provides the Nova UI):
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;

class Slide extends Model implements Sortable
{
    use SortableTrait;

    public $sortable = [
        'order_column_name'  => 'sort_order',
        'sort_when_creating' => true,
    ];
}
  1. Opt the Nova resource in:
use Outl1ne\NovaSortable\Traits\HasSortableRows;

class Slide extends Resource
{
    use HasSortableRows;
}

Where to look first. Open vendor/outl1ne/nova-sortable/src/Traits/HasSortableRows.php — it's short and tells you exactly which properties/methods you can override (how sorting is enabled, what the drag endpoint expects, whether you can gate it per-user). Then read Spatie's SortableTrait::buildSortQuery() and setNewOrder(), because 90% of the real behaviour — scoping, ordering, bulk writes — lives there, not in the Nova package. Finally check the package's docs/ folder and config/ / lang/ directories; publish them if you need to customise labels.

First use case. The ideal candidate is a short, flat, fully-curated list that an editor wants to control without touching code: homepage banners, FAQ entries, navigation links, slideshow images. Add the trait, drag one row, then confirm in the DB that sort_order actually changed — that one check validates the whole install.

Implementation Patterns

Index-view reordering (the default). Add HasSortableRows to the resource and the index table gets drag handles plus "move to start / move to end" arrows. Set the resource's default sort so the first page load matches the stored order:

public static $defaultSort = ['sort_order' => 'asc'];

If you override indexQuery(), call parent::indexQuery() and make sure you're not clobbering the ordering, or wrap your own queries with ->ordered().

HasMany inline sorting. This is the pattern most teams actually want. Put HasSortableRows on the child resource — the one rendered in the relationship table — and drag rows directly on the parent's detail page. Because the HasMany view is inherently scoped to one parent, ordering stays per-parent and pagination/filters are far less of a footgun than doing it on a global index. If the handles don't show up, check the HasMany field definition and the package config for the HasMany toggle before assuming the trait didn't take.

Scoped / grouped ordering. For a global index where items belong to a group (e.g. slides per banner_id), the package moves items relative to their visible neighbours. Make the sort scope explicit on the model:

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

Do this together with sort_when_creating => true, otherwise new rows in a group land at 0 and corrupt the sequence. The safer UX is still a HasMany view per parent.

BelongsTo / MorphTo with pivots. The order column lives on the pivot table, not the related model. Write the migration on the pivot, drag one item, then inspect the pivot table — never assume; the column name and relationship scoping have to line up exactly.

Programmatic reordering. Everything from eloquent-sortable is available, which is the real long-term value: seeders, importers, CSV uploads, and API endpoints can all honour the same ordering.

Slide::setNewOrder($idsFromRequest);      // bulk, one query
$slide->moveToStart();
$slide->moveOrderedAfter($other);
Slide::ordered()->get();                  // any consumer reads in order

Creation flow. With sort_when_creating => true new records append to the end — good default. Never expose sort_order as an editable Number field in the create form; let the trait own it. If you must show it, make it read-only on detail.

Authorisation. Reordering writes to the model, so it runs through Nova's update path and model policies. A user with read-only access will see handles that fail server-side (typically a 403 in the network tab) or silently do nothing. Gate the trait's sortability check for non-editors rather than relying on users to not drag.

Testing. Feature test the reorder route with an authenticated Nova user and assert sort_order values after the call; unit test setNewOrder() for your importer path. Assert row counts and ordering with ->ordered()->pluck('id') — a plain ->get() will pass even when ordering is broken.

Gotchas and Tips

  • order_column_name is the number-one failure. Spatie's default is order_column, not sort_order. Add order_column to the table or set order_column_name in $sortable and keep them identical everywhere, including pivot tables.
  • Column hygiene: integer, non-nullable (or default(0)), indexed, not unique. Nullable columns produce unpredictable positions; a unique index will blow up mid-drag.
  • Backfill before enabling. Existing rows with NULL/0/duplicate values make drag results non-deterministic. UPDATE ... SET sort_order = id (chunked on large tables) before you demo it.
  • Filtered and paginated indexes are a trap. Drag handles reorder relative to what's on screen. On page 2 of a search result the "new position" is a slice of the overall order. Prefer the HasMany view for scoped lists; if you can't, restrict reordering to the unfiltered index.
  • Large tables: every drag triggers a bulk UPDATE across the range. It's fine for hundreds of rows, not for tens of thousands — batch imports should use setNewOrder() once, not per record.
  • sort_when_creating and Nova's create modal: the value is set after insert, so anything that reads it in a creating observer will see the old value. Use created for that logic.
  • Debugging checklist: open the browser network tab, reproduce a drag, and read the request/response. 403 → policy. 422 → column missing or not fillable/assignable. 200 but nothing persists → model doesn't implement Spatie\EloquentSortable\Sortable, or the resource lacks the trait. Then SELECT id, sort_order FROM table ORDER BY sort_order — that query is the ground truth.
  • Don't run reorders concurrently with imports or queue jobs writing the same table. No locking is offered; a mid-import drag will interleave badly. Wrap reorder + import in a transaction and avoid scheduling them together.
  • Extension points worth knowing: override buildSortQuery() for scoping, override the trait's sortability check for authorisation, use Spatie's ordered() scope everywhere you consume the list (APIs, exports, sitemaps) so the Nova UI and the frontend never disagree.
  • Frontend assets: Nova 5 packages ship prebuilt assets, so no npm step. If your deploy customises Nova assets, include php artisan nova:publish in the deploy script — otherwise the drag handles are missing after a fresh server build even though the PHP side is correct.
  • Morph relations need the order column present on every table in the morph set, or scope with buildSortQuery() per type; otherwise sorting one type renumbers another.
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.
mydnic/volet-feature-board-filament-plugin
wmde/email-address
refinery29/test-util
dees040/laravel-api-responses
teamzac/geocoder
ideatica/config-writer
keaton-php/pipe-dream
jacksonwebservices/laravel-user-suite
maksimru/conditional-laravel-debugbar
gismatul/nova-support-chat
gismatul/laravel-support-chat
networkrailbusinesssystems/oracle-api
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
refinery29/php-cs-fixer-config
parsnick/steak