feat(member): let members tailor overview density and visible columns

The View dropdown drives row density and whether the Member and Address
fields render as composite cells or split into their underlying columns;
the column manager toggles visibility and resets to the curated default.
All choices persist per browser through the URL/session/cookie/global
chain, so a saved layout survives reloads without a per-account store.
This commit is contained in:
Simon 2026-07-06 10:45:53 +02:00
parent af2cc2e0d4
commit dfc616257d
21 changed files with 1643 additions and 263 deletions

View file

@ -4,11 +4,25 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
# The curated default columns hide the individual name/address sort headers,
# so these component tests make every sortable field visible via ?fields=.
# The individual name/address constituents are only *offered* when the compact
# Member / Address view settings are off, so the session opts them out of the
# composite columns before selecting the fields via ?fields=.
@cols "first_name,email,street,house_number,postal_code,city,country,join_date"
# Disables the compact Member/Address composites so first_name/last_name/email
# and street/house_number/postal_code/city are offered as individual sortable
# columns (persisted per browser via the view-settings session key).
defp non_compact(conn) do
Plug.Conn.put_session(
conn,
"member_view_settings",
~s({"compact_member":false,"compact_address":false})
)
end
describe "rendering" do
test "renders with correct attributes", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Test that the component renders with correct attributes
@ -18,7 +32,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "renders all sortable headers", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
sortable_fields = [
@ -38,7 +52,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "renders correct labels", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Test specific labels
@ -50,7 +64,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
describe "sort icons" do
test "shows neutral icon for specific field when not sorted", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# The neutral icon has the opcity class we can test for
@ -62,7 +76,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "shows ascending icon for specific field when sorted ascending", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, html} =
live(conn, "/members?fields=#{@cols}&query=&sort_field=city&sort_order=asc")
@ -85,7 +99,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "shows descending icon for specific field when sorted descending", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, _view, html} =
live(conn, "/members?fields=#{@cols}&query=&sort_field=email&sort_order=desc")
@ -99,7 +113,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "multiple fields can have different icon states", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} =
live(conn, "/members?fields=#{@cols}&query=&sort_field=city&sort_order=asc")
@ -118,7 +132,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "icon state changes correctly when clicking different fields", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Start: all fields neutral except first name as default
@ -146,7 +160,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "specific field shows correct icon for each sort state", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
# Test EMAIL field specifically
{:ok, view, html_asc} =
@ -167,7 +181,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "icon distribution shows exactly one active sort icon", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
# Test neutral state - only one field should have active sort icon
{:ok, _view, html_neutral} = live(conn, "/members?fields=#{@cols}")
@ -204,7 +218,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
describe "accessibility" do
test "sets aria-label correctly for unsorted state", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Check aria-label for unsorted state
@ -212,7 +226,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "sets aria-label correctly for ascending sort", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} =
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=asc")
@ -222,7 +236,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "sets aria-label correctly for descending sort", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} =
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=desc")
@ -232,7 +246,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "includes tooltip with correct aria-label", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} =
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=asc")
@ -243,7 +257,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "aria-labels work for all sortable fields", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}&sort_field=email&sort_order=desc")
# Test aria-labels for different fields
@ -260,7 +274,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
describe "component behavior" do
test "clicking triggers sort event on parent LiveView", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Click on the first name sort header
@ -273,7 +287,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "component handles different field types correctly", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Test that different field types render correctly
@ -285,7 +299,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
describe "edge cases" do
test "handles invalid sort field gracefully", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, html} =
live(conn, "/members?fields=#{@cols}&sort_field=invalid_field&sort_order=asc")
@ -296,7 +310,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "handles invalid sort order gracefully", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, html} =
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=invalid")
@ -307,7 +321,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "handles empty sort parameters", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, html} = live(conn, "/members?fields=#{@cols}&sort_field=&sort_order=")
# Should show neutral icons
@ -318,7 +332,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
describe "icon state transitions" do
test "icon changes when sorting state changes", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
# Start with neutral state
@ -334,7 +348,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
end
test "multiple fields can be tested for icon states", %{conn: conn} do
conn = conn_with_oidc_user(conn)
conn = conn |> conn_with_oidc_user() |> non_compact()
{:ok, view, html} = live(conn, "/members?fields=#{@cols}&sort_field=email&sort_order=desc")
# Email should be active (descending)