advanced
Transition modals
Collect data in a modal before a card may enter a column.
Some columns need to know something before they will take a card. requiresFormOnEnter() opens a
Filament modal on the drop, and the move only happens once the form validates — cancel it and the
card stays where it was.
use Asmit\AdvancedKanban\Columns\KanbanColumn;
use Filament\Forms\Components\Textarea;
KanbanColumn::make('review')
->label('Review')
->requiresFormOnEnter([
Textarea::make('review_notes')
->label('Review notes')
->required(),
])
->allowedTransitions(['done', 'in_progress']);
The collected data is filled onto the record and saved in the same update as the new status, so
the fields need to be mass assignable on the model — review_notes above has to be in $fillable.
The form only appears when a card enters the column from another one; reordering inside the column doesn't ask for anything. It also runs after the workflow and locked-card rules, so a move that isn't allowed in the first place never reaches the modal.
The check lives on the server, not in the browser: a moveRecord() call from anywhere else hits
it too, so the data can't be skipped.
Building the form per card
Pass a closure to decide what to ask for. $record is the card being moved and $fromStatus is
the column it came from. Return an empty array to wave the card through without a modal.
KanbanColumn::make('review')
->requiresFormOnEnter(fn ($record, $fromStatus) => $fromStatus === 'todo'
? [
Textarea::make('review_notes')->required(),
Select::make('reviewer_id')
->label('Reviewer')
->options(User::query()->pluck('name', 'id'))
->required(),
]
: []);
Modal appearance
KanbanColumn::make('review')
->requiresFormOnEnter([...])
->transitionModalHeading('Send for review')
->transitionModalDescription('Tell the reviewer what changed.')
->transitionModalSubmitActionLabel('Send');
Without them the modal reads Move to Review with a Move submit button.
Doing something else with the data
saveTransitionDataUsing() replaces the default fill. It runs before the move is persisted, so
anything set on the record is saved along with the new status — and anything else, like writing a
separate row, is yours to do.
KanbanColumn::make('review')
->requiresFormOnEnter([
Textarea::make('notes')->required(),
])
->saveTransitionDataUsing(function ($record, array $data): void {
$record->reviews()->create([
'notes' => $data['notes'],
'user_id' => auth()->id(),
]);
$record->submitted_for_review_at = now();
});
The callback receives $record, $data, $column, and $status.
Since the callback writes before the status does, the modal runs inside a database transaction: the callback, the move hooks, and the status update either all commit or all roll back. A move that fails halfway can't leave a review logged against a card that never arrived.
To abort the move from inside the callback — a check that only the collected data can answer —
throw Halt. The modal stays open and nothing is written:
use Filament\Support\Exceptions\Halt;
->saveTransitionDataUsing(function ($record, array $data): void {
if (! $record->isReadyFor($data['reviewer_id'])) {
Notification::make()->title('That reviewer is unavailable.')->danger()->send();
throw (new Halt)->rollBackDatabaseTransaction();
}
// ...
});
rollBackDatabaseTransaction() matters: a bare Halt stops the move but commits whatever the
callback already wrote.
One consequence of the transaction: side effects in afterRecordMove() run before the commit. If
you queue a job there, set after_commit => true on the queue connection, or a worker can pick it
up and read the row as it was before the move.
See also
- Columns — labels, workflow transitions, and locked cards
- Moves and queries —
moveRecord()and move hooks