degecko/nova-filters-summary
Adds a compact summary of currently active Laravel Nova filters to resource index screens, so users can quickly see which filters are applied and clear them. A lightweight usability enhancement for Nova dashboards.
Install once, register deliberately. The package is a single Nova card, so installation is a Composer require plus a registration line — there is no migration, no config file you must publish to boot, and no service class to bind.
composer require degecko/nova-filters-summary
Built assets normally ship in dist/, so you rarely need npm install against the package. If you do run into a blank card, that's the path to check first (see Gotchas).
Where to look first, in this order:
README.md — for a package this size the README is the API surface. Look for fluent setters (->width(), visibility helpers) and whether config is publishable.src/ — the Card class. Read component() and withMeta() in full. The meta payload tells you whether the summary is computed server-side (from card.meta) or client-side (read from the query string). This single detail determines whether the card survives a filter change without a page reload, so resolve it before you commit to the card.resources/js/ or dist/js/card.js — the Vue component. Confirm the Nova/Vue major it targets and whether values are rendered with v-text/interpolation rather than v-html.composer.json — the laravel/nova constraint. This is your compatibility contract.First use case — pick a filter-heavy resource, not your landing page. Register scoped, on one resource where users genuinely stack filters:
// app/Nova/Order.php
public function cards(Request $request): array
{
return [
new \Degecko\NovaFiltersSummary\FiltersSummary(),
];
}
An Order index with status, a date range, and a customer relation filter is the canonical case: the dropdown collapses to "Filters (3)" and users lose track of why the list is short. That's three chips the card makes readable. If your indexes only ever use a single filter, the card is vertical-space noise — don't start there, because it will make the package look useless when the problem is your placement.
Prefer per-resource registration over NovaServiceProvider::cards(). The global registration is one line of maintenance, but it also puts the card on every index and dashboard where "active filters" is meaningless. Use a scoped registration, or a shared base resource that filter-heavy resources extend:
abstract class FilterHeavyResource extends Resource
{
public function cards(Request $request): array
{
return [
(new \Degecko\NovaFiltersSummary\FiltersSummary())
->canSee(fn ($request) => ! empty($request->query('filter'))),
];
}
}
Rendering nothing when no filters are active is the difference between a helpful card and permanent chrome.
Your filter name() and option labels are the card's copy. The card can only render what your Filter classes expose. Treat the summary as a consumer of your filter definitions and write them for humans:
public function name(): string
{
return 'Status'; // not 'StatusFilter'
}
public function options(Request $request): array
{
return ['active' => 'Active', 'archived' => 'Archived']; // labels, not raw values
}
A filter returning raw values without labels yields "Status: 2" in the summary — technically correct, operationally useless. If the summary looks bad, the fix is usually in your filter classes, not the card.
Pin uriKey() on any filter that matters here. The summary keys off uriKey(), which Nova derives from the class name unless you override it. Set it explicitly so a class rename doesn't silently change which filter state the summary claims to describe:
public function uriKey(): string
{
return 'order-status';
}
Register on lenses too — that's the highest-value placement. Lenses bake filter state into the URL, so switching lenses silently changes what's shown. That is exactly the confusion this card fixes. Add it to the lens's cards() and test it there explicitly; a card that assumes a plain index request can render nothing or the wrong resource's filters under a lens.
Own the escape hatch. The card is read-only — it tells users what's filtered, not how to undo it. Pair it with a "clear all" affordance in the same cards() array: either a small custom card linking to the bare index URL with the filter[...] params stripped, or a Vue reset button. Without it, users read the summary and still hunt through the dropdown.
Filter changes are partial page visits, so decide your refresh strategy up front. Nova applies filters via its Vue router without a full reload. If the component trusts a server-computed withMeta() payload, the card will be stale until hard navigation while the table updates. If you see that, three options, in order of cost: (a) listen for Nova's filter-change event in the component and refetch; (b) read window.location / the query string client-side instead of trusting meta; (c) scope the card to lenses only, where switching is a full navigation.
// sketch of option (a) inside the card component
Nova.$on('filter-changed', () => this.refresh());
Debugging starts at the query string, not the Vue devtools. Filters arrive as a nested query parameter keyed by uriKey(), so dd($request->query('filter')) on the index route tells you more in five seconds than an hour of component inspection:
// on the index route
[
'order-status' => 'active',
'created_at' => ['start' => '2025-01-01', 'end' => '2025-01-31'],
'tags' => ['vip', 'wholesale'],
]
Filter values are not uniformly strings. Dates serialize as ['start' => ..., 'end' => ...], multi-selects as arrays, booleans as '1'/'0' or 'true'/'false'. Any custom chip formatting must branch on shape or you will ship Date: Array / [object Object] into the admin UI. Exercise every filter type on the resource — including custom and range filters — before rollout, not just the one you were testing with.
Vue major version mismatch fails silently. Nova 4 is Vue 3; Nova 3 is Vue 2. A card compiled against the wrong runtime produces a blank card with no PHP exception and no useful console output beyond a mount failure. Match the laravel/nova constraint in the package's composer.json against your Nova version, and if you fork the JS, keep the nova dev dependency aligned.
Blank card with no JS error = assets aren't served. Check the service provider's Nova asset registration, then run whichever publish command the package documents (php artisan vendor:publish or php artisan nova:publish). Re-publishing after a composer update is the standard fix for "it worked yesterday."
Confirm filter values are escaped before shipping. You are rendering user-influenced strings (filter values can come from the query string) into an authenticated admin UI. Verify the component interpolates rather than using v-html. If it doesn't, that's a one-line change in your fork and worth making regardless.
Low bus factor — plan to vendor. 20 stars, zero dependents, single maintainer, last release January 2025. The mitigation is cheap because the surface is tiny: read the ~200 lines once, and be ready to copy it into nova-components/ rather than waiting on an upstream fix. Vendoring also unlocks the customizations you'll want within a sprint anyway — per-filter chip colours, a clear-all button, collapse-after-three.
Test at the payload level, not the HTML level. HTTP tests never exercise the Vue side, so assert the card's component name appears in the index response's card payload rather than asserting rendered markup. That catches accidental removal during a refactor, which is the realistic failure mode for a card nobody touches for six months.
Budget the spike as validation, not implementation. The integration is two lines. The real work is confirming, in staging: the card renders on a representative resource and a lens; every filter shape on that resource renders sensibly; and whether the summary stays in sync after a filter change. If the staleness question comes back "no" and nobody wants to fork the component, walk away — a summary that disagrees with the table is worse than no summary at all.
How can I help you explore Laravel packages today?