feat(member): stream the overview with keyset infinite scroll instead of loading every member

This commit is contained in:
Simon 2026-07-03 11:31:44 +02:00
parent b09cdf7f3a
commit b79d7ac9ea
13 changed files with 1067 additions and 451 deletions

View file

@ -306,7 +306,8 @@ defmodule MvWeb.MemberLive.IndexTest do
# asserted on internal state to preserve the original coverage of the callback.
assigns = :sys.get_state(view.pid).socket.assigns
assert assigns.query == "Friedrich"
assert is_list(assigns.members)
# Loaded members are held as a `%{id => member}` map (the restream lookup window).
assert is_map(assigns.members)
end
@tag :ui
@ -1069,6 +1070,63 @@ defmodule MvWeb.MemberLive.IndexTest do
assert bcc =~ "scope1%40example.com"
refute bcc =~ "scope2%40example.com"
end
test "selecting a member does not read the DB for mailto recipients (deferred to open)",
%{conn: conn, member1: member1} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members")
# A checkbox toggle is a non-search interaction; it must not issue a member
# read just to precompute a mailto link the user may never open. The
# recipient list is fetched lazily when the bulk-actions dropdown opens.
member_reads =
capture_member_select_queries(fn ->
render_click(view, "select_member", %{"id" => member1.id})
end)
assert member_reads == [],
"select_member must not read members for the mailto recipients; got: #{inspect(member_reads)}"
# Opening the dropdown still surfaces the correct recipients for the selection.
bcc = mailto_bcc(view)
assert bcc =~ "scope1%40example.com"
refute bcc =~ "scope2%40example.com"
end
# Captures every SELECT against the members table emitted while `fun` runs.
defp capture_member_select_queries(fun) do
test_pid = self()
handler_id = "test-member-select-#{System.unique_integer([:positive])}"
:telemetry.attach(
handler_id,
[:mv, :repo, :query],
fn _event, _measurements, metadata, _config ->
sql = metadata[:query] || ""
if String.contains?(sql, "SELECT") and String.contains?(sql, "\"members\"") do
send(test_pid, {:member_query, sql})
end
end,
nil
)
try do
fun.()
after
:telemetry.detach(handler_id)
end
collect_member_queries([])
end
defp collect_member_queries(acc) do
receive do
{:member_query, sql} -> collect_member_queries([sql | acc])
after
0 -> Enum.reverse(acc)
end
end
end
describe "cycle status filter" do
@ -2372,12 +2430,12 @@ defmodule MvWeb.MemberLive.IndexTest do
# Should complete in less than 1 second (1000ms)
assert duration < 1000, "Filter took #{duration}ms, expected < 1000ms"
# Verify filtering worked correctly - should show all true members
Enum.each(1..75, fn i ->
assert html =~ "TrueMember#{i}"
end)
# The overview now keyset-paginates: mount loads only the first page, not
# the whole filtered set (§1.7). The filter still resolves DB-side, so the
# loaded page contains only matching (true) members and never a non-matching
# (false) one.
assert html =~ "TrueMember"
# Should not show false members
Enum.each(1..75, fn i ->
refute html =~ "FalseMember#{i}"
end)