hoceineel/filament-undo-toast
A Laravel Filament package that adds undoable toast notifications, letting users instantly reverse destructive actions like deletes. Customize messages, durations, and actions to make admin workflows safer and more forgiving.
undo-toast::undo-toast.models.{Model} keys. The package ships 23 locales and reuses Filament's terminology, so you usually only need keys for your own models.DeleteAction for the package's undo-enabled delete action (class/alias per README) and trigger a delete. You should get one toast — not one from Filament plus one from the package — with an Undo affordance and a mod+z shortcut.HasUndoableRelations on the model or an explicit ->restoreRelations([...]) on the action.mod+z after selecting rows for a bulk action. Also delete a record where no undo entry is created (e.g. policy/condition blocks it) and confirm Filament's normal "Deleted" notification still appears.Undo-enabled delete actions
Use the package action wherever you currently use DeleteAction, including table row actions and bulk actions. Keep Filament's confirmation modal; the undo toast happens after the fact.
// Resource table
UndoableDeleteAction::make(),
// Bulk
BulkActionGroup::make([
UndoableDeleteAction::make(),
]),
Notification replacement semantics
By default the undo toast replaces Filament's success notification for that action. If you have customised successNotification() or successNotificationTitle(), those are kept and Filament's notification is used instead. Turn replacement off globally for an action, or decide per action with a closure:
UndoableDeleteAction::make()
->replaceSuccessNotification(false);
UndoableDeleteAction::make()
->replaceSuccessNotification(fn ($action) => auth()->user()->isAdmin());
Remember Filament's notification still shows whenever no undo entry is created — that is the fallback path, not a bug.
Restoring relations on hard delete
Declare relations once on the model:
class Post extends Model implements HasUndoableRelations
{
// declare the relations the package should restore (see the contract for the method name)
public function undoableRelations(): array
{
return ['comments', 'tags'];
}
}
Or override per action:
UndoableDeleteAction::make()
->restoreRelations([
Post::class => ['comments', 'tags'],
]);
Supported relation types are BelongsToMany (pivot rows including pivot columns), HasMany, HasOne, MorphMany and MorphOne. Rows are inserted only when missing, inside the same transaction as the model restore.
Translations for model labels
Default English labels resolve from undo-toast::undo-toast.models.{Model}, then from your app's own translation of the label. Add keys for your domain models so non-English panels don't leak English:
// lang/fr/undo-toast.php
return [
'models' => [
'Post' => 'article',
'Comment' => 'commentaire',
],
];
Bulk actions and keyboard flow
Because mod+z now works while a checkbox, radio or button has focus, the bulk-delete flow works without clicking away: select rows, delete, then mod+z. Ensure your own JS doesn't intercept the shortcut or trap focus in a way that prevents the toast from receiving it.
Testing in your app
Assert the actual delete and undo outcomes rather than the toast text, since notification assertions may change when replacement is on. For Livewire tests: call the action, assert the row is gone, invoke undo, assert the row (and any restored relations) is back. Add an explicit case for the "no undo entry" path to confirm Filament's notification still fires.
successNotification()/successNotificationTitle() (which intentionally keeps Filament's notification) or overrode the action's notification behaviour yourself.HasManyThrough, MorphToMany, custom relations) are not restored. Model the relation as one of the supported types or handle it after the undo yourself.BelongsToMany, but verify timestamps/extra pivot data survive if your schema depends on them.mod+z works with form controls focused, but should not fire while typing in text inputs (that would break native text undo). If you add your own global shortcut handlers, don't fight the package's.replaceSuccessNotification closure mistakes. Return a strict boolean; a truthy non-boolean from a loosely written closure can make behaviour harder to reason about. Keep the closure cheap — it may run per action invocation.clip-path animations can reintroduce the clipping and stack-collapse bugs.How can I help you explore Laravel packages today?