Adds more consistency in various UX topics closes #447 #448
8 changed files with 411 additions and 324 deletions
|
|
@ -272,6 +272,11 @@ Notes:
|
||||||
- **MUST:** Truncate long values consistently (same max widths for name/email-like fields).
|
- **MUST:** Truncate long values consistently (same max widths for name/email-like fields).
|
||||||
- **MUST:** Tooltip reveals full value when truncated.
|
- **MUST:** Tooltip reveals full value when truncated.
|
||||||
|
|
||||||
|
### 8.5 Loading/Lists/Tables: keep filters visible on desktop
|
||||||
|
- On **desktop (lg: breakpoint)** only: list pages with large datasets (e.g. Members overview) keep the page header and filter/search bar visible while the user scrolls. Only the table body scrolls inside a constrained area (`lg:max-h-[calc(100vh-<offset>)] lg:overflow-auto`). This preserves context and avoids losing filters when scrolling.
|
||||||
|
- On **mobile**, sticky headers are not used; the layout uses normal flow (header and table scroll with the page) to preserve vertical space.
|
||||||
|
- When the table is inside such a scroll container, use the CoreComponents table’s `sticky_header={true}` so the table’s `<thead>` stays sticky within the scroll area on desktop (`lg:sticky lg:top-0`, opaque background `bg-base-100`, z-index). Sticky areas must not overlap content at 200% zoom; focus order must remain header → filters → table.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9) Flash / Toast messages (mandatory UX)
|
## 9) Flash / Toast messages (mandatory UX)
|
||||||
|
|
|
||||||
|
|
@ -715,6 +715,11 @@ defmodule MvWeb.CoreComponents do
|
||||||
attr :sort_field, :any, default: nil, doc: "current sort field"
|
attr :sort_field, :any, default: nil, doc: "current sort field"
|
||||||
attr :sort_order, :atom, default: nil, doc: "current sort order"
|
attr :sort_order, :atom, default: nil, doc: "current sort order"
|
||||||
|
|
||||||
|
attr :sticky_header, :boolean,
|
||||||
|
default: false,
|
||||||
|
doc:
|
||||||
|
"when true, thead th get lg:sticky lg:top-0 bg-base-100 z-10 for use inside a scroll container on desktop"
|
||||||
|
|
||||||
slot :col, required: true do
|
slot :col, required: true do
|
||||||
attr :label, :string
|
attr :label, :string
|
||||||
attr :class, :string
|
attr :class, :string
|
||||||
|
|
@ -745,12 +750,12 @@ defmodule MvWeb.CoreComponents do
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
:for={col <- @col}
|
:for={col <- @col}
|
||||||
class={Map.get(col, :class)}
|
class={table_th_class(col, @sticky_header)}
|
||||||
aria-sort={table_th_aria_sort(col, @sort_field, @sort_order)}
|
aria-sort={table_th_aria_sort(col, @sort_field, @sort_order)}
|
||||||
>
|
>
|
||||||
{col[:label]}
|
{col[:label]}
|
||||||
</th>
|
</th>
|
||||||
<th :for={dyn_col <- @dynamic_cols}>
|
<th :for={dyn_col <- @dynamic_cols} class={table_th_sticky_class(@sticky_header)}>
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
id={:"sort_custom_field_#{dyn_col[:custom_field].id}"}
|
id={:"sort_custom_field_#{dyn_col[:custom_field].id}"}
|
||||||
|
|
@ -760,7 +765,7 @@ defmodule MvWeb.CoreComponents do
|
||||||
sort_order={@sort_order}
|
sort_order={@sort_order}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th :if={@action != []}>
|
<th :if={@action != []} class={table_th_sticky_class(@sticky_header)}>
|
||||||
<span class="sr-only">{gettext("Actions")}</span>
|
<span class="sr-only">{gettext("Actions")}</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -891,6 +896,18 @@ defmodule MvWeb.CoreComponents do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Combines column class with optional sticky header classes (desktop only; theme-friendly bg).
|
||||||
|
defp table_th_class(col, sticky_header) do
|
||||||
|
base = Map.get(col, :class)
|
||||||
|
sticky = if sticky_header, do: "lg:sticky lg:top-0 bg-base-100 z-10", else: nil
|
||||||
|
[base, sticky] |> Enum.filter(& &1) |> Enum.join(" ")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp table_th_sticky_class(true),
|
||||||
|
do: "lg:sticky lg:top-0 bg-base-100 z-10"
|
||||||
|
|
||||||
|
defp table_th_sticky_class(_), do: nil
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a data list.
|
Renders a data list.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,302 +90,311 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.table
|
<%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%>
|
||||||
id="members"
|
<div
|
||||||
rows={@members}
|
class="lg:max-h-[calc(100vh-14rem)] lg:overflow-auto min-h-0"
|
||||||
row_id={fn member -> "row-#{member.id}" end}
|
data-testid="members-table-scroll"
|
||||||
row_click={fn member -> JS.push("select_row_and_navigate", value: %{id: member.id}) end}
|
role="region"
|
||||||
row_tooltip={gettext("Click for member details")}
|
aria-label={gettext("Members table")}
|
||||||
row_selected?={fn member -> MapSet.member?(@selected_members, member.id) end}
|
|
||||||
dynamic_cols={@dynamic_cols}
|
|
||||||
sort_field={@sort_field}
|
|
||||||
sort_order={@sort_order}
|
|
||||||
>
|
>
|
||||||
|
<.table
|
||||||
|
id="members"
|
||||||
|
rows={@members}
|
||||||
|
sticky_header={true}
|
||||||
|
row_id={fn member -> "row-#{member.id}" end}
|
||||||
|
row_click={fn member -> JS.push("select_row_and_navigate", value: %{id: member.id}) end}
|
||||||
|
row_tooltip={gettext("Click for member details")}
|
||||||
|
row_selected?={fn member -> MapSet.member?(@selected_members, member.id) end}
|
||||||
|
dynamic_cols={@dynamic_cols}
|
||||||
|
sort_field={@sort_field}
|
||||||
|
sort_order={@sort_order}
|
||||||
|
>
|
||||||
|
|
||||||
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
||||||
<:col
|
<:col
|
||||||
:let={member}
|
:let={member}
|
||||||
col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1}
|
col_click={&MvWeb.MemberLive.Index.checkbox_column_click/1}
|
||||||
label={
|
label={
|
||||||
~H"""
|
~H"""
|
||||||
|
<.input
|
||||||
|
type="checkbox"
|
||||||
|
name="select_all"
|
||||||
|
phx-click="select_all"
|
||||||
|
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())}
|
||||||
|
aria-label={gettext("Select all members")}
|
||||||
|
role="checkbox"
|
||||||
|
/>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
>
|
||||||
<.input
|
<.input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="select_all"
|
name={member.id}
|
||||||
phx-click="select_all"
|
checked={MapSet.member?(@selected_members, member.id)}
|
||||||
checked={MapSet.equal?(@selected_members, @members |> Enum.map(& &1.id) |> MapSet.new())}
|
aria-label={gettext("Select member")}
|
||||||
aria-label={gettext("Select all members")}
|
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
/>
|
/>
|
||||||
"""
|
</:col>
|
||||||
}
|
<:col
|
||||||
>
|
:let={member}
|
||||||
<.input
|
:if={:first_name in @member_fields_visible}
|
||||||
type="checkbox"
|
label={
|
||||||
name={member.id}
|
~H"""
|
||||||
checked={MapSet.member?(@selected_members, member.id)}
|
<.live_component
|
||||||
aria-label={gettext("Select member")}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
role="checkbox"
|
id={:sort_first_name}
|
||||||
/>
|
field={:first_name}
|
||||||
</:col>
|
label={gettext("First name")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:first_name in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.first_name}
|
||||||
id={:sort_first_name}
|
</:col>
|
||||||
field={:first_name}
|
<:col
|
||||||
label={gettext("First name")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:last_name in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_last_name}
|
||||||
{member.first_name}
|
field={:last_name}
|
||||||
</:col>
|
label={gettext("Last name")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:last_name in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.last_name}
|
||||||
id={:sort_last_name}
|
</:col>
|
||||||
field={:last_name}
|
<:col
|
||||||
label={gettext("Last name")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:email in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_email}
|
||||||
{member.last_name}
|
field={:email}
|
||||||
</:col>
|
label={gettext("Email")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:email in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.email}
|
||||||
id={:sort_email}
|
</:col>
|
||||||
field={:email}
|
<:col
|
||||||
label={gettext("Email")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:join_date in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_join_date}
|
||||||
{member.email}
|
field={:join_date}
|
||||||
</:col>
|
label={gettext("Join Date")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:join_date in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{MvWeb.MemberLive.Index.format_date(member.join_date)}
|
||||||
id={:sort_join_date}
|
</:col>
|
||||||
field={:join_date}
|
<:col
|
||||||
label={gettext("Join Date")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:exit_date in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_exit_date}
|
||||||
{MvWeb.MemberLive.Index.format_date(member.join_date)}
|
field={:exit_date}
|
||||||
</:col>
|
label={gettext("Exit Date")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:exit_date in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{MvWeb.MemberLive.Index.format_date(member.exit_date)}
|
||||||
id={:sort_exit_date}
|
</:col>
|
||||||
field={:exit_date}
|
<:col
|
||||||
label={gettext("Exit Date")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:notes in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={gettext("Notes")}
|
||||||
/>
|
>
|
||||||
"""
|
{member.notes}
|
||||||
}
|
</:col>
|
||||||
>
|
<:col
|
||||||
{MvWeb.MemberLive.Index.format_date(member.exit_date)}
|
:let={member}
|
||||||
</:col>
|
:if={:city in @member_fields_visible}
|
||||||
<:col
|
label={
|
||||||
:let={member}
|
~H"""
|
||||||
:if={:notes in @member_fields_visible}
|
<.live_component
|
||||||
label={gettext("Notes")}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_city}
|
||||||
{member.notes}
|
field={:city}
|
||||||
</:col>
|
label={gettext("City")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:city in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.city}
|
||||||
id={:sort_city}
|
</:col>
|
||||||
field={:city}
|
<:col
|
||||||
label={gettext("City")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:street in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_street}
|
||||||
{member.city}
|
field={:street}
|
||||||
</:col>
|
label={gettext("Street")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:street in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.street}
|
||||||
id={:sort_street}
|
</:col>
|
||||||
field={:street}
|
<:col
|
||||||
label={gettext("Street")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:house_number in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_house_number}
|
||||||
{member.street}
|
field={:house_number}
|
||||||
</:col>
|
label={gettext("House Number")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:house_number in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.house_number}
|
||||||
id={:sort_house_number}
|
</:col>
|
||||||
field={:house_number}
|
<:col
|
||||||
label={gettext("House Number")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:postal_code in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_postal_code}
|
||||||
{member.house_number}
|
field={:postal_code}
|
||||||
</:col>
|
label={gettext("Postal Code")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:postal_code in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{member.postal_code}
|
||||||
id={:sort_postal_code}
|
</:col>
|
||||||
field={:postal_code}
|
<:col
|
||||||
label={gettext("Postal Code")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:membership_fee_start_date in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_membership_fee_start_date}
|
||||||
{member.postal_code}
|
field={:membership_fee_start_date}
|
||||||
</:col>
|
label={gettext("Membership Fee Start Date")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:membership_fee_start_date in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
|
||||||
id={:sort_membership_fee_start_date}
|
</:col>
|
||||||
field={:membership_fee_start_date}
|
<:col
|
||||||
label={gettext("Membership Fee Start Date")}
|
:let={member}
|
||||||
sort_field={@sort_field}
|
:if={:membership_fee_type in @member_fields_visible}
|
||||||
sort_order={@sort_order}
|
label={
|
||||||
/>
|
~H"""
|
||||||
"""
|
<.live_component
|
||||||
}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
>
|
id={:sort_membership_fee_type}
|
||||||
{MvWeb.MemberLive.Index.format_date(member.membership_fee_start_date)}
|
field={:membership_fee_type}
|
||||||
</:col>
|
label={gettext("Fee Type")}
|
||||||
<:col
|
sort_field={@sort_field}
|
||||||
:let={member}
|
sort_order={@sort_order}
|
||||||
:if={:membership_fee_type in @member_fields_visible}
|
/>
|
||||||
label={
|
"""
|
||||||
~H"""
|
}
|
||||||
<.live_component
|
>
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
<%= if member.membership_fee_type do %>
|
||||||
id={:sort_membership_fee_type}
|
{member.membership_fee_type.name}
|
||||||
field={:membership_fee_type}
|
<% else %>
|
||||||
label={gettext("Fee Type")}
|
<span class="text-base-content/50">—</span>
|
||||||
sort_field={@sort_field}
|
<% end %>
|
||||||
sort_order={@sort_order}
|
</:col>
|
||||||
/>
|
<:col
|
||||||
"""
|
:let={member}
|
||||||
}
|
:if={:membership_fee_status in @member_fields_visible}
|
||||||
>
|
label={gettext("Membership Fee Status")}
|
||||||
<%= if member.membership_fee_type do %>
|
>
|
||||||
{member.membership_fee_type.name}
|
<%= if badge = MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(
|
||||||
<% else %>
|
|
||||||
<span class="text-base-content/50">—</span>
|
|
||||||
<% end %>
|
|
||||||
</:col>
|
|
||||||
<:col
|
|
||||||
:let={member}
|
|
||||||
:if={:membership_fee_status in @member_fields_visible}
|
|
||||||
label={gettext("Membership Fee Status")}
|
|
||||||
>
|
|
||||||
<%= if badge = MvWeb.MemberLive.Index.MembershipFeeStatus.format_cycle_status_badge(
|
|
||||||
MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, @show_current_cycle)
|
MvWeb.MemberLive.Index.MembershipFeeStatus.get_cycle_status_for_member(member, @show_current_cycle)
|
||||||
) do %>
|
) do %>
|
||||||
<span class={["badge", badge.color]}>
|
<span class={["badge", badge.color]}>
|
||||||
<.icon name={badge.icon} class="size-4" />
|
<.icon name={badge.icon} class="size-4" />
|
||||||
{badge.label}
|
{badge.label}
|
||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="badge badge-ghost">{gettext("No cycle")}</span>
|
<span class="badge badge-ghost">{gettext("No cycle")}</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</:col>
|
</:col>
|
||||||
<:col
|
<:col
|
||||||
:let={member}
|
:let={member}
|
||||||
:if={:groups in @member_fields_visible}
|
:if={:groups in @member_fields_visible}
|
||||||
label={
|
label={
|
||||||
~H"""
|
~H"""
|
||||||
<.live_component
|
<.live_component
|
||||||
module={MvWeb.Components.SortHeaderComponent}
|
module={MvWeb.Components.SortHeaderComponent}
|
||||||
id={:sort_groups}
|
id={:sort_groups}
|
||||||
field={:groups}
|
field={:groups}
|
||||||
label={gettext("Groups")}
|
label={gettext("Groups")}
|
||||||
sort_field={@sort_field}
|
sort_field={@sort_field}
|
||||||
sort_order={@sort_order}
|
sort_order={@sort_order}
|
||||||
/>
|
/>
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<%= for group <- (member.groups || []) do %>
|
<%= for group <- (member.groups || []) do %>
|
||||||
<span
|
<span
|
||||||
class="badge badge-outline badge-primary"
|
class="badge badge-outline badge-primary"
|
||||||
aria-label={gettext("Member of group %{name}", name: group.name)}
|
aria-label={gettext("Member of group %{name}", name: group.name)}
|
||||||
>
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if (member.groups || []) == [] do %>
|
<%= if (member.groups || []) == [] do %>
|
||||||
<span class="text-base-content/50">—</span>
|
<span class="text-base-content/50">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</:col>
|
</:col>
|
||||||
<:action :let={member}>
|
<:action :let={member}>
|
||||||
<div class="sr-only">
|
<div class="sr-only">
|
||||||
<.link navigate={~p"/members/#{member}"} data-testid="member-show-link">
|
<.link navigate={~p"/members/#{member}"} data-testid="member-show-link">
|
||||||
{gettext("Show")}
|
{gettext("Show")}
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
</:action>
|
</:action>
|
||||||
</.table>
|
</.table>
|
||||||
|
</div>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,11 @@ defmodule MvWeb.RoleLive.Show do
|
||||||
{gettext("Back")}
|
{gettext("Back")}
|
||||||
</.button>
|
</.button>
|
||||||
<%= if can?(@current_user, :update, Mv.Authorization.Role) do %>
|
<%= if can?(@current_user, :update, Mv.Authorization.Role) do %>
|
||||||
<.button variant="primary" navigate={~p"/admin/roles/#{@role}/edit"} data-testid=role-edit">
|
<.button
|
||||||
|
variant="primary"
|
||||||
|
navigate={~p"/admin/roles/#{@role}/edit"}
|
||||||
|
data-testid="role-show-edit-btn"
|
||||||
|
>
|
||||||
{gettext("Edit role")}
|
{gettext("Edit role")}
|
||||||
</.button>
|
</.button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
||||||
|
|
@ -3116,6 +3116,7 @@ msgid "Edit member"
|
||||||
msgstr "Mitglied bearbeiten"
|
msgstr "Mitglied bearbeiten"
|
||||||
|
|
||||||
#: lib/mv_web/live/role_live/index.html.heex
|
#: lib/mv_web/live/role_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/role_live/show.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Edit role"
|
msgid "Edit role"
|
||||||
msgstr "Rolle bearbeiten"
|
msgstr "Rolle bearbeiten"
|
||||||
|
|
@ -3125,16 +3126,6 @@ msgstr "Rolle bearbeiten"
|
||||||
msgid "Edit user"
|
msgid "Edit user"
|
||||||
msgstr "Benutzer*in bearbeiten"
|
msgstr "Benutzer*in bearbeiten"
|
||||||
|
|
||||||
#: lib/mv_web/live/role_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Rolle bearbeiten"
|
|
||||||
msgstr "Rolle bearbeiten"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Click for custom field details"
|
|
||||||
msgstr "Klicke für Datenfeld-Details"
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Click for datafield details"
|
msgid "Click for datafield details"
|
||||||
|
|
@ -3160,11 +3151,26 @@ msgstr "Klicke für Rollen-Details"
|
||||||
msgid "Click for user details"
|
msgid "Click for user details"
|
||||||
msgstr "Klicke für Benutzer*innen-Details"
|
msgstr "Klicke für Benutzer*innen-Details"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Click for dataield details"
|
||||||
|
msgstr "Klicke für Datenfeld-Details"
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Members table"
|
||||||
|
msgstr "Mitglieder"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/member_field_live/form_component.ex
|
#~ #: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Back to Settings"
|
#~ msgid "Back to Settings"
|
||||||
#~ msgstr "Zurück zu den Einstellungen"
|
#~ msgstr "Zurück zu den Einstellungen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Click for custom field details"
|
||||||
|
#~ msgstr "Klicke für Datenfeld-Details"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/member_live/form.ex
|
#~ #: lib/mv_web/live/member_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Coming soon"
|
#~ msgid "Coming soon"
|
||||||
|
|
@ -3175,6 +3181,11 @@ msgstr "Klicke für Benutzer*innen-Details"
|
||||||
#~ msgid "Reset"
|
#~ msgid "Reset"
|
||||||
#~ msgstr "Zurücksetzen"
|
#~ msgstr "Zurücksetzen"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/role_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Rolle bearbeiten"
|
||||||
|
#~ msgstr "Rolle bearbeiten"
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/role_live/form.ex
|
#~ #: lib/mv_web/live/role_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Save Role"
|
#~ msgid "Save Role"
|
||||||
|
|
|
||||||
|
|
@ -3116,6 +3116,7 @@ msgid "Edit member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/role_live/index.html.heex
|
#: lib/mv_web/live/role_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/role_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Edit role"
|
msgid "Edit role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -3125,16 +3126,6 @@ msgstr ""
|
||||||
msgid "Edit user"
|
msgid "Edit user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/role_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Rolle bearbeiten"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Click for custom field details"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Click for datafield details"
|
msgid "Click for datafield details"
|
||||||
|
|
@ -3159,3 +3150,13 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Click for user details"
|
msgid "Click for user details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Click for dataield details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Members table"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -3116,6 +3116,7 @@ msgid "Edit member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/role_live/index.html.heex
|
#: lib/mv_web/live/role_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/role_live/show.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Edit role"
|
msgid "Edit role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -3125,16 +3126,6 @@ msgstr ""
|
||||||
msgid "Edit user"
|
msgid "Edit user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/role_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Rolle bearbeiten"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/custom_field_live/index_component.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Click for custom field details"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: lib/mv_web/live/member_field_live/index_component.ex
|
#: lib/mv_web/live/member_field_live/index_component.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Click for datafield details"
|
msgid "Click for datafield details"
|
||||||
|
|
@ -3160,11 +3151,26 @@ msgstr ""
|
||||||
msgid "Click for user details"
|
msgid "Click for user details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Click for dataield details"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
msgid "Members table"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/member_field_live/form_component.ex
|
#~ #: lib/mv_web/live/member_field_live/form_component.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Back to Settings"
|
#~ msgid "Back to Settings"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/custom_field_live/index_component.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Click for custom field details"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/member_live/form.ex
|
#~ #: lib/mv_web/live/member_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format
|
#~ #, elixir-autogen, elixir-format
|
||||||
#~ msgid "Coming soon"
|
#~ msgid "Coming soon"
|
||||||
|
|
@ -3175,6 +3181,11 @@ msgstr ""
|
||||||
#~ msgid "Reset"
|
#~ msgid "Reset"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/role_live/show.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Rolle bearbeiten"
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/role_live/form.ex
|
#~ #: lib/mv_web/live/role_live/form.ex
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
#~ msgid "Save Role"
|
#~ msgid "Save Role"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,35 @@ defmodule MvWeb.MemberLive.IndexTest do
|
||||||
|> Ash.create!(actor: actor)
|
|> Ash.create!(actor: actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "desktop layout: scroll container and sticky table header" do
|
||||||
|
@describetag :ui
|
||||||
|
|
||||||
|
test "header and filters are outside scroll container; table is in scroll container with lg:max-h and lg:overflow-auto",
|
||||||
|
%{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/members")
|
||||||
|
|
||||||
|
assert html =~ ~r/data-testid="members-table-scroll"/
|
||||||
|
# Scroll container has lg: overflow and max-height for desktop-only scroll
|
||||||
|
assert html =~ "lg:overflow-auto"
|
||||||
|
assert html =~ "lg:max-h-[calc(100vh-14rem)]"
|
||||||
|
|
||||||
|
# Header (page title) is present and not inside the scroll container (scroll container comes after filters)
|
||||||
|
assert html =~ "Members"
|
||||||
|
assert html =~ "id=\"members\""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "table thead has sticky classes on desktop when sticky_header is set", %{conn: conn} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/members")
|
||||||
|
|
||||||
|
# CoreComponents table with sticky_header adds lg:sticky lg:top-0 bg-base-100 z-10 to th
|
||||||
|
assert html =~ "lg:sticky"
|
||||||
|
assert html =~ "lg:top-0"
|
||||||
|
assert html =~ "bg-base-100"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "translations" do
|
describe "translations" do
|
||||||
@describetag :ui
|
@describetag :ui
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue