Shared state glue for the calendar assignee/overdue filter — the chip-rail model (person chips via the core search_picker, a Me quick-toggle, an Unassigned lens, Personal-only and Overdue-only refinements, one Clear).
Two LiveViews consume it (the Overview Tasks calendar and the per-project Calendar tab), each with its own item semantics — the Overview filters flattened leaf tasks, the project tab filters top-level bars with descendant-aware matching. This module owns everything that must NOT drift between them: the assigns, the event handling (including the picker's search/pick/staged contract), scope resolution, and union matching.
Usage
# mount
socket = socket |> assign(AssigneeFilter.defaults()) |> ...
# one handle_event clause forwards every filter event
def handle_event(event, params, socket) when event in @assignee_filter_events do
case AssigneeFilter.update(socket, event, params) do
{socket, :reapply} -> {:noreply, apply_my_filter(socket)}
{socket, :noop} -> {:noreply, socket}
end
endwith @assignee_filter_events AssigneeFilter.events(). The panel UI lives
in <.assignee_filter_panel> (Web.Components.AssigneeFilterPanel).
Summary
Functions
How many filters are active — badges the funnel button.
The resolved scopes of the picked person chips.
Default assigns for a consuming LiveView's mount.
The filter's event names — guard the forwarding handle_event clause with these.
Matches one assignment against every selected scope; a :direct hit for
anyone wins (so direct-only keeps a task any selected person holds
personally), otherwise the first inherited provenance labels the row.
Resolves the viewer's own scope once per mount (:unresolved → scope | nil).
Call from the consumer's data-load path; nil hides the Me quick-adder.
Applies one filter event to the socket. Returns {socket, :reapply} when
the consumer must re-derive its filtered view, {socket, :noop} otherwise
(picker searches answer via push_event and change nothing else).
Functions
@spec active_count(map()) :: non_neg_integer()
How many filters are active — badges the funnel button.
@spec current_scopes(map()) :: [PhoenixKitProjects.Assignees.scope()]
The resolved scopes of the picked person chips.
@spec defaults() :: keyword()
Default assigns for a consuming LiveView's mount.
@spec events() :: [String.t()]
The filter's event names — guard the forwarding handle_event clause with these.
@spec match_any( struct(), [PhoenixKitProjects.Assignees.scope()] ) :: :direct | {:team, String.t()} | {:department, String.t()} | nil
Matches one assignment against every selected scope; a :direct hit for
anyone wins (so direct-only keeps a task any selected person holds
personally), otherwise the first inherited provenance labels the row.
@spec resolve_me(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
Resolves the viewer's own scope once per mount (:unresolved → scope | nil).
Call from the consumer's data-load path; nil hides the Me quick-adder.
@spec update(Phoenix.LiveView.Socket.t(), String.t(), map()) :: {Phoenix.LiveView.Socket.t(), :reapply | :noop}
Applies one filter event to the socket. Returns {socket, :reapply} when
the consumer must re-derive its filtered view, {socket, :noop} otherwise
(picker searches answer via push_event and change nothing else).