Courses

Creating CRM with Filament 3: Step-By-Step

SoftDeletes: Archive and Restore Customers

Summary of this lesson:
- Adding "Archived" tab with soft deleted records
- Implementing restore functionality
- Customizing row actions for archived records
- Adding delete/restore buttons with proper visibility rules

Sometimes, your Customer might get deleted for various reasons, but you might need to recover them months later. In Laravel, it's about SoftDeletes, but in the Filament version, we will show it as an Archived tab with a Restore button:

In this lesson, we will do the following:

  • Add the Archived tab to the Customers table
  • Add Delete button to the table
  • Add the Restore button to the Archived tab
  • Disable row click on the Archived tab

Adding Delete Button

The first thing to do is add the missing Delete button to our form:

app/Filament/Resources/CustomerResource.php

// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
 
// ...
])
->bulkActions([
// ...
]);
}
 
// ...

That's it. Now we have a delete button in our table:


Adding Archived Tab

Now that we can delete our customers, we must see them somewhere. Let's add an Archived tab to our table:

app/Filament/Resources/CustomerResource/Pages/ListCustomers.php

// ...
 
public function getTabs(): array
{
$tabs = [];
 
$tabs['all'] = Tab::make('All Customers')
->badge(Customer::count());
 
$pipelineStages = PipelineStage::orderBy('position')->withCount('customers')->get();
 
foreach ($pipelineStages as $pipelineStage) {
$tabs[str($pipelineStage->name)->slug()->toString()] = Tab::make($pipelineStage->name)
->badge($pipelineStage->customers_count)
->modifyQueryUsing(function ($query) use ($pipelineStage) {
return $query->where('pipeline_stage_id', $pipelineStage->id);
});
}
 
$tabs['archived'] = Tab::make('Archived')
->badge(Customer::onlyTrashed()->count())
->modifyQueryUsing(function ($query) {
return $query->onlyTrashed();
});
 
return $tabs;
}
 
// ...

This will add an Archived tab to our table:

We currently have 2 Customers here, but as you can see - there is an Edit button and a Move to Stage button. We don't want that, so let's hide them on this tab:

app/Filament/Resources/CustomerResource.php

// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make()
->hidden(fn($record) => $record->trashed()),
Tables\Actions\DeleteAction::make(),
Tables\Actions\Action::make('Move to Stage')
->hidden(fn($record) => $record->trashed())
->icon('heroicon-m-pencil-square')
->form([
// ...
])
->action(function (Customer $customer, array $data): void {
// ...
}),
])
->bulkActions([
// ...
]);
}
 
// ...

We have now hidden the Edit and Move to Stage buttons by adding a condition to check if they are trashed or not. This gives us the following result:

Disabling Row Actions on Archived Tab

If you visit the Archived tab and click on a row, you will get an error like this:

This happens because we have deleted the record previously, but now we are trying to edit it. To prevent this, we can add the following code to our table:

app/Filament/Resources/CustomerResource.php

// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
//
])
->actions([
// ...
])
->recordUrl(function ($record) {
// If the record is trashed, return null
if ($record->trashed()) {
// Null will disable the row click
return null;
}
 
// Otherwise, return the edit page URL
return Pages\EditCustomer::getUrl([$record->id]);
})
->bulkActions([
// ...
]);
}
 
// ...

Clicking the row will not do anything in the Archived tab but will point to the Edit page in all other tabs.


Adding Restore Button

The last thing to do is add a Restore button:

app/Filament/Resources/CustomerResource.php

// ...
 
public static function table(Table $table): Table
{
return $table
->columns([
// ...
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make()
->hidden(fn($record) => $record->trashed()),
Tables\Actions\DeleteAction::make(),
Tables\Actions\RestoreAction::make(),
Tables\Actions\Action::make('Move to Stage')
->hidden(fn($record) => $record->trashed())
->icon('heroicon-m-pencil-square')
->form([
// ...
])
->action(function (Customer $customer, array $data): void {
// ...
}),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make()
->hidden(function (Pages\ListCustomers $livewire) {
return $livewire->activeTab == 'archived';
}),
Tables\Actions\RestoreBulkAction::make()
->hidden(function (Pages\ListCustomers $livewire) {
return $livewire->activeTab != 'archived';
}),
]);
}
 
// ...

This will add the button to the end of the table:

And if you are wondering why we did not add ->hidden(...) to the button - Filament handles that for us. If the record is not trashed - the button will not be shown:


That's it. In the next lesson, we will be building a Customer View page.

Previous: Customers by Stage: Tabs with Numbers
avatar

Very helpfull, thank you! Just want to add something: if you also have related records that you want to delete/restore, you can place the following in an observer (or in the model boot):

    public function deleted(Device $Device): void
    {
        // delete also related models
        $Device->malfunctions()->delete();
        $Device->maintenances()->delete();
    }
    public function restoring(Device $Device): void
    {
        $Device->malfunctions()->withTrashed()
            ->where('deleted_at', '>=', $Device->deleted_at)->restore();
        $Device->maintenances()->withTrashed()
            ->where('deleted_at', '>=', $Device->deleted_at)->restore();
    }
		```
		
👍 3
avatar
You can use Markdown
avatar
You can use Markdown