Blog

Changes to Admin Panel resources in v2.13.0

Jun 14, 2022
Dan Harrin
Admin panel, News, Table builder

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:

  • Tables can have actions, which are links displayed at the end of each table row. By default, admin resources have an "edit" action link, which takes the user to the Edit page of the resource.
  • Tables can also have "header" actions, which are buttons displayed above the table content. These are used by components like the relation manager to render the "create" button, which opens a modal to create a new related record.
  • Also, tables can have "bulk" actions, which can run a task on multiple Eloquent records at once. Out of the box, these are used by resources and relation managers to allow you to delete many table records at once.
  • Admin panel pages can have actions, which are buttons rendered in the header of a page. Resources use page actions to add a "New" button to resource List pages and a "Delete" buttons to Edit pages, which opens a confirmation modal.
  • Recently, we've also added the concept of actions to our Form Builder. Now, form components can open their own actions, with modals. Select fields can open a "create option form", which is used to add a new option to the Select, storing it in the database. For example, this could be used to create a customer from an Order resource, without leaving the page.

The state of actions - pre-v2.13.0

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:

  • The "edit" action was found on resources, used to send the user to the Edit page of the resource, or open a modal to edit the record without leaving the page.
  • The "edit" action was also found on relation managers, which allows the user to edit a related record.
  • If the user had any custom tables built using the Table Builder, they'd have to create their own "edit" action.

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

Our goals of the refactor

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?

  • Reduce internal repetition in code.
  • Allow users of the table builder to easily build CRUD tables, without having to define custom actions themselves. The admin panel would be able to use these pre-built actions to simplify the internal code of the package.
  • Improve DX for customizing default admin panel actions, avoid overriding class methods and properties.
  • Improve DX for adding new actions to tables and pages.

Introducing - pre-built action classes to reduce code repetition

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.

Using the new action classes in the Table Builder

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.

Changes to new Admin Panel resources and relation managers

Now to tackle the last two goals - improving the DX for admin panel users who want to customize the actions in their admin panel.

New: actions in the code

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.

New: customizing actions by chaining methods

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))),
];
}

New: Consolidated relation managers

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 ... --attach
php artisan make:filament-relation-manager ... --associate

Customizing the attach action

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.

New: partial simple (modal) resources

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(),
])

New: native support for soft deleted records

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

"Upgrading" your Admin Panel

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.

Resources

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(),
];
}

Simple (modal) resources

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.

Relation managers

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.

Conclusion

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.

avatar

Filament mean AWESOME . Nice and clean 👌 thanks a a lot Dan, Ryan an all help to growth this package. And thanks for blog post ❤

😍 4
avatar

Amazing article. It explains everything clearly. Thank you all guys for your great works.

🥳 3