xingchuangyang/dcat-admin-sortable
xingchuangyang/dcat-admin-sortable is a Laravel/Dcat Admin extension that adds drag-and-drop row sorting to admin list grids. Reorder records directly in the table and persist their positions via a sortable field, with straightforward installation and configuration for Dcat Admin projects.
Install and pin. This package is not on a well-trodden path (5 stars, 0 dependents), so install with an exact version constraint rather than a caret:
composer require xingchuangyang/dcat-admin-sortable:1.x.y
Before anything else, open vendor/xingchuangyang/dcat-admin-sortable/composer.json and confirm three things: the laravel/framework and dcat/laravel-admin constraints intersect your stack, the name field actually matches xingchuangyang/dcat-admin-sortable (the repo is called doct-admin-sortable — a divergence worth verifying before you trust the tag), and the license permits an internal fork. If any of those fail, stop and budget the 1–2 days to reimplement.
Where to look first. The package is small — read it end to end in one sitting. Focus on three files: the service provider (what it registers into Dcat), the grid tool / column class (how it binds to Dcat\Admin\Grid), and the JS asset plus the controller method that receives the drag payload. That controller is the security-relevant surface: check what it does for authorization, which columns it writes, and whether it's transactional. You will likely own this code, so reading it now is cheaper than debugging it later.
Prepare the schema yourself. Do not let the package own the column — add it in your own migration so the contract is yours:
Schema::table('banners', function (Blueprint $table) {
$table->unsignedInteger('sort')->default(0)->index();
});
Backfill deterministically so existing lists don't reshuffle on first load:
DB::statement('
UPDATE banners b
JOIN (SELECT id, ROW_NUMBER() OVER (ORDER BY created_at, id) - 1 AS rn FROM banners) t
ON b.id = t.id
SET b.sort = t.rn
');
Default the model scope. Every read path must be stable, otherwise the drag "works" but the grid reloads in a different order:
public function scopeOrdered(Builder $q): Builder
{
return $q->orderBy('sort')->orderBy('id'); // id tie-breaker matters
}
First use case: one grid, one column. Pick the list where admins currently type integers — banners, FAQs, nav items, form fields. Attach the sort control in that resource's Grid method. The exact call differs by tag; confirm against the package README, but expect something in the shape of a grid column or tool registration:
protected function grid()
{
return Grid::make(new Banner(), function (Grid $grid) {
$grid->model()->ordered();
// confirm actual method name against the README for your pinned tag
$grid->addSortableColumn('sort');
$grid->columns(['id', 'title', 'sort', 'created_at']);
});
}
Drag a row in the browser, reload, confirm the order persisted. If that works, the integration is done. If it doesn't, the failure is almost always the model scope, a stale asset cache, or the endpoint failing silently — see below.
Wrap the package behind an internal helper. This is the single highest-leverage pattern given the maintenance profile. Do not sprinkle package calls across every resource; route them through one helper so a future swap (or fork) touches one file:
namespace App\Admin\Support;
class SortableGrid
{
public static function attach(Grid $grid, string $column = 'sort'): void
{
if (! config('admin.features.drag_sort')) {
return;
}
$grid->model()->ordered();
$grid->addSortableColumn($column); // adjust to pinned version's API
}
}
Then every resource calls SortableGrid::attach($grid). When the package breaks on a Dcat minor upgrade, you fix one method.
One resource at a time, behind a toggle. Ship to a single grid first (banners is a good candidate), gate it on a config flag, and roll out per resource as stakeholders ask. This bounds blast radius and gives you a kill switch that doesn't require a revert.
Keep the manual fallback in the form. Leave the sort field editable in the resource's form, or expose it as a read-only display at minimum. Drag-and-drop is a JS-dependent affordance; if the asset 404s after a deploy, admins need another way to set order. This also gives you a way to repair corrupted ordering by hand.
Scope-aware reordering is your main correctness question. Before trusting the package on any grid with filters or tabs, verify what happens when an admin filters to a subset and drags. A naive implementation writes absolute positions from the visible list, which renumbers everything and demotes rows that were filtered out. Test it explicitly:
If this fails, either disable drag while filters are active or scope reordering server-side by the same query constraints. Many sorting packages get this wrong.
Test the persistence endpoint directly, outside the browser. Write a feature test that hits the reorder route under a real admin session, and assert on the resulting sort values in the database:
public function test_reorder_persists_positions()
{
$this->actingAs($admin = Admin::factory()->create());
Banner::factory()->count(5)->create();
[$a, $b] = Banner::ordered()->take(2)->get();
$this->postJson(route('admin.banners.sort'), [
'order' => [$b->id, $a->id],
])->assertOk();
$this->assertSame($b->id, Banner::ordered()->first()->id);
}
Cover the cases that bite: pagination across pages, active filters, rows with equal or NULL sort values, and the authorization path (a non-superuser admin who lacks update permission on the resource).
Sequence the rollout. Spike branch → authorization check under a real admin (not just the superuser) → edge-case QA → single-resource release → two-sprint review gate to decide keep, fork, or replace. Put the review date on the calendar when you merge, not when you remember.
Pagination will surprise you. Dragging on page 2 must not reset rows to positions 0, 1, 2. Confirm the endpoint offsets by the page cursor or by the row's current value. If it doesn't, sorting page 2 will interleave those rows into the top of the list. Test with perPage(15) and a table of 30+ rows.
Concurrency is last-write-wins. The typical write pattern is "rewrite sort for every row affected by the drag," O(n) writes, not transactional, no locking. Two admins reordering simultaneously will silently clobber each other. For internal admin tooling this is usually acceptable — but know it, and don't adopt this package for anything where a stale write causes real damage. If you need safety, wrap the endpoint in a transaction with lockForUpdate() on the affected rows, and reload the grid after each drop rather than assuming the client's view is current.
Equal or NULL sort values break the drag. If your backfill leaves duplicates or zeros, the first drag renumbers a subset and the rest stay in arbitrary order. Enforce uniqueness of intent: index the column, backfill with ROW_NUMBER(), and always order by sort, id so ties resolve deterministically. A drag that "doesn't save" is frequently a tie-breaking problem, not a persistence problem.
Confirm authorization on the reorder route. The endpoint accepts row IDs and positions — which means it accepts any row IDs. Verify it runs behind Dcat's admin middleware and that it respects the per-resource policy, not just "is logged in." Ideally it should also reject mass-assignment of columns other than the sort field. Read the controller; don't assume. If it doesn't check policy, add the check in your fork before rollout.
The package name and repo name differ — pin tags and verify they're immutable. xingchuangyang/dcat-admin-sortable vs doct-admin-sortable is either a typo or a fork, and the difference matters for provenance. Once pinned, record the resolved commit hash. If the maintainer force-pushes or deletes a tag, your builds break and you can't tell what you were running.
Fork at adoption time, not after the first breakage. Grid tool registration is the most fragile part of Dcat's API surface, and a single-maintainer, low-star package will not keep pace with Dcat 2.x minors. Move it into acme/dcat-admin-sortable (vendored or VCS repo) on day one, record the Dcat constraint explicitly, and re-verify against each Dcat upgrade. The code footprint is small enough that a fork is a morning's work — cheaper than a stalled upgrade.
Asset caching will cost you an hour. Dcat publishes package assets; after install or upgrade run php artisan admin:publish (or your project's equivalent), then hard-refresh. A drag handle that renders but doesn't drag is almost always stale JS, not broken PHP. Check the browser network tab for a 404 on the sortable asset before debugging server-side.
Tree and nested grids are out of scope. This package models flat ordering over one integer column. If a resource is a nested set or a parent_id hierarchy, drag-and-drop across levels will produce nonsense. Use a dedicated nested-set package (e.g. kalnoy/nestedset) for those, and keep this package confined to flat lists.
Log the reorder call. A one-line Log::info('reorder', ['resource' => ..., 'admin' => ..., 'order' => $payload]) in the endpoint (via your fork) gives you a reconstruction path when someone reports "my order got scrambled." The package almost certainly doesn't include audit history, and this is the cheapest substitute.
Watch the query cost on large tables. Every drag rewriting every row is fine at 200 rows and painful at 50,000. Cap adoption to lists with modest row counts, or implement a sparse-integer / fractional-index scheme in the fork if you need to scale.
How can I help you explore Laravel packages today?