Saved Filter Presets
Saved filter presets let each team member save, name, pin, and reuse filter configurations. Presets are personal (scoped to member_id + organization_id) and stored as opaque JSON, so the same system works for any module that has filterable views.
Database Schema
Section titled “Database Schema”The saved_filter table stores one row per preset:
| Column | Type | Notes |
|---|---|---|
id | UUID (PK) | Auto-generated |
organization_id | UUID (FK) | → organization.id, CASCADE delete |
member_id | UUID (FK) | → member.id, CASCADE delete |
module | VARCHAR | "contacts" or "plans" |
name | VARCHAR | User-chosen preset name |
filter_json | JSONB | Opaque filter state — deserialized per module |
is_pinned | BOOLEAN | Default false |
created_at | TIMESTAMP | Auto-set |
updated_at | TIMESTAMP | Auto-set |
Indexes: composite (member_id, module) for fast per-user lookups; organization_id for org-scoped queries.
SavedFilterModule
Section titled “SavedFilterModule”#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]#[serde(rename_all = "snake_case")]pub enum SavedFilterModule { Contacts, Plans,}Serializes to "contacts" / "plans". Implements Display and FromStr for route parsing.
SavedFilter
Section titled “SavedFilter”The API response type:
pub struct SavedFilter { pub id: Uuid, pub module: SavedFilterModule, pub name: String, pub filter_json: serde_json::Value, pub is_pinned: bool, pub created_at: String, pub updated_at: String,}SavedFilterData
Section titled “SavedFilterData”The create/update request payload:
pub struct SavedFilterData { pub module: SavedFilterModule, pub name: String, pub filter_json: serde_json::Value,}API Endpoints
Section titled “API Endpoints”All endpoints require an authenticated session. Presets are scoped to the current member and organization — no resource-level permissions needed.
| Method | Route | Description | Request Body | Response |
|---|---|---|---|---|
GET | /api/saved-filters/by-module/{module} | List presets | — | Vec<SavedFilter> |
POST | /api/saved-filters | Create preset | SavedFilterData | SavedFilter |
PUT | /api/saved-filters/{id} | Update preset | SavedFilterData | SavedFilter |
POST | /api/saved-filters/{id}/toggle-pin | Toggle pinned state | — | SavedFilter |
DELETE | /api/saved-filters/{id} | Delete preset | — | () |
List order: pinned presets first (is_pinned DESC), then alphabetical by name.
Example: Create a preset
Section titled “Example: Create a preset”POST /api/saved-filters{ "module": "contacts", "name": "VIP Clients", "filter_json": { "tag_ids": ["a1b2c3d4-..."], "status_filter": "active", "sort": "LastContactedAt" }}Example: List presets for contacts
Section titled “Example: List presets for contacts”GET /api/saved-filters/by-module/contactsReturns all presets for the current member in the contacts module, pinned first.
Filter JSON Storage
Section titled “Filter JSON Storage”filter_json is stored as opaque JSONB. Each module deserializes it into its own filter type:
- Contacts →
ContactFilter(with#[serde(default)]on all fields) - Plans →
PlanListFilters(with#[serde(default)]on all fields)
UI Components
Section titled “UI Components”SavedPresetsSection
Section titled “SavedPresetsSection”src/mods/saved_filter/components/saved_presets_section_component.rsA reusable inline section embedded inside filter panels. It renders:
- Normal mode — clickable preset chips, ”+ Save” button, “Manage” toggle
- Manage mode — editable list with rename, pin/unpin, and delete controls
- Empty state — “No saved presets yet” with a save link
- Save dialog — modal name input when saving the current filter state
SaveFilterDialog
Section titled “SaveFilterDialog”src/mods/saved_filter/components/save_filter_dialog_component.rsA modal dialog for naming a new preset. Takes the current filter_json and module, calls POST /api/saved-filters, and refreshes the preset list on success.
Integration Points
Section titled “Integration Points”The SavedPresetsSection is embedded in three views:
| View | Component | Behavior |
|---|---|---|
| Contact list | ContactFilterPopover | Presets section at top of filter popover |
| Messaging sidebar | ContactFilterPopover (compact) | Same popover, compact layout |
| Plan list | PlanListView | Inline above state filter pills |
Each integration passes the current filter state as JSON and an on_preset_apply callback. Selecting a preset deserializes the JSON and replaces the active filter state.
Auto-Apply Behavior
Section titled “Auto-Apply Behavior”Filters auto-apply on every interaction — there is no “Apply” button. A use_effect watches all filter signals and commits changes immediately. When you select a preset, the filter state updates and the view refreshes in one step.