docs(member-live): document the overview and use English throughout its internals

This commit is contained in:
Simon 2026-07-06 10:58:39 +02:00
parent 77fc11a0b0
commit 1c67527c56
4 changed files with 48 additions and 32 deletions

View file

@ -1,6 +1,6 @@
defmodule MvWeb.Components.BulkActionsDropdown do defmodule MvWeb.Components.BulkActionsDropdown do
@moduledoc """ @moduledoc """
Single "Aktionen" dropdown bundling the four member bulk actions, flattened to Single "Actions" dropdown bundling the four member bulk actions, flattened to
one level: open in email program (mailto), copy email addresses, export to CSV, one level: open in email program (mailto), copy email addresses, export to CSV,
export to PDF. export to PDF.
@ -10,10 +10,10 @@ defmodule MvWeb.Components.BulkActionsDropdown do
## Scope and trigger badge ## Scope and trigger badge
The trigger reads `Aktionen` followed by a scope badge: an emphasized The trigger reads the `Actions` label followed by a scope badge: an emphasized
(`primary`) count `N` when `N` members are selected, and a muted (`neutral`) (`primary`) count `N` when `N` members are selected, and a muted (`neutral`)
badge otherwise `gefiltert` when a search term or filter narrows the list, badge otherwise `filtered` when a search term or filter narrows the list,
`alle` when nothing is selected and no search/filter is active. Only an actual `all` when nothing is selected and no search/filter is active. Only an actual
selection is emphasized. The badge sits inside the shared `dropdown_menu/1` selection is emphasized. The badge sits inside the shared `dropdown_menu/1`
trigger via its `trigger_badge` slot, matching the member-filter dropdown's trigger via its `trigger_badge` slot, matching the member-filter dropdown's
count badge. The `scope`, `selected_count`, `mailto_bcc`, `recipient_count` count badge. The `scope`, `selected_count`, `mailto_bcc`, `recipient_count`
@ -216,7 +216,7 @@ defmodule MvWeb.Components.BulkActionsDropdown do
gettext("Too many recipients for this function. Copy the addresses or export the list.") gettext("Too many recipients for this function. Copy the addresses or export the list.")
end end
# The trigger scope is shown as a badge after the "Aktionen" label. Only an # The trigger scope is shown as a badge after the "Actions" label. Only an
# actual selection is emphasized (primary); both the "filtered" and "all" # actual selection is emphasized (primary); both the "filtered" and "all"
# scopes are muted (neutral), since neither means members are selected. # scopes are muted (neutral), since neither means members are selected.
defp scope_label(assigns) do defp scope_label(assigns) do

View file

@ -4,24 +4,40 @@ defmodule MvWeb.MemberLive.Index do
## Features ## Features
- Full-text search across member profiles using PostgreSQL tsvector - Full-text search across member profiles using PostgreSQL tsvector
- Sortable columns (name, email, address fields) - DB-side filtering, sorting, and keyset pagination (infinite scroll)
- Bulk selection for future batch operations - Sortable columns, including composite Name (lastfirst) and Address (citypostalstreet)
- Real-time updates via LiveView - Column manager and per-browser view settings (density, composite Member/Address, include-email)
- Bookmarkable URLs with query parameters - Bulk selection spanning the whole filtered set (select-all / copy emails / export)
- Bookmarkable URLs: filter/sort/search/view state is URL-encoded
## URL Parameters ## URL Parameters
- `query` - Search query string for full-text search All list state is URL-encoded so the view is bookmarkable and restored on reload:
- `sort_field` - Field to sort by (e.g., :first_name, :email, :join_date) - `query` - full-text search string
- `sort_order` - Sort direction (:asc or :desc) - `sort_field` / `sort_order` - sort column and direction (`asc` / `desc`)
- `fields` - visible-column selection
- `highlight` - member id to render as selected (return from the show page)
- group / fee-type / boolean / date custom-field filter params (see
`MvWeb.MemberLive.Index.FilterParams` and `DateFilter` for the encoding)
## Events ## Events (`handle_event`)
- `select_member` - Toggle individual member selection - `select_member` / `select_all` - toggle individual / whole-filtered-set selection
- `select_all` - Toggle selection of all visible members - `select_row_and_navigate` - open a member's show page
- `copy_emails` - Copy email addresses of the selected members, or of all/filtered members when nothing is selected - `load_more` - fetch and append the next keyset page (infinite scroll)
- `sort` - change the active sort column/direction
- `toggle_cycle_view` - switch the payment-status view between current and last cycle
- `copy_emails` - copy emails of the selected members, or of the whole filtered set when none are selected
## Messages (`handle_info`, from the filter/search/view-settings components)
- `search_changed`, `view_setting_toggled`, `field_toggled`, `fields_selected`,
`fields_reset`, `reset_all_filters`, and the per-filter change messages
(`group_filter_changed`, `fee_type_filter_changed`, `boolean_filter_changed`,
`date_filters_changed`, `payment_filter_changed`)
## Implementation Notes ## Implementation Notes
- Search uses PostgreSQL full-text search (plainto_tsquery) - Filtering/sorting/pagination run in PostgreSQL via Ash; the socket holds only
- Sort state is synced with URL for bookmarkability the loaded keyset window, not the whole table
- List state is synced with the URL for bookmarkability; a signature guard
avoids redundant reloads
- Components communicate via `handle_info` for decoupling - Components communicate via `handle_info` for decoupling
""" """
use MvWeb, :live_view use MvWeb, :live_view
@ -980,7 +996,7 @@ defmodule MvWeb.MemberLive.Index do
# Flips the "include email" sub-toggle. Only meaningful while the composite # Flips the "include email" sub-toggle. Only meaningful while the composite
# Member cell is on: it moves the email between an in-cell line (on) and a # Member cell is on: it moves the email between an in-cell line (on) and a
# separate E-Mail column (off), so besides re-rendering the rows it recomputes # separate email column (off), so besides re-rendering the rows it recomputes
# which columns the manager offers and their visibility. # which columns the manager offers and their visibility.
defp toggle_view_setting(socket, :member_include_email) do defp toggle_view_setting(socket, :member_include_email) do
new_value = not socket.assigns.view_settings.member_include_email new_value = not socket.assigns.view_settings.member_include_email
@ -995,10 +1011,10 @@ defmodule MvWeb.MemberLive.Index do
end end
# Flips the compact Member field. Besides re-rendering the rows, this changes # Flips the compact Member field. Besides re-rendering the rows, this changes
# which columns the manager offers (composite "Name" vs. Vorname/Nachname/E-Mail, # which columns the manager offers (composite "Name" vs. first-name/last-name/email,
# §7b). The email visibility carries over between the two representations, # §7b). The email visibility carries over between the two representations,
# analogously to the "include email" sub-toggle: composite + include-email ⇄ the # analogously to the "include email" sub-toggle: composite + include-email ⇄ the
# E-Mail column being visible. # email column being visible.
defp toggle_view_setting(socket, :compact_member) do defp toggle_view_setting(socket, :compact_member) do
new_value = not socket.assigns.view_settings.compact_member new_value = not socket.assigns.view_settings.compact_member
email_visible? = email_currently_visible?(socket) email_visible? = email_currently_visible?(socket)
@ -1013,8 +1029,8 @@ defmodule MvWeb.MemberLive.Index do
|> push_field_selection_url() |> push_field_selection_url()
end end
# Flips the compact Address field: offers the composite "Adresse" vs. the # Flips the compact Address field: offers the composite "Address" vs. the
# separate Straße/Hausnummer/PLZ/Ort columns, making the newly relevant columns # separate street/house-number/postal-code/city columns, making the newly relevant columns
# visible and recomputing the offered set + visibility for the new mode. # visible and recomputing the offered set + visibility for the new mode.
defp toggle_view_setting(socket, :compact_address) do defp toggle_view_setting(socket, :compact_address) do
new_value = not socket.assigns.view_settings.compact_address new_value = not socket.assigns.view_settings.compact_address
@ -1031,7 +1047,7 @@ defmodule MvWeb.MemberLive.Index do
end end
# Whether the member email is currently surfaced: while the composite is on, # Whether the member email is currently surfaced: while the composite is on,
# either folded into the cell (include_email) or as a separate E-Mail column; # either folded into the cell (include_email) or as a separate email column;
# while it is off, as its own column. Read before the flip, so `compact_member` # while it is off, as its own column. Read before the flip, so `compact_member`
# still holds the previous mode. # still holds the previous mode.
defp email_currently_visible?(socket) do defp email_currently_visible?(socket) do
@ -1063,8 +1079,8 @@ defmodule MvWeb.MemberLive.Index do
# Makes the now-relevant Member columns visible for the new mode. Turning the # Makes the now-relevant Member columns visible for the new mode. Turning the
# composite on surfaces "Name" (with the email either folded via include_email # composite on surfaces "Name" (with the email either folded via include_email
# or kept as a separate column); turning it off surfaces Vorname/Nachname and # or kept as a separate column); turning it off surfaces first-name/last-name and
# carries the prior email visibility onto the E-Mail column. # carries the prior email visibility onto the email column.
defp apply_member_toggle(socket, true = _new_compact, email_visible?) do defp apply_member_toggle(socket, true = _new_compact, email_visible?) do
selection = selection =
socket.assigns.user_field_selection socket.assigns.user_field_selection
@ -1910,7 +1926,7 @@ defmodule MvWeb.MemberLive.Index do
@doc """ @doc """
Returns true when the member list is restricted by a non-empty search term or Returns true when the member list is restricted by a non-empty search term or
any active filter (cycle status, group, fee type, boolean custom field, or a any active filter (cycle status, group, fee type, boolean custom field, or a
date filter differing from the default). Drives the "(gefiltert)" vs "(alle)" date filter differing from the default). Drives the "filtered" vs "all"
trigger label and reads only assigns no DB access. trigger label and reads only assigns no DB access.
""" """
def filters_active?(assigns) do def filters_active?(assigns) do

View file

@ -76,7 +76,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
defp apply_name_default(set, true = _compact, true = _include_email), do: set defp apply_name_default(set, true = _compact, true = _include_email), do: set
# Compact Member cell without folded email: the composite plus a separate # Compact Member cell without folded email: the composite plus a separate
# default-visible E-Mail column. # default-visible email column.
defp apply_name_default(set, true = _compact, false = _include_email), defp apply_name_default(set, true = _compact, false = _include_email),
do: MapSet.put(set, :email) do: MapSet.put(set, :email)
@ -111,7 +111,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
3. the address group the composite `:address` when compact, otherwise its 3. the address group the composite `:address` when compact, otherwise its
constituents `:street`, `:house_number`, `:postal_code`, `:city`. constituents `:street`, `:house_number`, `:postal_code`, `:city`.
Everything else follows in the configured Datenfelder order. This keeps the Everything else follows in the configured data-fields order. This keeps the
name group ahead of the address group even when the name group is expanded name group ahead of the address group even when the name group is expanded
into `:first_name`/`:last_name` while the address stays compact. into `:first_name`/`:last_name` while the address stays compact.
""" """
@ -145,7 +145,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do
address_variant_hidden?(field, compact_address) address_variant_hidden?(field, compact_address)
end end
# Compact hides Vorname/Nachname (folded into the composite). The email is # Compact hides first-name/last-name (folded into the composite). The email is
# hidden only when it is folded into the cell (compact + include_email); with # hidden only when it is folded into the cell (compact + include_email); with
# include_email off it stays offered as a separate column. Non-compact hides the # include_email off it stays offered as a separate column. Non-compact hides the
# composite in favour of the constituents. # composite in favour of the constituents.

View file

@ -6,11 +6,11 @@ defmodule MvWeb.MemberLive.Index.ViewSettings do
* `:density` `:compact` or `:comfortable` (the row-spacing / compact mode) * `:density` `:compact` or `:comfortable` (the row-spacing / compact mode)
* `:compact_member` composite "Member" cell (last+first name) vs separate * `:compact_member` composite "Member" cell (last+first name) vs separate
Vorname/Nachname columns first-name/last-name columns
* `:member_include_email` whether the email is surfaced (email line in the * `:member_include_email` whether the email is surfaced (email line in the
composite cell, or a separate Email column when the member field is split) composite cell, or a separate Email column when the member field is split)
* `:compact_address` composite Address cell (street / postal+city) vs * `:compact_address` composite Address cell (street / postal+city) vs
separate Straße/PLZ/Ort columns separate street/postal-code/city columns
Persistence is per browser (not per account), resolved in priority order from Persistence is per browser (not per account), resolved in priority order from
the LiveView connect params, the session, the request cookie, and finally the the LiveView connect params, the session, the request cookie, and finally the