filamerce/filament-model-states
Filament plugin for managing Laravel model states directly from your admin panel. View current states, transition models through their state machine, and add state columns, badges, and actions—integrating with spatie/laravel-model-states for clean, typed state management.
composer require filamerce/filament-model-states
php artisan vendor:publish --tag="filament-model-states-config"
The package is a thin Filament bridge over spatie/laravel-model-states — you still define your state machine the Spatie way (class Draft extends State { public static $name = 'draft'; } plus Transition classes and a HasStates model), and this package supplies the Filament presentation layer for it.
Where to look first, in order:
config/filament-model-states.php — the color/icon maps that drive badges and transition buttons. This is the file you will touch most.StateColumn / StateSelect examples — those two components are ~90% of real usage.grep -rn "FilamentIcon::register" vendor/filamerce/filament-model-states/src) — as of v2.2.0 this is where the icon alias list lives, and reading it is the fastest way to learn the alias names you can override.First use case — a table badge plus a transition action:
use Filamerce\FilamentModelStates\Columns\StateColumn;
use Filamerce\FilamentModelStates\Forms\Components\StateSelect;
// OrderResource
public static function table(Table $table): Table
{
return $table
->columns([
StateColumn::make('state'), // renders label + color + icon
])
->actions([
Action::make('transition')
->form([StateSelect::make('state')])
->action(fn ($record, array $data) => $record->state->transitionTo($data['state'])),
]);
}
Make sure the model casts the attribute (protected $casts = ['state' => OrderState::class];) — without the cast the column receives a raw string and renders nothing useful.
Badge-first resources. The dominant pattern is read-only state display in the table and all mutation happening through actions. Keep StateColumn on the table and put the transition affordance in a row action rather than an inline editable select; it keeps the audit trail (Spatie fires events on transition) meaningful.
Drive the select from the state machine, not the enum. StateSelect should be fed the transitions actually legal from the current state. If the package's select exposes the full state list, constrain it with a closure over $record->state->transitionableStates() (or your own allowedTransitions() helper) so users can't submit an illegal transition and eat a TransitionNotFound exception.
Group transitions into a single action. Instead of one button per transition (which multiplies as the state machine grows), expose a ActionGroup or a select-based action whose options are the legal target states. Confirmation modals belong on destructive transitions (cancelled, refunded); a ->requiresConfirmation() on all of them trains users to click through.
Bulk transitions carefully. Bulk actions run per-record and each fires a transition — wrap in a transaction only if your domain allows it, and ->deselectRecordsAfterCompletion() so the table refreshes correctly. A failed transition mid-batch should surface which records were skipped.
Notifications close the loop. After transitionTo(), send a Filament notification with the new state's color/label pulled from the same resolver the column uses, so the vocabulary stays consistent.
Theme icons globally (new in v2.2.0). Previously the badge/action icons were baked into the package. Now they're registered as Filament icon aliases, which means you can swap them for your icon set in one place:
use Filament\Support\Facades\FilamentIcon;
public function boot(): void
{
FilamentIcon::register([
// check the package provider for exact alias keys — they follow
// the `filament-model-states::…` convention
'filament-model-states::state-column' => 'heroicon-o-flag',
]);
}
Do this in a service provider that boots after the package's, and remember php artisan filament:icon-cache if you cache icons in production — a stale cache is the usual reason an override "doesn't work."
Integration with policies. Gate the transition action on ->visible(fn ($record) => auth()->user()->can('transition', $record)). The package renders what you give it; it does not consult your policies, so authorization is entirely your responsibility.
->formatStateUsing() that receives null for records created before the state column existed. Backfill or provide a default state before shipping the column.success, warning, danger, info, gray, primary resolve; #ff0000 does not render as a badge color. Custom branding goes through a panel palette or a custom getColor() returning a registered name.SelectFilter won't match user-friendly labels. Build the filter options from State::all() with labels as keys, and be aware alphabetical sorting sorts class names, not human labels — use a sortable() closure with a CASE expression if ordering matters.$record->refresh() (and returning the record from the action) avoids the "clicked it twice because nothing changed" bug.FilamentIcon::register() in your own provider wins only if it runs later. If the alias isn't taking effect, check provider order first, then filament:icon-cache, then whether the panel actually uses the icon set you named.TransitionNotFound / InvalidTransition; catch them in the action and convert to a Notification::make()->danger() rather than letting the Livewire request explode.StateColumn and the action components accept closures for color(), icon(), and label(), so anything the config can't express you can override per-column without forking. For a genuinely custom look, publish views and swap the badge view rather than fighting the resolver chain.FilamentIcon::register() instead; the fork will break on future upgrades for no benefit.How can I help you explore Laravel packages today?