feat(member): stream the overview with keyset infinite scroll instead of loading every member
This commit is contained in:
parent
b09cdf7f3a
commit
b79d7ac9ea
13 changed files with 1067 additions and 451 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -953,6 +953,11 @@ defmodule MvWeb.CoreComponents do
|
|||
doc:
|
||||
"when true, first header/body column gets sticky left positioning to keep selection controls visible"
|
||||
|
||||
attr :viewport_bottom, :any,
|
||||
default: nil,
|
||||
doc:
|
||||
"optional phx-viewport-bottom event (string) rendered on the streamed tbody so the built-in InfiniteScroll hook appends the next page without shifting the scroll position; nil omits the binding"
|
||||
|
||||
slot :col, required: true do
|
||||
attr :label, :string
|
||||
attr :class, :string
|
||||
|
|
@ -964,6 +969,10 @@ defmodule MvWeb.CoreComponents do
|
|||
|
||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
||||
|
||||
slot :footer,
|
||||
doc:
|
||||
"optional after-rows content rendered as a full-width row in a separate, non-streamed tbody. Combined with viewport_bottom it acts as the infinite-scroll sentinel (e.g. a loading indicator) that fires the load event and disappears once no more pages remain."
|
||||
|
||||
def table(assigns) do
|
||||
assigns =
|
||||
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
||||
|
|
@ -986,6 +995,15 @@ defmodule MvWeb.CoreComponents do
|
|||
assigns =
|
||||
assign(assigns, :first_row_click_col_idx, first_row_click_col_idx)
|
||||
|
||||
# Total column span for full-width footer rows (e.g. the infinite-scroll
|
||||
# loading indicator): fixed columns + dynamic custom-field columns + the
|
||||
# optional action column.
|
||||
col_count =
|
||||
length(assigns.col) + length(assigns[:dynamic_cols] || []) +
|
||||
if assigns.action != [], do: 1, else: 0
|
||||
|
||||
assigns = assign(assigns, :col_count, col_count)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
id={@row_click && "#{@id}-keyboard"}
|
||||
|
|
@ -1021,7 +1039,10 @@ defmodule MvWeb.CoreComponents do
|
|||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tbody
|
||||
id={@id}
|
||||
phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}
|
||||
>
|
||||
<tr
|
||||
:for={row <- @rows}
|
||||
id={@row_id && @row_id.(row)}
|
||||
|
|
@ -1126,6 +1147,21 @@ defmodule MvWeb.CoreComponents do
|
|||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<%!-- After-rows footer in its own, non-streamed tbody so it stays put
|
||||
outside the phx-update="stream" container. When viewport_bottom is set it
|
||||
doubles as the infinite-scroll sentinel: rendered only while more pages
|
||||
exist, so once the last page loads it disappears and stops firing. --%>
|
||||
<tbody
|
||||
:if={@footer != [] && @viewport_bottom}
|
||||
id={"#{@id}-footer"}
|
||||
phx-viewport-bottom={@viewport_bottom}
|
||||
>
|
||||
<tr>
|
||||
<td colspan={@col_count}>
|
||||
{render_slot(@footer)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue