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
|
|
@ -5,7 +5,6 @@
|
|||
<.live_component
|
||||
module={MvWeb.Components.BulkActionsDropdown}
|
||||
id="bulk-actions-dropdown"
|
||||
open={@bulk_actions_open}
|
||||
export_payload_json={@export_payload_json}
|
||||
selected_count={@selected_count}
|
||||
scope={@scope}
|
||||
|
|
@ -72,13 +71,23 @@
|
|||
</span>
|
||||
</.button>
|
||||
</.tooltip>
|
||||
<.live_component
|
||||
module={MvWeb.Components.FieldVisibilityDropdownComponent}
|
||||
id="field-visibility-dropdown"
|
||||
all_fields={@all_available_fields}
|
||||
custom_fields={@all_custom_fields}
|
||||
selected_fields={@user_field_selection}
|
||||
/>
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<.live_component
|
||||
module={MvWeb.Components.FieldVisibilityDropdownComponent}
|
||||
id="field-visibility-dropdown"
|
||||
all_fields={@all_available_fields}
|
||||
custom_fields={@all_custom_fields}
|
||||
selected_fields={@user_field_selection}
|
||||
/>
|
||||
<.live_component
|
||||
module={MvWeb.Components.ViewSettingsDropdownComponent}
|
||||
id="view-settings-dropdown"
|
||||
density={@density}
|
||||
compact_member={@compact_member}
|
||||
member_include_email={@member_include_email}
|
||||
compact_address={@compact_address}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Polite live region: present on first render (before it is filled) so
|
||||
|
|
@ -103,6 +112,8 @@
|
|||
role="region"
|
||||
aria-label={gettext("Members table")}
|
||||
aria-busy={to_string(@loading?)}
|
||||
data-density={@density}
|
||||
tabindex="0"
|
||||
>
|
||||
<.table
|
||||
id="members"
|
||||
|
|
@ -122,6 +133,7 @@
|
|||
dynamic_cols={@dynamic_cols}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
size_class={if @density == :compact, do: "table-xs", else: "table-md"}
|
||||
>
|
||||
|
||||
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
||||
|
|
@ -130,10 +142,14 @@
|
|||
col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1}
|
||||
label={
|
||||
~H"""
|
||||
<.input
|
||||
<input
|
||||
type="checkbox"
|
||||
id="select-all-checkbox"
|
||||
name="select_all"
|
||||
class="checkbox checkbox-sm"
|
||||
phx-click="select_all"
|
||||
phx-hook="IndeterminateCheckbox"
|
||||
data-indeterminate={to_string(@selected_count > 0 and @selected_count < @total_count)}
|
||||
checked={@total_count > 0 and @selected_count == @total_count}
|
||||
aria-label={gettext("Select all members")}
|
||||
role="checkbox"
|
||||
|
|
@ -141,9 +157,10 @@
|
|||
"""
|
||||
}
|
||||
>
|
||||
<.input
|
||||
<input
|
||||
type="checkbox"
|
||||
name={member.id}
|
||||
class="checkbox checkbox-sm"
|
||||
checked={MapSet.member?(@selected_members, member.id)}
|
||||
aria-label={gettext("Select member")}
|
||||
role="checkbox"
|
||||
|
|
@ -151,8 +168,21 @@
|
|||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:name in @member_fields_visible}
|
||||
label={gettext("Name")}
|
||||
:if={:name in @member_fields_visible and @compact_member}
|
||||
sort_field={:name}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_name}
|
||||
field={:name}
|
||||
label={gettext("Name")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
sort_hint={gettext("Click to sort by last name")}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<% full_name =
|
||||
[member.first_name, member.last_name]
|
||||
|
|
@ -163,7 +193,8 @@
|
|||
{full_name}
|
||||
</div>
|
||||
<div
|
||||
:if={member.email not in [nil, ""]}
|
||||
:if={@member_include_email and member.email not in [nil, ""]}
|
||||
data-testid="member-name-email"
|
||||
class="opacity-70 truncate"
|
||||
title={member.email}
|
||||
>
|
||||
|
|
@ -171,29 +202,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:address in @member_fields_visible}
|
||||
label={gettext("Address")}
|
||||
>
|
||||
<% line1 =
|
||||
[member.street, member.house_number]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<% line2 =
|
||||
[member.postal_code, member.city]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<.maybe_value value={line1 <> line2} empty_sr_text={gettext("No address")}>
|
||||
<div data-testid="member-address">
|
||||
<div class="truncate" title={line1}>{line1}</div>
|
||||
<div class="opacity-70 truncate" title={line2}>{line2}</div>
|
||||
</div>
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:first_name in @member_fields_visible}
|
||||
sort_field={:first_name}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -212,6 +224,7 @@
|
|||
<:col
|
||||
:let={member}
|
||||
:if={:last_name in @member_fields_visible}
|
||||
sort_field={:last_name}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -230,6 +243,7 @@
|
|||
<:col
|
||||
:let={member}
|
||||
:if={:email in @member_fields_visible}
|
||||
sort_field={:email}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -245,9 +259,117 @@
|
|||
>
|
||||
{member.email}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:address in @member_fields_visible and @compact_address}
|
||||
sort_field={:address}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_address}
|
||||
field={:address}
|
||||
label={gettext("Address")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
sort_hint={gettext("Click to sort by city")}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<% line1 =
|
||||
[member.street, member.house_number]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<% line2 =
|
||||
[member.postal_code, member.city]
|
||||
|> Enum.reject(&(&1 in [nil, ""]))
|
||||
|> Enum.join(" ") %>
|
||||
<div data-testid="member-address">
|
||||
<div class="truncate" title={line1}>{line1}</div>
|
||||
<div class="opacity-70 truncate" title={line2}>{line2}</div>
|
||||
</div>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:street in @member_fields_visible}
|
||||
sort_field={:street}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_street}
|
||||
field={:street}
|
||||
label={gettext("Street")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.street}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:house_number in @member_fields_visible}
|
||||
sort_field={:house_number}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_house_number}
|
||||
field={:house_number}
|
||||
label={gettext("House Number")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.house_number}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:postal_code in @member_fields_visible}
|
||||
sort_field={:postal_code}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_postal_code}
|
||||
field={:postal_code}
|
||||
label={gettext("Postal Code")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.postal_code}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:city in @member_fields_visible}
|
||||
sort_field={:city}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_city}
|
||||
field={:city}
|
||||
label={gettext("City")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.city}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:join_date in @member_fields_visible}
|
||||
sort_field={:join_date}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -266,6 +388,7 @@
|
|||
<:col
|
||||
:let={member}
|
||||
:if={:exit_date in @member_fields_visible}
|
||||
sort_field={:exit_date}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -291,6 +414,7 @@
|
|||
<:col
|
||||
:let={member}
|
||||
:if={:country in @member_fields_visible}
|
||||
sort_field={:country}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -306,89 +430,10 @@
|
|||
>
|
||||
{member.country}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:city in @member_fields_visible}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_city}
|
||||
field={:city}
|
||||
label={gettext("City")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<.maybe_value value={member.city} empty_sr_text={gettext("Not specified")}>
|
||||
{member.city}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:street in @member_fields_visible}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_street}
|
||||
field={:street}
|
||||
label={gettext("Street")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<.maybe_value value={member.street} empty_sr_text={gettext("Not specified")}>
|
||||
{member.street}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:house_number in @member_fields_visible}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_house_number}
|
||||
field={:house_number}
|
||||
label={gettext("House Number")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<.maybe_value value={member.house_number} empty_sr_text={gettext("Not specified")}>
|
||||
{member.house_number}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:postal_code in @member_fields_visible}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_postal_code}
|
||||
field={:postal_code}
|
||||
label={gettext("Postal Code")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<.maybe_value value={member.postal_code} empty_sr_text={gettext("Not specified")}>
|
||||
{member.postal_code}
|
||||
</.maybe_value>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
:if={:membership_fee_start_date in @member_fields_visible}
|
||||
sort_field={:membership_fee_start_date}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -407,6 +452,7 @@
|
|||
<:col
|
||||
:let={member}
|
||||
:if={:membership_fee_type in @member_fields_visible}
|
||||
sort_field={:membership_fee_type}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -443,6 +489,7 @@
|
|||
<:col
|
||||
:let={member}
|
||||
:if={:groups in @member_fields_visible}
|
||||
sort_field={:groups}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
|
|
@ -491,11 +538,17 @@
|
|||
class="h-0"
|
||||
>
|
||||
</div>
|
||||
<%!-- The accent (background + top separator) and row height live on the
|
||||
full-width footer cell (see #members-footer in app.css) so they span the
|
||||
whole table edge-to-edge and match the active density. This inner element
|
||||
stays pinned to the visible viewport width via StickyViewportWidth and is
|
||||
transparent, so the cell accent shows through while the spinner/label stay
|
||||
centered during horizontal scroll. --%>
|
||||
<div
|
||||
id="members-loading-bar"
|
||||
phx-hook="StickyViewportWidth"
|
||||
data-scroll-container="members-table-guard"
|
||||
class="sticky left-0 flex items-center justify-center gap-2 border-t border-base-300 bg-base-200 py-4 text-base-content/70"
|
||||
class="sticky left-0 flex items-center justify-center gap-2 text-base-content/70"
|
||||
data-testid="members-loading-row"
|
||||
>
|
||||
<span class="loading loading-spinner loading-sm" aria-hidden="true"></span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue