wotz/filament-table-filter-presets
A Filament plugin for saving, naming, and reapplying table filter combinations as reusable presets, helping admins skip repetitive filtering and switch between common views faster.
Install the package
composer require wotz/filament-table-filter-presets
Then check the package service provider for publishable assets. At minimum you will likely need the migration:
php artisan vendor:publish --tag="filament-table-filter-presets-migrations"
php artisan migrate
Also inspect for a config publish tag (--tag="filament-table-filter-presets-config") before assuming defaults.
Look here first
src/Concerns/HasSavedTableFilters.php — the public API you will actually call and override.database/migrations/* — confirms the storage model (presumably a table_filter_presets table keyed by user, resource/table, and name).config/filament-table-filter-presets.php, if present — user model, table name, and authorization hooks.$tableFilters payload shape.Add the trait to the List page
use Wotz\FilamentTableFilterPresets\Concerns\HasSavedTableFilters;
class ListTickets extends ListRecords
{
use HasSavedTableFilters;
}
Add a filter worth saving The package is designed around Filament table filters, especially query-builder-style filters. A typical first use case:
public function table(Table $table): Table
{
return $table
->columns([...])
->filters([
QueryBuilder::make()
->constraints([
// your resource constraints
]),
]);
}
Then a user builds “Open tickets assigned to me, older than 3 days”, saves it as a named preset, and reloads it later without rebuilding the filter.
Trait-on-the-List-page pattern. The trait belongs on the ListRecords page, not on the Resource or the model. Each List page gets its own preset namespace automatically via the resource/table identity. If you have multiple List pages for the same model (e.g. ListOpenTickets and ListAllTickets), verify whether presets are scoped per page or per resource — this affects whether a preset shows up where you expect.
Save-current-state pattern. The normal workflow is: user configures filters in the table UI, triggers “Save preset”, supplies a name, and the package serializes the current $tableFilters state. When loaded, it restores that array and the table re-filters. In custom actions, treat $this->tableFilters as the source of truth:
// Inspect what will be saved
dump($this->tableFilters);
Programmatic apply pattern. If the trait exposes an apply method, prefer it over manually assigning $this->tableFilters, because it will also refresh the filters form and table state. If you must assign manually, follow with the same refresh/reset call the trait uses internally; otherwise the visible filter form and the actual query can drift apart.
Per-user scoping. Presets are described as per-user. Rely on auth()->user() resolution rather than passing a user ID from the client. For team/tenant setups, override the trait method that resolves the owning user or add a global scope; do not assume the package is tenant-aware out of the box.
Integration with existing table state. Presets compose with pagination, search, sorting, and column toggles, but they should generally only capture filters. Avoid treating a preset as a full “saved view” unless you confirm the package stores more than $tableFilters. If you need saved search/sort too, add that on top rather than expecting it here.
Default filters pattern. If your table has ->default() filters, decide whether a loaded preset should replace or merge with them. The safest UX is replace: the preset is the complete filter state. Test this explicitly, since default filters can otherwise reappear after loading a preset.
Testing pattern. Use Filament/Livewire table tests to exercise save/load:
livewire(ListTickets::class)
->set('tableFilters.status.value', 'open')
->callTableAction(SaveFilterPresetAction::class, data: ['name' => 'My open tickets'])
->assertHasNoTableActionErrors();
Then assert the persisted row and reload the page to confirm the preset is offered. Because adoption is low, write these tests against your installed version rather than trusting examples online.
Extension pattern. Keep custom behavior in your own List page or a subclassed trait method, not by editing vendor files. Good extension points are the preset model (if configurable), the authorization method, and the save/apply action classes, if the package exposes them.
Very low adoption (0 stars, 0 dependents). Budget time to read the source. There is little community troubleshooting, so pin the exact version in composer.json and review changelogs before upgrading. The 2026 release date also means you should verify compatibility with your installed Filament major version rather than assuming it.
Filament version coupling. The package depends on internal Filament table APIs ($tableFilters, filter form state, query builder serialization). Minor Filament upgrades can change the shape of query-builder filter state. After upgrading Filament, re-test saving and loading presets, not just rendering the table.
Stale presets after filter changes. If you rename a filter key, remove a filter class, or change query-builder constraints, previously saved presets may reference filters that no longer exist. Loading them can silently do nothing or throw. Consider a migration/cleanup strategy, or validate preset payloads on load and discard unknown keys.
Migration not published. The most common first-run failure will be a missing presets table. If saving appears to succeed but nothing persists, check that the package migration was published and run, and that the configured connection is correct.
Authorization and IDOR. Presets are likely loaded by ID. Ensure the package (or your override) scopes reads to the current user. Do not expose a generic “load preset by ID” action without an ownership check, especially in multi-tenant panels.
Confusion with Filament’s built-in filter persistence. Filament can persist filters in the session or query string. This package is for named, reusable presets. Using both is fine, but document the difference for users; otherwise “why did my filters come back?” and “why did my preset not load?” become the same bug report.
Filter form desync. If you apply a preset by assigning $this->tableFilters directly and skip the form refresh, the table query changes but the filter UI still shows the old values. Always use the trait’s apply path or mirror its internal refresh call.
Hidden/inactive filters. Filters that are conditionally hidden may still exist in the saved payload. On load, confirm the package re-evaluates visibility and does not apply constraints for filters the user cannot currently see.
Debugging tip. Log or dump the raw preset payload ($preset->filters or equivalent) next to $this->tableFilters at save and load time. Most issues are a shape mismatch between the serialized filter state and what Filament expects on rehydration, and comparing those two values identifies it quickly.
Default to a replace, not merge, UX. Merging loaded presets with existing filters produces surprising results and is hard to explain. If the package merges, override that behavior or reset filters before applying a preset.
How can I help you explore Laravel packages today?