Changes to Admin Panel resources in v2.13.0
TLDR: When you upgrade to Filament v2.13.0, new admin resources and relation managers that you generate will have "actions". These actions allow you to more easily customise parts of your admin panel interface, without overriding methods or properties. Your existing resources and relation managers should continue to work exactly the same as they have done previously.
If you're interested to learn more about why we made the changes, and how to add actions to your existing resources, read on. This is a long blog post, so I don't blame you if you don't want to read it. I just think it's important for some users to understand our reasoning, and also learn more about how Filament works internally as well. If you're just interested in our new "soft deletes" feature for the admin panel, skip to this section. If you're interested in upgrading your existing resources and relation managers, skip to this section.
In Filament, "actions" are PHP objects that you can use to run a task in your admin panel. For example, you can delete an Eloquent record, dispatch a job, open a URL. Actions are powerful, as they have the ability to open a modal and accept user input, which can then be used as data when the action runs.
There are a few different types of action, which are used in different places of the Filament project:
Previously, all actions extended a base "Action" class. Let's take table actions as an example:
use Filament\Tables\Actions\Action;use Illuminate\Database\Eloquent\Model; Action::make('edit') ->label('Edit customer') ->url(fn (): string => CustomerResource::getUrl('edit', ['record' => $record])) ->color('primary') ->icon('heroicon-s-pencil') Action::make('delete') ->label('Delete customer') ->action(fn (Model $record) => $record->delete()) ->requiresConfirmation() ->color('danger') ->icon('heroicon-s-trash')
Both the "edit" and "delete" actions use the Action class to create a new action.
This was fine, but we found that there was repetition between different parts of the admin panel. For example:
All these edit actions use the same Filament\Tables\Actions\Action
class, have the same label, the same color, the same icon, and the same logic that could be used to load and save the record data if you use a modal to edit the record.
In addition to the repetitive code, the translations strings for each "edit" action were copied for each context, which added extra overhead for developers when translating the admin panel into their language.
As well as the internal issues, the DX experience for end users could be optimized. For example, to change the label for an action, you'd have to override a method on the admin panel page class:
use Filament\Pages\Actions\Action; protected function getCreateAction(): Action{ return parent::getCreateAction()->label('New label');}
While this is quite simple code, it relies on inheritance and is a little unclear exactly where getCreateAction()
comes from originally.
Also, if users wanted to add their own actions, the experience was not optimal. For example, adding a new page action looked a bit like:
use Filament\Pages\Actions; protected function getActions(): array{ return array_merge(parent::getActions(), [ Actions\Action::make('replicate'), ]);}
And adding a new table action from the resource always felt a little hacky, using prependActions()
and pushActions()
to add actions to either end of the existing actions array:
use Filament\Tables; // Existing table actions: Edit, View $table ->prependActions([ Tables\Actions\Action::make('replicate'), ]) ->pushActions([ Tables\Actions\Action::make('delete'), ]) // New table actions: Replicate, Edit, View, Delete
Now we have identified the issues with our existing actions structure, it's time to list the goals of the refactor. Which of those problems are solvable without introducing any breaking changes?
Ryan and I have discussed the concept of "pre-built" actions in the project for a while now. The concept is simple - have a dedicated action class for each of the main CRUD actions, which contains all the shared code between actions of that type.
On 27th April 2022, we released v2.11.4, which introduced a new "replicate" action. You can check out Ryan's PR here. This was the first "pre-built" action we introduced into the codebase. It's usage was simple - you could add it to any table, and it would duplicate a record:
use Filament\Tables; $table ->prependActions([ Tables\Actions\ReplicateAction::make(), ])
This was a simple starting point that proved the concept of dedicated action classes worked. Shortly after, we planned to do the same for every action, by Filament v3. The possibilities were very exciting, as it would allow us to introduce some cool new features for the admin panel that previously required custom actions. For example, we could add pre-built actions to handle soft-deleting records - ForceDeleteAction
, ForceDeleteBulkAction
, RestoreAction
and RestoreBulkAction
. We could even include a TrashedFilter
which would allow the user to filter deleted records in their queries, without building their own ternary filter with these options.
At some point, I was chatting with Dennis Koch about this, and he decided to PR support for these new soft delete action classes and the TrashedFilter
. He did a good job, and it inspired me to get on and refactor our entire actions architecture to also use these pre-built classes. I didn't like the thought of having half of our codebase using custom actions, and half using this structure.
In v2.13.0, we now have the following action classes:
AssociateAction
- Associates an existing record onto a one-to-many relationship. It opens a modal which contains a Select field to allow the user to choose which record to associate. Can be used in table headers and on admin panel pages.AttachAction
- Attaches an existing record onto a many-to-many relationship. It opens a modal which contains a Select field to allow the user to choose which record to attach. Can be used in table headers and on admin panel pages.CreateAction
- Creates a new record. It opens a modal which contains a form, which is saved to the database when submitted. Optionally, you can specify a URL to open when the link is clicked, which will send you to a Create page instead of using a modal form. Can be used in table headers and on admin panel pages.DeleteAction
- Deletes a record from the database. Asks for confirmation in a modal first. Can be used in table rows and on admin panel pages.DeleteBulkAction
- Deletes multiple records from the database. Asks for confirmation in a modal first. Can be used in table bulk action dropdown menus.DetachAction
- Removes a record from a many-to-many relationship. Asks for confirmation in a modal first. Can be used in table rows.DetachBulkAction
- Removes multiple records from a many-to-many relationship. Asks for confirmation in a modal first. Can be used in table bulk action dropdown menus.DissociateAction
- Removes a record from a one-to-many relationship. Asks for confirmation in a modal first. Can be used in table rows.DissociateBulkAction
- Removes multiple records from a one-to-many relationship. Asks for confirmation in a modal first. Can be used in table bulk action dropdown menus.EditAction
- Updates data for an existing record. It opens a modal which contains a form, which is saved to the database when submitted. Optionally, you can specify a URL to open when the link is clicked, which will send you to an Edit page instead of using a modal form. Can be used in table rows and on admin panel pages.ForceDeleteAction
- Force-deletes a soft-deleted record from the database. Asks for confirmation in a modal first. Can be used in table rows and on admin panel pages.ForceDeleteBulkAction
- Force-deletes multiple soft-deleted records from the database. Asks for confirmation in a modal first. Can be used in table bulk action dropdown menus.ReplicateAction
- Duplicates a record in the database. Optionally allows the user to customize the new record using a form in a modal. Asks for confirmation in a modal first. Can be used in table rows and on admin panel pages.RestoreAction
- Restores a soft-deleted record in the database. Asks for confirmation in a modal first. Can be used in table rows and on admin panel pages.RestoreBulkAction
- Restores multiple soft-deleted records in the database. Asks for confirmation in a modal first. Can be used in table bulk action dropdown menus.ViewAction
- Uses a modal form to show all record to the user. All fields in the form are disabled so they are not editable. Optionally, you can specify a URL to open when the link is clicked, which will send you to a View page instead of using a modal form. Can be used in table rows and on admin panel pages.It's a lot, yes. But the admin panel has many functions, and I wanted to make sure that everything was covered.
It took a little while, but I built all of the other action classes, and then refactored the existing admin panel classes to use them.
As well as reducing internal repetition in the Filament codebase, these pre-built actions unlock a range of new DX optimizations for users of the table builder. Instead of building CRUD actions yourself, you can now just use these:
use Filament\Tables; protected function getTableHeaderActions(): array{ return [ Tables\Actions\CreateAction::make()->form([...]), ];} protected function getTableActions(): array{ return [ Tables\Actions\ViewAction::make()->form([...]), Tables\Actions\EditAction::make()->form([...]), Tables\Actions\DeleteAction::make(), ];} protected function getTableBulkActions(): array{ return [ Tables\Actions\DeleteBulkAction::make(), ];}
This alone deserves extensive documentation and probably a YouTube video, which I will work on in the following weeks / months.
Now to tackle the last two goals - improving the DX for admin panel users who want to customize the actions in their admin panel.
As a reminder, previously, adding new actions to an admin panel table would require the use of prependActions()
or pushActions()
:
use Filament\Tables; $table ->prependActions([ Tables\Actions\ReplicateAction::make(), ])
Now, we're including the action classes in the generated code for resources and relation managers:
use Filament\Tables; $table ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ])
So you can add a new one like this:
use Filament\Tables; $table ->actions([ Tables\Actions\ReplicateAction::make(), Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ])
Adding actions to a resource page used to look like this:
use Filament\Pages\Actions; protected function getActions(): array{ return array_merge(parent::getActions(), [ Actions\ReplicateAction::make(), ]);}
Now, we're including the action classes in the generated code for resource pages:
use Filament\Pages\Actions; protected function getActions(): array{ return [ Actions\DeleteAction::make(), ];}
So you can add a new one like this:
use Filament\Pages\Actions; protected function getActions(): array{ return [ Actions\DeleteAction::make(), Actions\ReplicateAction::make(), ];}
I'm sure you will agree, this is a much more verbose approach and really improves the clarity of your Filament code.
Now, you can easily customize actions, just by chaining methods. No more overriding methods or properties:
use App\Models\User;use Filament\Pages\Actions\CreateAction;use Illuminate\Auth\Events; protected function getActions(): array{ return [ CreateAction::make() ->label('Register user') ->modalHeading('Sign up user') ->after(fn (User $record) => event(new Registered($record))), ];}
I've also taken this opportunity to refactor relation managers a bit. Before, depending on the relationship, you would use a different command to generate your relation manager:
php artisan make:filament-has-many ...php artisan make:filament-belongs-to-many ...php artisan make:filament-morph-many ...
Using a different command would mean that you get a different relation manager class generated - HasManyRelationManager
, BelongsToManyRelationManager
, MorphManyRelationManager
. Which class you used dictated which actions were on your relation table, for example, BelongsToManyRelationManager
had attach / detach actions which HasManyRelationManager
did not have.
Now, in v2.13.0, you only need one command to generate a relation manager:
php artisan make:filament-relation-manager ...
This command generates a class that simply extends RelationManager
, which is used for every type of relationship.
To add attach / detach or associate / dissociate actions, you can just use --attach
or --associate
:
php artisan make:filament-relation-manager ... --attachphp artisan make:filament-relation-manager ... --associate
The Filament documentation previously stated that you needed to use properties like $shouldPreloadAttachActionRecordSelectOptions
on the relation manager class to preload the Select field for the attach action. This has now been moved to a simple preloadRecordSelect()
method:
use Filament\Tables\Actions\AttachAction; AttachAction::make()->preloadRecordSelect()
You may wish to define a custom form to add pivot attributes to the relationship. You can now do this with the form()
method, instead of overriding attachForm()
on the relation manager class:
use Filament\Forms;use Filament\Tables\Actions\AttachAction; AttachAction::make() ->form(fn (AttachAction $action): array => [ $action->getRecordSelect(), Forms\Components\TextInput::make('role')->required(), ])
In this example, $action->getRecordSelect()
outputs the select field to pick the record to attach. The role
text input is then saved to the pivot table's role
column.
Simple resources are resources with only one page - Manage. This replaces List, Create, Edit and View, as it handles all those actions in modals instead.
Generating those simple resources remains the same, and they now contain all the pre-built actions inside the table()
method.
However, you can now do cool stuff like making your resources partially "simple". For example, you may want the user to create records in a modal instead of a new page, but keep the Edit page for updating records. In this case, you can just delete the Create page class, and remove it from your resource's getPages()
method. Filament will detect that there is no Create page, but since you still have a CreateAction
on your List page, it will open the create form in a modal!
Additionally, you may wish to steal the "delete" action from simple resource tables and add it to your normal resource. Now, you can just add the DeleteAction
to your resource file:
use Filament\Tables; $table ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ])
Last but not least, we've added a --soft-deletes
flag to the make:filament-resource
and make:filament-relation-manager
commands:
php artisan make:filament-resource ... --soft-deletesphp artisan make:filament-relation-manager ... --soft-deletes
This will automatically configure soft delete actions and the new TrashedFilter
to your resource or relation manager file/s:
use Filament\Tables; $table ->columns([ // ]) ->filters([ Tables\Filters\TrashedFilter::make(), ]) ->actions([ Tables\Actions\EditAction::make(), ]) ->bulkActions([ Tables\Actions\DeleteBulkAction::make(), Tables\Actions\RestoreBulkAction::make(), Tables\Actions\ForceDeleteBulkAction::make(), ]);
use Filament\Pages\Actions; protected function getActions(): array{ return [ Actions\DeleteAction::make(), Actions\ForceDeleteAction::make(), Actions\RestoreAction::make(), ];}
It will also remove the SoftDeletingScope
from your resource, so you are able to access soft deleted records without restrictions.
Firstly, ensure you are using Filament v2.13.0. If you're using official Filament plugins, make sure you are also using v2.13.0 for those as well.
In your resource, add actions to the $table->actions()
and $table->bulkActions()
. You may want to exclude ViewAction
and DeleteAction
if you don't need those:
use Filament\Tables; $table ->columns([ // ]) ->actions([ Tables\Actions\ViewAction::make(), Tables\Actions\EditAction::make(), ]) ->bulkActions([ Tables\Actions\DeleteBulkAction::make(), ]);
On the List page class:
use Filament\Pages\Actions; protected function getActions(): array{ return [ Actions\CreateAction::make(), ];}
On the Edit page class (you might want to delete the ViewAction
if you don't have a View page):
use Filament\Pages\Actions; protected function getActions(): array{ return [ Actions\ViewAction::make(), Actions\DeleteAction::make(), ];}
On the View page class (you might want to delete the EditAction
if you don't have an Edit page):
use Filament\Pages\Actions; protected function getActions(): array{ return [ Actions\EditAction::make(), ];}
The process of customizing simple resources changes when you use the new actions structure. We've documented the new customization options in our docs. Check out the docs for create modals, edit modals, and view modals.
Firstly, relation managers now only extend 1 class - RelationManager
:
use Filament\Resources\RelationManagers\RelationManager; class PostsRelationManager extends RelationManager{ // ...}
Now, add the header actions, row actions, and bulk actions to the $table
:
use Filament\Tables; $table ->columns([ // ]) ->filters([ // ]) ->headerActions([ Tables\Actions\CreateAction::make(), ]) ->actions([ Tables\Actions\EditAction::make(), Tables\Actions\DeleteAction::make(), ]) ->bulkActions([ Tables\Actions\DeleteBulkAction::make(), ]);
If you have a many-to-many relation manager, you might want attach / detach actions:
use Filament\Tables; $table ->headerActions([ // ... Tables\Actions\AttachAction::make(), ]) ->actions([ // ... Tables\Actions\DetachAction::make(), ]) ->bulkActions([ // ... Tables\Actions\DetachBulkAction::make(), ]);
If you have a one-to-many relation manager, you might want associate / dissociate actions:
use Filament\Tables; $table ->headerActions([ // ... Tables\Actions\AssociateAction::make(), ]) ->actions([ // ... Tables\Actions\DissociateAction::make(), ]) ->bulkActions([ // ... Tables\Actions\DissociateBulkAction::make(), ]);
The process of customizing relation managers changes when you use the new actions structure. We've documented the new customization options in our docs. Double check that any methods or properties on your class haven't been deprecated.
Filament's aim is to make your Laravel DX more productive. These changes, while quite monumental, work towards that goal. If you have any ideas, you can find our active community on Discord. If you have any questions or issues when you upgrade to these new features, let us know there or on GitHub. We'd love to help you out.
Filament mean AWESOME . Nice and clean 👌 thanks a a lot Dan, Ryan an all help to growth this package. And thanks for blog post ❤
Amazing article. It explains everything clearly. Thank you all guys for your great works.