Reordering Records
Reordering lets the user drag rows into an order and writes that order back to a column. Reach for it when the order is data — menu items, steps in a checklist, priority in a queue — rather than something the reader is choosing to look at.
A minimal working example
Add an integer column to the table:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('menu_items', function (Blueprint $table): void {
$table->unsignedInteger('position')->default(0);
});2
3
4
5
6
Then name it on the schema:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\MenuItems\Tables;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
final class MenuItemsTable
{
public static function configure(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('label')->searchable(),
])
->reorderable('position');
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Every row now carries a drag handle, and dropping one posts the new order.
The API
use PandaPanel\Tables\TableSchema;
TableSchema::reorderable(string $column): self
TableSchema::getReorderColumn(): ?string
TableSchema::isReorderable(): bool2
3
4
5
| Method | Returns | Default |
|---|---|---|
reorderable('position') | self | off — a table is not reorderable until it says so |
getReorderColumn() | ?string | null |
isReorderable() | bool | false |
reorderable() also fixes the sort: it calls defaultSort($column, SortDirection::Ascending) for you. An order the user arranged only means something while the table is showing that order, so the two are one decision rather than two that can disagree.
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\SortDirection;
use PandaPanel\Tables\TableSchema;
$schema = TableSchema::make()
->columns([TextColumn::make('label')])
->reorderable('position');
$schema->getReorderColumn(); // 'position'
$schema->getDefaultSortColumn(); // 'position'
$schema->getDefaultSortDirection(); // SortDirection::Ascending2
3
4
5
6
7
8
9
10
11
The serialized definition carries one boolean, reorderable, which is what makes the handle column appear:
$schema->toArray()['reorderable']; // trueWhat the drag sends
The client works out the resulting order and posts the key order — position in that list is the order. It never invents a value for a column it knows nothing about.
POST {panel path}/actions/reorder route name: panel.{panelId}.actions.reorder{ "resource": "menu-items", "records": [7, 3, 12, 4] }A nested resource also sends parent, resolved and bound the way route middleware does for the resource's own pages.
The request is validated as records being an array of 1 to 500 entries, each required. Then, in order:
- The resource slug resolves inside the panel resolved for this request, or 404.
TableSchema::getReorderColumn()is non-null, or 400 — "This table is not reorderable."Resource::query()->findMany($keys)returns exactly as many records as keys, or 404. This one deliberately uses the list query rather than the record lookup: a trashed record has no place in the arrangement.Resource::canEdit($record)for every record, or 403 — checked before anything is written.- The writes run inside one
DB::transaction():$record->forceFill([$column => $position])->save()for each. A list that half-reordered would be worse than one that did not move.
The response redirects back with a success flash of "Order updated."
Position is the index in the submitted list
The controller flips the submitted keys into positions:
$positions = array_flip(array_map(strval(...), $keys));So the first key becomes 0, the second 1, and so on. Two consequences worth planning for:
- The client sends the rows on screen. Reordering on page two writes
0…n-1again for those rows, overlapping page one's values. Give a reorderable table aperPagelarge enough to hold the whole list, or accept that ordering is per page. - Whatever the table is currently sorted by is the order that gets written. If the user sorts by
labeland then drags a row, the positions recorded are the positions of the displayed order. Fixing the default sort to the order column is what makes the common case right;toggleable(false)on the columns that matter and leaving the other columns unsortable is what keeps it right.
Authorization
There is no per-action policy here — reordering is not an Action and has no name to look up. It is authorized with Resource::canEdit() on every record in the payload, before any of them is touched. A resource whose policy refuses even one row refuses the whole arrangement.
use App\Models\MenuItem;
use App\Policies\MenuItemPolicy;
use Illuminate\Support\Facades\Gate;
Gate::policy(MenuItem::class, MenuItemPolicy::class);2
3
4
5
Without a policy registered, the Gate refuses and the drag answers 403, which is the correct default: an unguarded write endpoint would be worse.
Notes
- Reordering is not an action. There is nothing to confirm and no handler to look up, only a new order to record, so it posts to its own endpoint and never enters the confirmation flow.
reorderable()arranges rows;reorderableColumns()arranges columns. The names are close and the features are unrelated: one writes a database column, the other is presentation and is never persisted to the database. See Column manager.- The column must be writable. The controller uses
forceFill(), so a$guardedlist does not block it, but a column that does not exist is a database error rather than a validation message. - 500 records per request is the hard limit in the endpoint's validation rules.
- Relation manager tables do not reorder. The schema accepts
reorderable()and the handle is drawn, but the relation table's frontend does not wire the drag to an endpoint, and the reorder endpoint resolves a resource rather than a relation. Reorder related records from their own resource list instead. - A reorderable table still paginates, searches, and filters. Nothing about reordering changes what the query returns.