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

@ -36,11 +36,13 @@ defmodule MvWeb.Components.BulkActionsDropdown do
## Event routing
`dropdown_menu/1` sends `toggle_dropdown`/`close_dropdown` to `@myself`, so the
component owns its own `:open` state. The copy item carries an *un-targeted*
`phx-click="copy_emails"`, which therefore reaches the parent LiveView's
`handle_event/3` (which keeps access to `@members`), plus the
`CopyToClipboard` hook.
`dropdown_menu/1` sends `toggle_dropdown`/`close_dropdown` *un-targeted*, so the
parent LiveView owns the `:open` state (passed back in via the `open` assign).
This lets the parent fetch the mailto recipients lazily when the dropdown opens
rather than on every selection/filter change. The copy item likewise carries an
un-targeted `phx-click="copy_emails"`, which reaches the parent's
`handle_event/3` (which keeps access to `@members`), plus the `CopyToClipboard`
hook.
"""
use MvWeb, :live_component
use Gettext, backend: MvWeb.Gettext
@ -72,13 +74,13 @@ defmodule MvWeb.Components.BulkActionsDropdown do
|> assign(:recipient_count, assigns[:recipient_count] || 0)
|> assign(:mailto_disabled?, assigns[:mailto_disabled?] || false)
# The parent never sets :open (the component owns it via toggle/close).
# Honouring an explicit :open assign keeps the component renderable in
# isolation (render_component/2) for structural tests.
# The parent owns :open and passes it in. Defaulting to false keeps the
# component renderable in isolation (render_component/2) for structural tests
# that omit it.
socket =
case Map.fetch(assigns, :open) do
{:ok, open} -> assign(socket, :open, open)
:error -> socket
:error -> assign_new(socket, :open, fn -> false end)
end
{:ok, socket}
@ -98,7 +100,7 @@ defmodule MvWeb.Components.BulkActionsDropdown do
button_label={gettext("Actions")}
icon="hero-bolt"
open={@open}
phx_target={@myself}
phx_target={nil}
menu_width="w-70"
menu_align="left"
button_class="btn-secondary gap-2"
@ -232,12 +234,6 @@ defmodule MvWeb.Components.BulkActionsDropdown do
end
end
@impl true
def handle_event("toggle_dropdown", _params, socket) do
{:noreply, assign(socket, :open, !socket.assigns.open)}
end
def handle_event("close_dropdown", _params, socket) do
{:noreply, assign(socket, :open, false)}
end
# Open/close are handled by the parent LiveView (see "Event routing"); the
# component no longer owns dropdown state.
end