# Filament Filter UX Standard

> Canonical rules for all Filament table filters in TenantPilot.
> Every new or modified filtered surface must follow this standard.
> Deviations must be documented in the spec or PR with rationale.

**Last reviewed**: 2026-03-09

---

## Governing Principle

**Filament-native first.**
Filter standardization uses native Filament filter APIs wherever reasonably possible.
A thin optional helper for repeated mechanical patterns (e.g., TrashedFilter preset, date range) is allowed but not required.

---

## Tiered Persistence Model

### Tier 1 — Critical resources (mandatory persistence)

- PolicyResource
- FindingResource
- OperationRunResource
- TenantResource
- InventoryItemResource

### Tier 2 — Important resources (mandatory persistence)

- BackupScheduleResource
- BackupSetResource
- RestoreRunResource
- PolicyVersionResource
- ProviderConnectionResource
- AlertDeliveryResource
- EntraGroupResource

### Tier 3 — Standard resources (persistence optional)

- ReviewPackResource
- AlertRuleResource
- AlertDestinationResource
- BaselineProfileResource
- BaselineSnapshotResource
- WorkspaceResource

### Persistence trio

All Tier 1–2 resource lists with filters must use:

```php
->persistFiltersInSession()
->persistSearchInSession()
->persistSortInSession()
```

### Documented exceptions

- Dashboard widgets: no persistence (glance surfaces)
- Picker tables: no persistence (workflow-local)
- Relation managers: persistence optional, per judgment

---

## Soft-Delete Filter Standard

Every soft-deletable resource table must include:

```php
TrashedFilter::make()
    ->label('Archived')
    ->placeholder('Active')
    ->trueLabel('All')
    ->falseLabel('Archived')
```

Labels must stay consistent across all resources. Do not vary wording.

---

## Status / Enum Filter Standard

### Rule

Status and outcome filter options must derive from model enums or an equivalent central catalog.
Do not use hardcoded string arrays.

### Pattern

```php
SelectFilter::make('status')
    ->options(MyStatusEnum::class)
```

Or, if using a catalog:

```php
SelectFilter::make('status')
    ->options(StatusCatalog::filterOptions())
```

### Forbidden

```php
// Do not do this
SelectFilter::make('status')
    ->options([
        'active' => 'Active',
        'inactive' => 'Inactive',
    ])
```

Temporary transitional arrays are allowed only when no enum exists yet and introducing one would materially expand the scope of the current spec.

---

## Date Range Filter Standard

Time-based Tier 1–2 surfaces should use a native date range filter:

```php
Filter::make('date_range')
    ->form([
        DatePicker::make('from'),
        DatePicker::make('until'),
    ])
    ->query(function (Builder $query, array $data): Builder {
        return $query
            ->when($data['from'], fn (Builder $q, $date) => $q->whereDate('created_at', '>=', $date))
            ->when($data['until'], fn (Builder $q, $date) => $q->whereDate('created_at', '<=', $date));
    })
    ->indicateUsing(function (array $data): array {
        $indicators = [];
        if ($data['from'] ?? null) {
            $indicators[] = Indicator::make('From ' . Carbon::parse($data['from'])->toFormattedDateString())
                ->removeField('from');
        }
        if ($data['until'] ?? null) {
            $indicators[] = Indicator::make('Until ' . Carbon::parse($data['until'])->toFormattedDateString())
                ->removeField('until');
        }
        return $indicators;
    })
```

### Required date range surfaces

- FindingResource (`created_at`)
- AlertDeliveryResource (`created_at`)
- RestoreRunResource (`created_at`)
- PolicyVersionResource (`captured_at`)
- OperationRunResource (already has date range)

### Indicator requirement

All date range filters must implement `indicateUsing()` so active filters are visible in the filter summary.

---

## Filter Type Decision Guide

| Scenario | Recommended filter | Notes |
|---|---|---|
| Enum/status with 3–8 options | `SelectFilter` | Most common and clearest |
| Boolean yes/no/all | `TernaryFilter` | e.g., "Has errors", "Is current" |
| Soft-delete active/archived | `TrashedFilter` | Standard labels, see above |
| Date range | `Filter::make()` + DatePicker form | With `indicateUsing()` |
| Multi-select (e.g., platforms) | `SelectFilter` with `->multiple()` | When users need to select several |
| Cross-cutting dimension (status as tabs) | Filament Tabs | Only when 2–4 high-frequency states dominate the workflow |
| Complex custom logic | `Filter::make()` + custom form | Last resort; keep query simple |

### When NOT to filter

- Very small tables (< 20 typical rows)
- Read-only evidence/log tables where search + sort covers the use case
- Picker/selection tables where narrowing is done via search only

---

## Filter Count and Ordering

### Count guideline

- Tier 1–2 resources: typically 2–5 filters
- Tier 3 resources: 0–3 filters
- Relation managers: 0–2 filters
- Widgets: 0 filters (use summary/glance, not filtering)

### Ordering rule

Filters should appear in priority order:

1. Status / primary state dimension
2. Type / category
3. Severity / risk
4. Date range
5. Soft-delete (archived)
6. Secondary dimensions

---

## Smart Defaults

### When defaults are allowed

- Active-only / open-only: when the typical workflow focuses on actionable records
- Recent date windows: only on monitoring/time-series surfaces
- Soft-delete active-only: standard via TrashedFilter placeholder behavior

### Guardrail

Defaults must be:

- obvious (user can see what's filtered)
- one-click reversible
- not hiding data the user expects to see

---

## Labels and Terminology

### Requirements

- Filter labels must be clear, enterprise-appropriate, and consistent with domain vocabulary.
- Use sentence case for filter labels.
- Use the established term from the enum/status catalog.

### Consistency targets

| Concept | Standard label |
|---|---|
| Soft-delete visibility | "Archived" (not "Trashed", not "Deleted") |
| Record lifecycle | "Status" |
| Operation result | "Outcome" |
| Platform dimension | "Platform" |
| Policy category | "Policy Type" |
| Time range | "Date Range" or "Period" |

---

## Query and Performance Guardrails

- Relation-heavy or computed filters must be reviewed for query cost.
- Custom query filters with subqueries should be kept simple.
- Filters must not alter tenancy/workspace scoping behavior.
- New filters must compose correctly with existing filters.

---

## RBAC / Tenancy Safety

- Filters must not expose sensitive values to unauthorized roles.
- Cross-tenant / cross-workspace filters must preserve proper entitlement checks.
- Filter additions must not change authorization behavior.

---

## What Remains Table-Local

- Exact filter choices per domain
- Custom boolean quick filters
- Advanced governance/compliance filters
- System page-specific filter design
- Picker-specific narrowing rules

---

## Shared Helper (Optional)

A thin `App\Support\Filament\FilterPresets` helper may be introduced for clearly repeated patterns only:

- TrashedFilter with standard labels
- Date range pattern
- Policy type / platform multi-select

This is DRY convenience, not a framework. Keep it thin and optional.
