Unified Contact Filters
The ContactFilter type provides a single filter model used by both the contacts list page and the messaging sidebar. Search, sort, tags, status, custom fields, and last-contacted range all flow through one type, one service, and one URL-serialization layer.
ContactFilter Type
Section titled “ContactFilter Type”pub struct ContactFilter { pub search: String, pub sort: ContactListSort, pub tag_ids: Vec<Uuid>, pub status_filter: Option<ContactStatus>, pub member_filter: Option<AssignmentMemberFilter>, pub custom_field_filters: Vec<CustomFieldFilter>, pub custom_field_logic: CustomFieldLogic, pub last_contacted_range: Option<TimeRange>, pub page: u64, pub page_size: u64,}ContactFilter replaces the old ContactListQuery. It defaults to page_size: 25, sort: LastContactedAt, and custom_field_logic: And.
Sort Options
Section titled “Sort Options”| Variant | Behavior |
|---|---|
LastContactedAt | Most recently contacted first (default) |
RecentlyAdded | Newest contacts first |
NameAsc | Alphabetical A–Z |
Custom Field Filters
Section titled “Custom Field Filters”pub struct CustomFieldFilter { pub field_def_id: Uuid, pub field_type: ContactFieldType, pub values: Vec<String>,}Each filter targets one custom field definition. How values is interpreted depends on field_type:
| Field Type | Values |
|---|---|
Select | Multiple option strings (OR within the field) |
Boolean | Single "true" or "false" |
Text, Textarea, Email, Url | Single substring for case-insensitive match |
Number | [min, max] — either can be empty for unbounded |
Date | Single JSON-serialized TimeRange |
CustomFieldLogic controls how multiple custom field filters combine: And (contact matches all) or Or (contact matches any).
Active Filter Count
Section titled “Active Filter Count”Call filter.active_filter_count() to get the number of active filter categories. The UI uses this to display a badge count on the filter icon button.
Server-Side Filter Service
Section titled “Server-Side Filter Service”pub fn apply_contact_filters( condition: sea_orm::Condition, filter: &ContactFilter, org_id: Uuid,) -> sea_orm::ConditionPass in an existing SeaORM Condition and the function appends clauses for search, tags, status, custom fields, and last-contacted range. It does not handle permission scoping or member_filter — those stay in the API handler.
Key implementation details:
- Search matches against
first_name,last_name, phone numbers, and emails via subqueries - Tags use an OR subquery — the contact must have at least one of the selected tags
- Custom fields use per-field subqueries with defense-in-depth org scoping on
field_def_id - Number casts the stored string to
DOUBLE PRECISIONviaExpr::cust_with_valuesfor min/max range comparison - Date deserializes
values[0]as aTimeRange, converts to a(start_date, end_date)pair viarange.to_date_range(), and compares the storedYYYY-MM-DDstring withgte/lte - Email/Url share the same case-insensitive
ILIKEsubstring logic as Text and Textarea - No catch-all arms — the compiler enforces exhaustiveness across all 8
ContactFieldTypevariants
URL Persistence
Section titled “URL Persistence”ContactFilterQuery serializes filters into URL query parameters so they survive page reloads.
pub struct ContactFilterQuery { pub search: String, pub tab: String, // messaging tab (empty = "all") pub sort: String, // "recently_added" | "name_asc" | "" (default) pub tags: String, // comma-separated UUIDs pub status: String, pub cf: String, // JSON-encoded Vec<CustomFieldFilter> pub logic: String, // "or" or "" (default = and) pub member: String, // "" | "unassigned" | member UUID pub lcr: String, // JSON-encoded TimeRange}The Display implementation replaces & with ! and = with ~ to prevent the Dioxus router from splitting query values during URL decoding. FromStr reverses this.
Convert between the two types:
// ContactFilter → URL querylet query = ContactFilterQuery::from_filter(&filter);let url_string = query.to_string();
// URL query → ContactFilterlet query: ContactFilterQuery = url_string.parse().unwrap();let filter = query.to_filter(25); // page_sizeContactFilterBar Components
Section titled “ContactFilterBar Components”The filter bar lives in src/mods/contact/components/filter_bar/ as a set of focused sub-components:
| Component | File | Responsibility |
|---|---|---|
ContactFilterBar | contact_filter_bar_component.rs | Orchestrator (~180 lines). Wires search, sort, popover, and member filter. |
ContactFilterPopover | contact_filter_popover_component.rs | Popover panel with collapsible sections for tags, status, date range, custom fields, and AND/OR logic. |
ContactSortSelect | contact_sort_select_component.rs | Sort dropdown. Rendered inline in full mode, inside the popover in compact mode. |
CustomFieldFilterInput | custom_field_filter_component.rs | Type-aware filter control per custom field (see below). |
Display Modes
Section titled “Display Modes”ContactFilterBar operates in two modes controlled by the compact prop:
| Mode | Where | Layout |
|---|---|---|
Full (compact: false) | Contacts page | Inline search + sort dropdown + “Filters” button |
Compact (compact: true) | Messaging sidebar | Search always visible, filter icon button opens a popover |
The popover anchors to the full filter-bar width to prevent clipping in narrow sidebars.
Draft State Hook
Section titled “Draft State Hook”use_contact_filter_draft (src/mods/contact/hooks/use_contact_filter_draft.rs) bundles the popover’s draft signals into a ContactFilterDraftState struct:
pub struct ContactFilterDraftState { pub local_tag_ids: Signal<Vec<String>>, pub local_status: Signal<Option<ContactStatus>>, pub local_custom_filters: Signal<Vec<CustomFieldFilter>>, pub local_logic: Signal<CustomFieldLogic>, pub local_sort: Signal<ContactListSort>, pub local_last_contacted_range: Signal<Option<TimeRange>>, pub st_section_open: Signal<bool>, pub dr_section_open: Signal<bool>, pub cf_section_open: Signal<bool>,}Key methods:
sync_from_committed(&mut self, filter)— copy committed filter into draft signals when the popover opensapply(&self, filter, compact, show_sort)— build a newContactFilterfrom draft values and reset paginationclear_all()/clear_status_and_tags()/clear_custom_fields()/clear_date_range()— section-level resetsstatus_and_tags_count()/custom_fields_count()/date_range_count()— badge counts per section
CustomFieldFilterInput
Section titled “CustomFieldFilterInput”CustomFieldFilterInput renders the correct filter control based on the field’s ContactFieldType:
| Field Type | Control | Behavior |
|---|---|---|
Select | MultiSelect dropdown | OR within selected options. Shows “No options defined” if the field has no options. |
Boolean | Three toggle buttons (Any / Yes / No) | Exact match on "true" or "false" |
Text, Textarea, Email, Url | Single text Input | Case-insensitive substring match |
Number | Two side-by-side number inputs (Min – Max) | Either bound can be empty for unbounded range |
Date | TimeRangeSelector (presets + calendar) | JSON-serialized TimeRange value |
The Number range filter uses a shared render_range_filter helper with .peek() on the signal to read the “other” slot without triggering re-renders — this prevents signal borrow panics in Dioxus.
API Integration
Section titled “API Integration”The contacts list API (get_contacts_list_api) accepts ContactFilter directly and delegates shared filtering to apply_contact_filters:
GET /api/contacts?search=...&sort=...&tags=...&status=...The messaging contact list also uses ContactFilterBar on the “All” tab. Unanswered tabs use a separate API and hide the filter bar.