All pages

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.

php
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.

php
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(),
        ]
        : []);
php
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.

php
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:

php
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