Assignment Filters & Bulk Operations
The assignments view (/contacts/assignments) uses the same ContactFilter system as the contacts list. Search, tags, status, custom fields, and member filtering all work through the unified filter pipeline, with URL persistence and bulk operations built in.
Filter Integration
Section titled “Filter Integration”The assignments view passes ContactFilter to the same apply_contact_filters service used by the contacts list. The one assignments-specific field is member_filter:
pub enum AssignmentMemberFilter { All, // show all contacts Unassigned, // contacts with no member assigned Member(String), // contacts assigned to a specific member}ContactFilterBar renders the member dropdown when you set show_member_filter: true:
ContactFilterBar { filter: filter_signal, on_filter_change: move |f| { /* update filter + URL */ }, show_member_filter: true, member_options: org_members.iter() .map(|m| (m.id.to_string(), m.display_name.clone())) .collect(),}URL Persistence
Section titled “URL Persistence”The route includes query parameters so filter state survives page reloads:
#[route("/contacts/assignments?:query")]ContactAssignmentsView { query: ContactFilterQuery },The member query parameter maps to:
| URL value | Filter state |
|---|---|
"" or absent | All — show all contacts |
"unassigned" | Unassigned — no member assigned |
| UUID string | Member(id) — assigned to that member |
Org-Scoped Member Validation
Section titled “Org-Scoped Member Validation”When filtering by a specific member ID, the API validates that the member belongs to the current organization before executing the query:
let member_exists = schemas::member::Entity::find_by_id(member_id) .filter(schemas::member::Column::OrganizationId.eq(session.organization.id)) .count(&db) .await?;
if member_exists == 0 { return HttpError::bad_request("Invalid member ID");}This prevents cross-org data access if a user crafts a request with another organization’s member ID.
Bulk Operations
Section titled “Bulk Operations”Select multiple contacts via checkboxes, then use “Assign to…” or “Remove from…” to apply changes in batch.
How It Works
Section titled “How It Works”- Select contacts — the bulk action bar appears with the count
- Pick an action (assign or unassign) and choose a member
- The
run_bulk_operationhelper processes each contact sequentially, tracking progress - A toast summarizes the result:
"Assigned to 10 contacts"or"Removed from 7 of 10 contacts — 3 failed"
Loading State
Section titled “Loading State”The view tracks loading state per contact with a HashSet<Uuid> instead of a single Option<Uuid>. This lets multiple rows show spinners simultaneously during bulk operations:
let mut loading_ids: Signal<HashSet<Uuid>> = use_signal(HashSet::new);
// During bulk operationloading_ids.write().insert(contact_id);// ... API call ...loading_ids.write().remove(&contact_id);Progress Indicator
Section titled “Progress Indicator”Bulk operations report progress via bulk_progress: Signal<Option<(usize, usize)>>. The action bar displays “Processing 3 of 10…” while the operation runs.
Mobile Responsiveness
Section titled “Mobile Responsiveness”The assignments view adapts to small screens:
- Phone column hides below the
smbreakpoint (hidden sm:table-cell) - Inline member picker stretches full-width on mobile, fixed 256px on desktop
- Bulk action bar spans full width with margins on mobile, auto-width centered on desktop
API Reference
Section titled “API Reference”GET /api/contacts/assignments
Section titled “GET /api/contacts/assignments”Returns paginated contacts with assignment data. Accepts all ContactFilter query parameters.
Response:
pub struct ContactAssignmentResponse { pub contacts: Vec<ContactAssignment>, pub total_count: u64, pub assigned_count: u64, // org-wide assigned count pub page: u64, pub page_size: u64, pub total_pages: u64,}Permission: Contact::Collection(ContactCollectionPermission::List)
POST /api/contacts/{contact_id}/members/{member_id}
Section titled “POST /api/contacts/{contact_id}/members/{member_id}”Assigns a member to a contact. Idempotent — assigning an already-assigned member is a no-op.
Permission: Contact::Instance(ContactInstancePermission::Update)
DELETE /api/contacts/{contact_id}/members/{member_id}
Section titled “DELETE /api/contacts/{contact_id}/members/{member_id}”Removes a member assignment from a contact.
Permission: Contact::Instance(ContactInstancePermission::Update)