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:
parent
af2cc2e0d4
commit
dfc616257d
21 changed files with 1643 additions and 263 deletions
129
lib/mv_web/live/components/view_settings_dropdown_component.ex
Normal file
129
lib/mv_web/live/components/view_settings_dropdown_component.ex
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
defmodule MvWeb.Components.ViewSettingsDropdownComponent do
|
||||
@moduledoc """
|
||||
LiveComponent for the member-overview view settings (§1.20).
|
||||
|
||||
Renders an icon button with a tooltip that opens a dropdown of toggles:
|
||||
|
||||
* Compact mode — table row density (compact ↔ comfortable)
|
||||
* Compact Member field — composite last+first name cell, with a sub-toggle
|
||||
"include email"
|
||||
* Compact address field — composite street / postal+city cell
|
||||
|
||||
Turning a composite field off surfaces the underlying separate columns in the
|
||||
table. The component is display-only: it emits `{:view_setting_toggled, key}`
|
||||
to the parent LiveView, which owns and persists the settings.
|
||||
|
||||
## Props
|
||||
- `:density` — `:compact` or `:comfortable`
|
||||
- `:compact_member` — boolean
|
||||
- `:member_include_email` — boolean
|
||||
- `:compact_address` — boolean
|
||||
- `:id` — component id
|
||||
"""
|
||||
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:open, fn -> false end)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.dropdown_menu
|
||||
id="view-settings-menu"
|
||||
icon="hero-cog-6-tooth"
|
||||
button_label={gettext("View")}
|
||||
open={@open}
|
||||
phx_target={@myself}
|
||||
menu_width="w-72"
|
||||
testid="view-settings"
|
||||
button_testid="view-settings-button"
|
||||
>
|
||||
<li role="none" class="mb-1 px-2">
|
||||
<span class="font-semibold">{gettext("View settings")}</span>
|
||||
</li>
|
||||
|
||||
<.toggle_item
|
||||
setting="density"
|
||||
label={gettext("Compact mode")}
|
||||
checked={@density == :compact}
|
||||
target={@myself}
|
||||
/>
|
||||
|
||||
<.toggle_item
|
||||
setting="compact_member"
|
||||
label={gettext("Compact Member field")}
|
||||
checked={@compact_member}
|
||||
target={@myself}
|
||||
/>
|
||||
|
||||
<%!-- The email line lives inside the composite Member cell, so this
|
||||
sub-toggle is only meaningful while that cell is compact. When the
|
||||
composite is off, email is its own column and this toggle is omitted. --%>
|
||||
<li :if={@compact_member} role="none" class="pl-6">
|
||||
<.toggle_item
|
||||
setting="member_include_email"
|
||||
label={gettext("include email")}
|
||||
checked={@member_include_email}
|
||||
target={@myself}
|
||||
/>
|
||||
</li>
|
||||
|
||||
<.toggle_item
|
||||
setting="compact_address"
|
||||
label={gettext("Compact address field")}
|
||||
checked={@compact_address}
|
||||
target={@myself}
|
||||
/>
|
||||
</.dropdown_menu>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :setting, :string, required: true
|
||||
attr :label, :string, required: true
|
||||
attr :checked, :boolean, required: true
|
||||
attr :target, :any, required: true
|
||||
|
||||
defp toggle_item(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={to_string(@checked)}
|
||||
aria-label={@label}
|
||||
tabindex="0"
|
||||
class="flex items-center gap-2 px-2 py-1 rounded w-full text-left cursor-pointer hover:bg-base-200"
|
||||
phx-click="toggle"
|
||||
phx-value-setting={@setting}
|
||||
phx-target={@target}
|
||||
data-testid={"view-setting-#{String.replace(@setting, "_", "-")}"}
|
||||
>
|
||||
<span class={if @checked, do: "text-primary", else: "text-base-300"} aria-hidden="true">
|
||||
<.icon name="hero-check" class="size-4 shrink-0" />
|
||||
</span>
|
||||
<span>{@label}</span>
|
||||
</button>
|
||||
"""
|
||||
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
|
||||
|
||||
def handle_event("toggle", %{"setting" => setting}, socket) do
|
||||
send(self(), {:view_setting_toggled, String.to_existing_atom(setting)})
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue