jeffersongoncalves/filament-saved-filter
filament-saved-filter by Jefferson Goncalves lets Filament users save and reuse table filter combinations, making admin panels and dashboards faster to navigate. Persist frequently used filters and restore them with a click.
Install and migrate first.
composer require <vendor>/laravel-saved-views
php artisan vendor:publish --tag="saved-views-config" # config
php artisan vendor:publish --tag="saved-filter-migrations" # migrations, incl. the new columns
php artisan migrate
If you are upgrading to 1.6.0: bump jeffersongoncalves/laravel-saved-filter to ^1.10 before upgrading the parent package, then publish the saved-filter-migrations tag again and run php artisan migrate. This release adds the report timezone column and view/report IDs to the report context so every schedule links to its view. Skipping the migration produces SQL errors only when saving a view or scheduling a report — easy to miss if nobody clicks "Save view" or opens the schedule form. Optionally install laravel-report-url if you want email reports too large to attach delivered as temporary download links.
Where to look first (in order):
config/saved-views.php — default visibility, table/column names, whether shortcuts are on, shortcut overrides.->keyBindings() and ->tabShortcuts() live).?view= link all surface there.table() method. ManageRelatedRecords pages and custom pages using InteractsWithTable only get saved views if you call configureSavedFilters() inside table().First use case: make a table remember everything a user tuned. The cheapest win is enabling per-page and grouping persistence on an existing filtered table — a user sets 50 rows per page grouped by team, saves the view, and gets it back on next login without any custom code on your side.
// illustrative — confirm exact signatures against your version's docs
return SavedViews::make('orders')
->presets(fn () => [
'mine' => OrderFilter::mine(),
'overdue' => OrderFilter::overdue(),
])
->keyBindings() // ship the default Ctrl/Cmd+Shift+S / +M
->tabShortcuts(); // opt-in Alt+1…9, Alt+] / Alt+[
Choose tabs vs. dropdown by count, not by importance. Below ~5 views, tabs are faster to hit and make the active view obvious. Beyond that the tab bar wraps or scrolls and the dropdown becomes the real navigation. Both surfaces render the same visibility label, so keep the labels meaningful: Preset for code-defined, Shared for team-wide saves, and the colleague's name for personal shares. Pages without tabs now list their views in the Views menu.
Treat presets as code, saved views as data. Presets belong in a filter/query class under app/Filters (or your domain namespace) so they are versioned and testable. User-saved views are rows — never let a preset depend on a saved row's ID, or deleting a view breaks the preset. The same applies to report schedules: link them to the view row, not to a hard-coded preset name.
Persist grouping and per-page deliberately. A stored per-page value is user input that later reaches paginate(); validate it against your allowed page-size list on read, not just on write. Grouping keys should be canonical column/relation identifiers, not display labels, so renaming a column header doesn't strand old views.
Wire shortcuts once, at the component level, not per page.
->keyBindings([
'save' => 'mod+shift+s',
'manage' => 'mod+shift+m',
])
// or disable entirely
->keyBindings(false);
Tab shortcuts are opt-in for a reason: ->tabShortcuts() is right for a dedicated reporting screen where the user lives in the table, and wrong for a CRUD index that sits inside a larger app with its own keyboard model.
Compose with your existing query-string/state layer — the URL is now part of the contract. The active view is in the URL as ?view=4, and links open it in dropdown mode too. Saved views should be the source of initial table state, not a competing store: hydrate the table from the view parameter on mount, let subsequent user changes live in normal component state until they explicitly save, and decide whether changing a filter clears view or leaves it as a "based on" marker. Filters present in the same link still win, so ?view=4&status=overdue means "view 4, but status forced to overdue".
Any table page, not just resource lists. ManageRelatedRecords pages (tabs included) and custom pages with InteractsWithTable get saved views once you call configureSavedFilters() in table(). Outside list pages views are keyed by the page class, so they deliberately do not mix with the resource list's views. Email reports remain a list-page feature.
Email reports are now a first-class workflow. The Views menu has a slide-over with the user's reports across every table of the tenant, so they can turn them on/off or remove them in one place. The schedule form carries a timezone, defaulting to the user's timezone attribute and then to the app timezone, and it shows what is currently scheduled for the picked view. The report email's Open the view button uses the same ?view= link, so a report and an in-app share point at the same thing.
Sharing is a UI concept backed by a policy. Decide who can share (creator only? anyone with update on the view?) and enforce it server-side — both when a view is shared and when a ?view= link is resolved. The visibility badge and an unguessable-looking URL are hints, not Authorization checks.
Handle imports as a user-facing choice. On import, the same-named view can be overwritten (the previous behavior), skipped, or imported as "Name (2)". Wire that choice into your import UI instead of assuming overwrite, and don't treat view names as identities in your tests.
laravel-saved-filter ^1.10 plus publishing saved-filter-migrations and running php artisan migrate are hard prerequisites for the timezone column and view/report linkage. Test the save and schedule paths explicitly after deploying, including on a database that already has saved views and reports.Ctrl/Cmd+Shift+S is "Save page as" in Chrome and Firefox. The package should preventDefault(), but verify it in the browsers you support — and confirm you aren't shadowing a shortcut your users rely on. If in doubt, remap via keyBindings().Alt+1…9 collides with native tab switching on Windows/Linux Firefox. Alt+]/Alt+[ also clash with editor and some browser/OS bindings. That is why tabShortcuts() is opt-in — enable it on focused, full-screen table pages rather than globally.<input> is covered, but contenteditable regions, rich-text editors, and third-party comboboxes often are not. Fire a test: focus each editor on the page and press Alt+3.?view= links are user- and tenant-scoped IDs. A link shared after an import, deletion, or across tenants can resolve to a missing or foreign view. Resolve defensively (fall back to the default view, log a warning) instead of 500-ing, and re-check authorization on every resolve — URL enumeration must not reveal other users' views or filter values.Shared) rather than throwing in the view layer.user_id = 42 or a customer email is visible to everyone it's shared with — and now also to anyone with the ?view= link. Audit what your filters actually serialize before enabling sharing broadly.timezone attribute, then the app timezone; a null attribute is the common case, not the exception. Test DST boundaries and what "next run" shows in the schedule form, and remember the timezone is stored per report, not per view.laravel-report-url. Without it, reports too large to attach have no download link to email. If you rely on report emails for big exports, add the dependency before rollout.->getAttributes() on the view row); is laravel-saved-filter actually on ^1.10 (check composer.lock, not composer.json); did the page call configureSavedFilters() if it isn't a list page; and is the table rehydrating per-page from the view or overriding it with a default.laravel-report-url installed if the report is large; and the "Open the view" link actually resolving to a view the recipient can see.How can I help you explore Laravel packages today?