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

optimistdigital/nova-sortable

A Laravel Nova package for drag-and-drop sorting of resource records in index tables. Add a sortable trait and field, then reorder rows directly in Nova with minimal setup. Ideal for models using an order or sort_order column.

View on GitHub
Deep Wiki
Context7

Getting Started

optimistdigital/nova-sortable (now published as outl1ne/nova-sortable) is a small, focused Nova add-on: it adds a drag handle to a resource’s index rows and persists the new order to an integer column on the model’s table. It is not a general-purpose sorting library — it only matters when a Nova resource needs manual ordering (FAQs, banners, menu items, slides, checklist steps, etc.). The day-to-day value is that you avoid building a custom reorder UI and endpoint for every such resource.

1. Install the package

composer require outl1ne/nova-sortable

Older codebases may still reference the optimistdigital/nova-sortable package name. The namespace also changed from OptimistDigital to Outl1ne; check which one your composer.lock and imports use before copying snippets from old blog posts.

2. Add the sort column with a migration

Schema::table('banners', function (Blueprint $table) {
    $table->integer('sort_order')->nullable()->index();
});

The column is conventionally sort_order, but the name is configurable. Make it nullable so existing rows and creates that don’t set it don’t fail, then backfill (see below).

3. Opt the Nova resource in

namespace App\Nova;

use Outl1ne\NovaSortable\Traits\HasSortableRows;

class Banner extends Resource
{
    use HasSortableRows;
}

That’s the minimum. The resource index should now show drag handles and persist order.

4. First use case

The canonical first case is a Banner or Faq resource that editors want to arrange by hand. Add the trait, add the column, seed a few rows with sort_order values, then open the index in Nova and drag. If the handles don’t appear, confirm the trait is on the resource (not the model), the column exists, and the user has update permission on the resource.

Where to look first

  • The package README for the current static properties and Nova version matrix.
  • src/Traits/HasSortableRows.php — the entire behaviour is a thin trait; reading it takes minutes and answers most “why is it ordering like that?” questions.
  • Your resource’s indexQuery and $defaultSort — most integration problems originate there, not in the package.

There is generally no config file to publish; behaviour is configured with static properties on each resource.

Implementation Patterns

Default index ordering

By default the trait appends an orderBy on the sort column to the resource index query, so the list matches the drag order out of the box. If you need a different default order and only want drag-and-drop as a secondary tool, disable it per resource:

public static $sortOnIndex = false;

Custom column name

If sort_order clashes with an existing column or naming convention:

public static $sortColumnName = 'position';

Keep the property and the migration in sync; a mismatch produces SQL “unknown column” errors on the index.

Grouped sorting (children within a parent)

For rows that should only be reordered inside a group (menu items under a menu, steps under a checklist), declare the group columns:

public static $sortableGroups = [
    'menu_id',
];

The practical pattern is one sort_order sequence per group. Make sure the group column is present and indexed, and that the index query actually exposes it (no global scope hiding it).

Backfilling existing rows

Never leave production rows with NULL sort values if you rely on the order. Backfill once, deterministically:

DB::table('banners')->orderBy('id')->get()->each(function ($row, $i) {
    DB::table('banners')->where('id', $row->id)->update(['sort_order' => $i + 1]);
});

For large tables, chunk by id or use a window function (ROW_NUMBER() OVER (ORDER BY id)) instead of loading everything.

Keep the model usable outside Nova

The package only owns the Nova endpoint. Your application code still needs deterministic ordering, so add a scope or default relation order on the model:

public function scopeOrdered($query)
{
    return $query->orderBy('sort_order')->orderBy('id');
}

Also assign a sort_order on create so new records don’t land at the top arbitrarily:

protected static function booted()
{
    static::creating(function ($model) {
        $model->sort_order ??= (static::max('sort_order') ?? 0) + 1;
    });
}

If the package has an “append after create” option in your version, prefer the built-in flag so the Nova and non-Nova paths agree.

Workflow tips

  • Use the trait on resources where ordering is a product requirement, not incidentally. It adds a writable column and an endpoint; treat it as a schema decision.
  • Pair it with a dedicated “Order” column in the index so editors can see the numeric value when drag fails or when debugging.
  • For a reorder-heavy admin workflow, keep the index page size modest (e.g. 25–50). Drag-and-drop is most reliable within a visible page.
  • If ordering must be tenant-specific, either add a tenant column to the sortable group list or use a per-tenant sort column/table. A single global sort_order will leak ordering across tenants.
  • The sort endpoint runs through Nova’s resource authorization. Confirm your update policy is correct, because that is effectively the permission to reorder.

Gotchas and Tips

  • Forgetting the migration is the #1 failure. The handles render, dragging sends a request, and the update fails on a missing column. If the index 500s after adding the trait, check the schema first.
  • NULL sort values sort unpredictably. Backfill before enabling the feature for editors, and set a value on create.
  • Ambiguous columns with joins. If indexQuery() joins another table that also has sort_order, an unqualified orderBy('sort_order') can throw an ambiguous-column error. Qualify the column (e.g. banners.sort_order) or set $sortColumnName to the qualified name.
  • $defaultSort conflicts. The trait’s index ordering and a resource-level $defaultSort can fight each other. Decide which one owns the default order; disable $sortOnIndex if the resource’s own sort should win.
  • Pagination limits drag-and-drop. Reordering across page boundaries is not a reliable workflow; users will drag within a page and expect global movement. Either keep page sizes small, provide a “sort all” view, or clearly constrain the feature to small, stable lists.
  • Filters and search can produce confusing results. Reordering a filtered subset changes relative positions among visible rows only, which can look like items moved unexpectedly when the filter is removed. Document this for editors or hide sorting when filters are active.
  • Global scopes and soft deletes. Scopes that hide rows still affect the sequence you see. If you use SoftDeletes, reordering doesn’t renumber deleted rows; gaps are normal and harmless unless you depend on contiguous values.
  • Gaps are fine; mass renumbering is not. Don’t write code that assumes sort_order is 1..N. Use ORDER BY sort_order, id and treat values as sparse.
  • Package rename and namespace drift. The move from optimistdigital to outl1ne means old imports (OptimistDigital\NovaSortable\...) and old Composer names can linger. Standardise on one namespace across resources and update upgrade guides when bumping majors.
  • Nova major version alignment. Nova 3, 4, and 5 have different internal APIs; pin the package version that matches your Nova major and test the index after any Nova upgrade. Drag-and-drop is a UI feature and is the first thing to break on a Nova upgrade.
  • Index the sort column. Every index page load orders by it. Add a database index, and consider a composite index with the group column when using grouped sorting.
  • Don’t stack sortable packages. If the model already uses another ordering package or a position trait, choose one. Two systems writing the same column will corrupt order silently.
  • Extension point: the trait itself. The behaviour lives in one small trait with static configuration and a few helper methods. If you need different query logic, override the relevant method in your resource rather than forking the package; keep those overrides documented because they’re easy to miss during debugging.
  • Debugging checklist: handles visible? → column exists and is writable? → network request returns 200? → rows updated in DB? → index query orders by the column (and isn’t overridden by $defaultSort or a global scope)? Work through those in order; it isolates nearly every issue.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings