The Publish UX Problem
Blog posts need a clear Published/Draft state. A simple boolean toggle is confusing — users don't know if "on" means published or live. The solution: a status Select that maps to the published_at timestamp.
Select::make('status')
->options(['published' => 'Published', 'draft' => 'Draft'])
->dehydrated(false) // don't write 'status' to DB
->live()
->afterStateHydrated(function (Select $component, $record) {
$component->state(
$record && $record->published_at !== null ? 'published' : 'draft'
);
})
->afterStateUpdated(function (Set $set, ?string $state) {
$set('published_at', $state === 'published' ? now() : null);
}),
The key is ->dehydrated(false) — Filament won't include the status field in the data it saves to the model. Instead, afterStateUpdated sets the real published_at field.
One-Click Publish in the Table
Action::make('togglePublish')
->label(fn ($record) => $record->published_at ? 'Unpublish' : 'Publish')
->icon(fn ($record) => $record->published_at ? 'heroicon-o-eye-slash' : 'heroicon-o-eye')
->color(fn ($record) => $record->published_at ? 'warning' : 'success')
->action(fn ($record) => $record->update([
'published_at' => $record->published_at ? null : now(),
]))
->requiresConfirmation(fn ($record) => (bool) $record->published_at),
Filament 4 Import Gotcha
In Filament 4 the namespaces changed. Custom table row actions live in Filament\Actions\Action, not Filament\Tables\Actions\Action. Using the wrong import causes a silent failure where the action appears but does nothing.
// Correct in Filament 4:
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
Watch out: Filament 4 also removed
BadgeColumn. Replace it with TextColumn::make()->badge()->color(fn (string $state) => match($state) { ... }).