From 1c67527c5651071285480149d36dc48d2915d830 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Jul 2026 10:58:39 +0200 Subject: [PATCH] docs(member-live): document the overview and use English throughout its internals --- .../components/bulk_actions_dropdown.ex | 10 ++-- lib/mv_web/live/member_live/index.ex | 60 ++++++++++++------- .../member_live/index/field_visibility.ex | 6 +- .../live/member_live/index/view_settings.ex | 4 +- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/lib/mv_web/components/bulk_actions_dropdown.ex b/lib/mv_web/components/bulk_actions_dropdown.ex index c6db9145..8d0e1509 100644 --- a/lib/mv_web/components/bulk_actions_dropdown.ex +++ b/lib/mv_web/components/bulk_actions_dropdown.ex @@ -1,6 +1,6 @@ defmodule MvWeb.Components.BulkActionsDropdown do @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, export to PDF. @@ -10,10 +10,10 @@ defmodule MvWeb.Components.BulkActionsDropdown do ## 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`) - badge otherwise — `gefiltert` when a search term or filter narrows the list, - `alle` when nothing is selected and no search/filter is active. Only an actual + badge otherwise — `filtered` when a search term or filter narrows the list, + `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` trigger via its `trigger_badge` slot, matching the member-filter dropdown's 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.") 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" # scopes are muted (neutral), since neither means members are selected. defp scope_label(assigns) do diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 16977816..6933e005 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -4,24 +4,40 @@ defmodule MvWeb.MemberLive.Index do ## Features - Full-text search across member profiles using PostgreSQL tsvector - - Sortable columns (name, email, address fields) - - Bulk selection for future batch operations - - Real-time updates via LiveView - - Bookmarkable URLs with query parameters + - DB-side filtering, sorting, and keyset pagination (infinite scroll) + - Sortable columns, including composite Name (last→first) and Address (city→postal→street) + - Column manager and per-browser view settings (density, composite Member/Address, include-email) + - Bulk selection spanning the whole filtered set (select-all / copy emails / export) + - Bookmarkable URLs: filter/sort/search/view state is URL-encoded ## URL Parameters - - `query` - Search query string for full-text search - - `sort_field` - Field to sort by (e.g., :first_name, :email, :join_date) - - `sort_order` - Sort direction (:asc or :desc) + All list state is URL-encoded so the view is bookmarkable and restored on reload: + - `query` - full-text search string + - `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 - - `select_member` - Toggle individual member selection - - `select_all` - Toggle selection of all visible members - - `copy_emails` - Copy email addresses of the selected members, or of all/filtered members when nothing is selected + ## Events (`handle_event`) + - `select_member` / `select_all` - toggle individual / whole-filtered-set selection + - `select_row_and_navigate` - open a member's show page + - `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 - - Search uses PostgreSQL full-text search (plainto_tsquery) - - Sort state is synced with URL for bookmarkability + - Filtering/sorting/pagination run in PostgreSQL via Ash; the socket holds only + 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 """ use MvWeb, :live_view @@ -980,7 +996,7 @@ defmodule MvWeb.MemberLive.Index do # 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 - # 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. defp toggle_view_setting(socket, :member_include_email) do new_value = not socket.assigns.view_settings.member_include_email @@ -995,10 +1011,10 @@ defmodule MvWeb.MemberLive.Index do end # 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, # 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 new_value = not socket.assigns.view_settings.compact_member email_visible? = email_currently_visible?(socket) @@ -1013,8 +1029,8 @@ defmodule MvWeb.MemberLive.Index do |> push_field_selection_url() end - # Flips the compact Address field: offers the composite "Adresse" vs. the - # separate Straße/Hausnummer/PLZ/Ort columns, making the newly relevant columns + # Flips the compact Address field: offers the composite "Address" vs. the + # separate street/house-number/postal-code/city columns, making the newly relevant columns # visible and recomputing the offered set + visibility for the new mode. defp toggle_view_setting(socket, :compact_address) do new_value = not socket.assigns.view_settings.compact_address @@ -1031,7 +1047,7 @@ defmodule MvWeb.MemberLive.Index do end # 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` # still holds the previous mode. 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 # 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 - # carries the prior email visibility onto the E-Mail column. + # or kept as a separate column); turning it off surfaces first-name/last-name and + # carries the prior email visibility onto the email column. defp apply_member_toggle(socket, true = _new_compact, email_visible?) do selection = socket.assigns.user_field_selection @@ -1910,7 +1926,7 @@ defmodule MvWeb.MemberLive.Index do @doc """ 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 - 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. """ def filters_active?(assigns) do diff --git a/lib/mv_web/live/member_live/index/field_visibility.ex b/lib/mv_web/live/member_live/index/field_visibility.ex index 4cd28c76..e7baba78 100644 --- a/lib/mv_web/live/member_live/index/field_visibility.ex +++ b/lib/mv_web/live/member_live/index/field_visibility.ex @@ -76,7 +76,7 @@ defmodule MvWeb.MemberLive.Index.FieldVisibility do defp apply_name_default(set, true = _compact, true = _include_email), do: set # 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), 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 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 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) 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 # include_email off it stays offered as a separate column. Non-compact hides the # composite in favour of the constituents. diff --git a/lib/mv_web/live/member_live/index/view_settings.ex b/lib/mv_web/live/member_live/index/view_settings.ex index b26fcb17..f3e2644c 100644 --- a/lib/mv_web/live/member_live/index/view_settings.ex +++ b/lib/mv_web/live/member_live/index/view_settings.ex @@ -6,11 +6,11 @@ defmodule MvWeb.MemberLive.Index.ViewSettings do * `:density` — `:compact` or `:comfortable` (the row-spacing / compact mode) * `: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 composite cell, or a separate Email column when the member field is split) * `: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 the LiveView connect params, the session, the request cookie, and finally the