Skip to content

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.

The saved_filter table stores one row per preset:

ColumnTypeNotes
idUUID (PK)Auto-generated
organization_idUUID (FK)organization.id, CASCADE delete
member_idUUID (FK)member.id, CASCADE delete
moduleVARCHAR"contacts" or "plans"
nameVARCHARUser-chosen preset name
filter_jsonJSONBOpaque filter state — deserialized per module
is_pinnedBOOLEANDefault false
created_atTIMESTAMPAuto-set
updated_atTIMESTAMPAuto-set

Indexes: composite (member_id, module) for fast per-user lookups; organization_id for org-scoped queries.

src/mods/saved_filter/types/saved_filter_module_type.rs
#[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.

The API response type:

src/mods/saved_filter/types/saved_filter_type.rs
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,
}

The create/update request payload:

src/mods/saved_filter/types/saved_filter_data_type.rs
pub struct SavedFilterData {
pub module: SavedFilterModule,
pub name: String,
pub filter_json: serde_json::Value,
}

All endpoints require an authenticated session. Presets are scoped to the current member and organization — no resource-level permissions needed.

MethodRouteDescriptionRequest BodyResponse
GET/api/saved-filters/by-module/{module}List presetsVec<SavedFilter>
POST/api/saved-filtersCreate presetSavedFilterDataSavedFilter
PUT/api/saved-filters/{id}Update presetSavedFilterDataSavedFilter
POST/api/saved-filters/{id}/toggle-pinToggle pinned stateSavedFilter
DELETE/api/saved-filters/{id}Delete preset()

List order: pinned presets first (is_pinned DESC), then alphabetical by name.

POST /api/saved-filters
{
"module": "contacts",
"name": "VIP Clients",
"filter_json": {
"tag_ids": ["a1b2c3d4-..."],
"status_filter": "active",
"sort": "LastContactedAt"
}
}
GET /api/saved-filters/by-module/contacts

Returns all presets for the current member in the contacts module, pinned first.

filter_json is stored as opaque JSONB. Each module deserializes it into its own filter type:

  • ContactsContactFilter (with #[serde(default)] on all fields)
  • PlansPlanListFilters (with #[serde(default)] on all fields)
src/mods/saved_filter/components/saved_presets_section_component.rs

A 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
src/mods/saved_filter/components/save_filter_dialog_component.rs

A 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.

The SavedPresetsSection is embedded in three views:

ViewComponentBehavior
Contact listContactFilterPopoverPresets section at top of filter popover
Messaging sidebarContactFilterPopover (compact)Same popover, compact layout
Plan listPlanListViewInline 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.

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.