packstub/filament-kanban
Filament Kanban adds drag-and-drop Kanban boards to Filament admin panels. Organize records into columns, move cards between stages, and manage workflows without leaving the panel. Simple setup for Laravel and Filament apps.
This is a Filament board component: you describe the board in PHP (query, columns, card actions) and it renders a drag-and-drop Kanban with server-side enforcement of moves.
Minimal path to a working board:
php artisan filament:assets), then clear caches.public function board(Board $board): Board
{
return $board
->query(Task::query())
->columns(Status::class) // one column per enum case
->cardAction('edit') // click a card → edit modal
->createAction(CreateAction::make()->schema([
Forms\Components\TextInput::make('title')->required(),
]));
}
Where to look first: the board class you just wrote, then the enum (add HasLabel and HasColor — the board reads those for column titles and colours), then Column::limit() and summarize() for the header.
First real use case: a task/ticket board on a model with a status column. Drag a card between columns, watch the status attribute persist, click a card to edit it in a modal, hit "+" on a column to create a record pre-filled with that column's value.
Board query is the single source of truth. Card actions are resolved through the board's query, so scoping the query (tenant, project, ->where('archived', false)) scopes what users can act on. Build the query once and let actions, WIP checks, and summaries all inherit it.
Enum columns for status workflows. columns(Status::class) or Column::fromEnum() generates one column per case with labels and colours pulled from HasLabel/HasColor. This is the pattern to default to; hand-rolled Column::make('in_progress') only when the column key isn't a straight enum case.
Card actions, two flavours. Card::actions([...]) narrows the menu per card (e.g. only show DeleteAction when the record is a draft), and the card's menu holds any Filament action you can build — EditAction, DeleteAction, or your own with a schema. cardAction('edit') is the click-through behaviour: name an action and clicking the card body runs it in a modal or slide-over. The board refreshes itself after the action completes, so you rarely need manual $this->resetTable()-style calls — but do dispatch your own refresh if the action mutates something shown outside the board.
Create-in-column. createAction(CreateAction::make()->schema([...])) adds a "+" to every droppable column and sets the column attribute on the new record for you. Use Column::creatable(false) for columns that shouldn't take new records (e.g. "Done", "Archived"). The server independently refuses creates on hidden, read-only, and full columns, so hiding the button is cosmetic, not the safeguard.
WIP limits as a workflow tool. Column::limit(5) renders 3/5 in the header and rejects both moves and creates at the boundary, server-side. Pair it with a summary so users understand why a column is full. Expect to surface the rejection message to users — decide early whether that's a notification or an inline board message.
Summaries for at-a-glance metrics. summarize(fn (Builder $query) => $query->sum('points')) gives you a line under each column title, recomputed after every move. Keep these cheap: they run per column and re-run on drag, poll, and action refresh. Prefer a single aggregate over a query per row.
Polling for multi-user boards. poll('10s') picks up other people's changes. It pauses while someone is dragging, and — importantly — a poll preserves the "Load more" state, so a user with 200 cards expanded doesn't get snapped back to page one every ten seconds. Tune the interval by board activity; 10s is aggressive for large boards with expensive summaries.
Reacting to moves. Listen for Packstub\Kanban\Events\CardMoved server-side (audit log, notifications, recalculating denormalised counters), and hook the kanban-card-moved browser event for Alpine-side reactivity (closing a detail panel, updating a counter in another widget).
Avatars on cards. Card::avatar($url, $name) puts owner/assignee faces in the card foot, with initials as the fallback when there's no picture. The instant search matches avatar names too, so "assigned to Sam" becomes searchable without extra config.
HasActions = no actions. If your board component doesn't implement Filament's action system, card and create actions silently don't appear. If the "+" or the card menu is missing, check this before anything else..sortable-ghost or styling cards via a parent selector, it will no longer match the ghost; style the ghost class directly.sum() is fine; a withCount chain across relations on a big board is not. Profile with the poll interval set to something short.HasLabel (or getLabel()) isn't implemented on the enum — that's the fix, not a board option.Event::listen / $listen), and the browser event needs an Alpine x-on:kanban-card-moved.window listener. Both fire after the move persists, so don't use them to validate a move — the server-side WIP/limit checks are the validation layer.Card::actions(), per-column creatable(false), and the CardMoved event as the seam for anything the package doesn't do — audit trails, webhooks, Slack pings, denormalised counters on the parent project.How can I help you explore Laravel packages today?