amjadiqbal / filament-change-requests
Hold record edits in Filament as change requests: field-by-field diff, approve, reject and roll back. Built on cjmellor/approval.
Package info
github.com/amjadiqbal/filament-change-requests
pkg:composer/amjadiqbal/filament-change-requests
Requires
- php: ^8.3
- cjmellor/approval: ^2.1
- filament/filament: ^4.0|^5.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- pestphp/pest-plugin-livewire: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Change Requests for Filament
Hold edits to your Filament records as change requests. An approver sees the old and new value of each field, then approves, rejects or rolls back the change. It works with Filament 4 and 5 and is built on cjmellor/approval.
Docs and demo: amjad.com.pk/filament/plugins/filament-change-requests
What it does
- Edits wait for approval. When someone saves a record of a model that needs approval, its own attributes don't
change yet.
- They see "Changes submitted for approval" instead of "Saved".
- The form goes back to the saved values.
- If you add
ViewChangeRequestsActionto the page header, a "Pending changes (n)" button opens the change requests list filtered to that record.
- New records wait too. A held create says "Submitted for approval" and goes back to the list. No broken redirect to a record that doesn't exist yet.
- A field-by-field diff. Each changed field shows its value before and after.
- By default
password,remember_token,api_token,two_factor_secretandtwo_factor_recovery_codesare masked. - Any other secret column is shown unless you add it with
maskedAttributes().
- By default
- Approve, reject, roll back.
- You can approve or reject from the list, one at a time or in bulk, from the request page, or from a dashboard widget.
- On the request page, an approved change can be rolled back once, which writes its "Before" values back.
- Four-eyes by default. Nobody can approve, reject or roll back a change request they made themselves.
- On your existing tables: a pending-changes count column and a "Has pending changes" filter.
- Light and dark mode. English strings, all translatable.
Requirements
- PHP 8.3+
- Laravel 12.4+ or 13
- Filament 4 or 5
- cjmellor/approval 2.1+, installed for you
Installation
composer require amjadiqbal/filament-change-requests
cjmellor/approval is installed with it. Run its migrations, then publish the plugin's stylesheet:
php artisan migrate php artisan filament:assets
Register the plugin on your panel:
use AmjadIqbal\FilamentChangeRequests\ChangeRequestsPlugin; public function panel(Panel $panel): Panel { return $panel // ... ->plugin(ChangeRequestsPlugin::make()); }
Usage
1. Make a model need approval
Add cjmellor/approval's trait to the model:
use Cjmellor\Approval\Concerns\MustBeApproved; class Product extends Model { use MustBeApproved; // Optional: only these attributes need approval; others save straight away. protected array $approvalAttributes = ['price', 'stock']; }
2. Use the page traits on the model's resource
Without these, Filament would say "Saved" or "Created" for a change that is only waiting for approval.
use AmjadIqbal\FilamentChangeRequests\Actions\ViewChangeRequestsAction; use AmjadIqbal\FilamentChangeRequests\Pages\Concerns\SubmitsChangeRequests; class EditProduct extends EditRecord { use SubmitsChangeRequests; protected function getHeaderActions(): array { return [ ViewChangeRequestsAction::make(), // "Pending changes (2)", only when there are any ]; } }
use AmjadIqbal\FilamentChangeRequests\Pages\Concerns\SubmitsCreationRequests; class CreateProduct extends CreateRecord { use SubmitsCreationRequests; }
3. Optional: show pending changes on the resource table
use AmjadIqbal\FilamentChangeRequests\Tables\Columns\PendingChangesColumn; use AmjadIqbal\FilamentChangeRequests\Tables\Filters\PendingChangesFilter; public static function table(Table $table): Table { return $table ->columns([ // ... PendingChangesColumn::make(), ]) ->filters([ PendingChangesFilter::make(), ]); }
4. Optional: the dashboard widget
use AmjadIqbal\FilamentChangeRequests\Widgets\PendingChangeRequestsWidget; $panel->widgets([PendingChangeRequestsWidget::class]);
Who can approve
The plugin checks these in order:
- Self-review is blocked. Nobody can approve, reject or roll back their own request. This only applies to requests
with a recorded requester, that is, ones made by a signed-in user. Turn it off with
->preventSelfReview(false). - Your callback, if you set one:
ChangeRequestsPlugin::make() ->authorizeUsing(fn (Approval $approval, User $user): bool => $user->is_admin)
- A
reviewChangeRequestgate, if your app defines one:Gate::define('reviewChangeRequest', fn (User $user, Approval $approval): bool => $user->hasRole('editor-in-chief'));
- The model's policy.
- A user who may
updatethe record can review edits to it. - A user who may
createone can review held creations. - An edit whose record has since been deleted can't be reviewed.
- A user who may
- Anyone signed in to the panel (except the requester), if the model has no policy.
A callback or the gate decides on its own: once either is set, the model's policy isn't checked.
These rules cover approving, rejecting and rolling back. They don't limit who can open the Change requests list or read
the diffs; for that, register a policy for Cjmellor\Approval\Models\Approval (its viewAny and view) or use
your own resource with ->resource(MyChangeRequestResource::class).
Configuration
ChangeRequestsPlugin::make() ->navigationGroup('Catalog') ->navigationSort(3) ->navigationBadge(false) // hide the pending count ->attributeLabels(['sku' => 'SKU']) // labels in the diff ->maskedAttributes(['iban']) // never show these values (password, tokens… are masked already) ->preventSelfReview() // default: on ->authorizeUsing(fn ($approval, $user) => true) ->resource(false); // or ->resource(MyChangeRequestResource::class)
The actions are plain Filament actions, so you can use them on your own pages or tables:
ApproveChangeRequestAction, RejectChangeRequestAction, RollBackChangeRequestAction,
ApproveChangeRequestsBulkAction and RejectChangeRequestsBulkAction.
How it works
cjmellor/approvalstores each held change in anapprovalstable, with the new and the original values.- Approving writes the new values to the record and records who approved it.
- For a held creation, the plugin links the change request to the record that approving it creates. cjmellor/approval alone leaves that link empty.
- Rolling back writes the stored "Before" values back. That also replaces any later edit to the same fields. Roll back is only offered for an approved request whose record still exists, and only once. The request keeps showing the change as it was asked for.
- Custom states from
config('approval.states')appear in the status filter and badge.
Limits
- Only the model's own attributes are held. Filament saves relationship fields (for example a
SelectorRepeaterwith->relationship()) itself, so on an edit they save straight away. - On a held create, relationship fields aren't saved at all, because the record doesn't exist until it's approved.
- Deletes aren't held. cjmellor/approval only holds creates and updates.
- Diff labels are the attribute names in headline case (
is_activebecomes "Is Active"), not your form labels. Set your own withattributeLabels(). - Values: arrays and JSON show pretty-printed; booleans show as Yes or No.
Testing
composer test # Pest composer analyse # PHPStan level 5 composer lint # Pint scripts/ci-local.sh # PHP 8.3–8.5 × Laravel 12/13 × Filament 4/5
Changelog
See CHANGELOG.md.
Support
- 🐛 Bug or feature request: open an issue
- 🔒 Security issue: please report it privately through GitHub's security tab
Need this customised, or something built?
I'm available for custom development, package integration, and technical consulting.
More packages
This is part of a family of open-source packages. See all of them.
| filament-url-image-uploader | Upload images to Filament from a URL |
| refresher | Restore a cached test database instead of migrate:fresh |
| kiln | Deploy-time OPcache control for Laravel |
Credits
- Amjad Iqbal
- Approval engine: cjmellor/approval by Chris Mellor (MIT)
License
The MIT License (MIT). See LICENSE.md.

