style: consistent save buttons and active tab
This commit is contained in:
parent
82962a2f2a
commit
ba08434604
15 changed files with 486 additions and 438 deletions
|
|
@ -46,14 +46,14 @@ Every authenticated page should follow the same structure:
|
|||
**MUST:** Use `<.header>` on every page (except login/public pages).
|
||||
**SHOULD:** Put short explanations into `<:subtitle>` rather than sprinkling random text blocks.
|
||||
|
||||
### 2.2 Edit/New form header: Back button left (mandatory)
|
||||
### 2.2 Edit/New form header and footer buttons (mandatory)
|
||||
|
||||
For LiveViews that render an edit or new form (e.g. member, group, role, user, custom field, membership fee type):
|
||||
|
||||
- **MUST:** Provide a **Back** button on the **left** side of the header using the `<:leading>` slot (same as data fields: Back left, title next, primary action on the right).
|
||||
- **MUST:** Provide a **Back** button on the **left** side of the header using the `<:leading>` slot (same as data fields: Back left, title next).
|
||||
- **MUST:** Use the same pattern everywhere: Back button with `variant="neutral"`, arrow-left icon, and label “Back”. It navigates to the previous context (e.g. detail page or index) via a `return_path`-style helper.
|
||||
- **SHOULD:** Place the primary action (e.g. “Save”) in `<:actions>` on the right.
|
||||
- **Rationale:** Users expect a consistent way to leave the form without submitting; Back left matches the data fields edit view and keeps primary actions on the right.
|
||||
- **MUST:** Place **exactly one** form button bar **below all form fields**, inside the `<.form>`, with: **Abbrechen** (Cancel) left, **Speichern** (Save) right. Use `gettext("Cancel")`, `gettext("Save <ressourcename>")`, `phx-disable-with={gettext("Saving...")}` on the submit button. No submit button in the header; no duplicate submit buttons.
|
||||
- **Rationale:** Users expect a consistent way to leave the form without submitting; Back left. One primary action (Save) per form, in the footer, avoids double submits and matches the reference (member edit form).
|
||||
|
||||
**Template for form pages:**
|
||||
```heex
|
||||
|
|
@ -66,15 +66,20 @@ For LiveViews that render an edit or new form (e.g. member, group, role, user, c
|
|||
</:leading>
|
||||
Page title (e.g. “Edit Member” or “New User”)
|
||||
<:subtitle>Short explanation.</:subtitle>
|
||||
<:actions>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
||||
{gettext("Save")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
```
|
||||
|
||||
If the `<.header>` is outside the `<.form>`, the submit button must reference the form via the `form` attribute (e.g. `form="user-form"`).
|
||||
<.form for={@form} id="..." phx-change="validate" phx-submit="save">
|
||||
<%!-- form sections and fields --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button navigate={return_path(@return_to, @resource)} variant="neutral" type="button">
|
||||
{gettext("Abbrechen")}
|
||||
</.button>
|
||||
<.button type="submit" phx-disable-with={gettext("Speichern...")} variant="primary">
|
||||
{gettext("Speichern")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
```
|
||||
|
||||
## 3) Typography (system)
|
||||
|
||||
|
|
|
|||
|
|
@ -577,7 +577,9 @@
|
|||
}
|
||||
|
||||
/* ============================================
|
||||
WCAG 2.2 AA: Tab list inactive tab text contrast (4.5:1)
|
||||
Member detail tabs (show + edit): inactive vs active contrast
|
||||
WCAG 2.2 AA: inactive tab text contrast (4.5:1)
|
||||
Active tab: visible border (DaisyUI tabs-bordered) and weight so which tab is selected is clear.
|
||||
============================================ */
|
||||
#member-tablist .tab:not(.tab-active) {
|
||||
color: oklch(0.35 0.02 285);
|
||||
|
|
@ -586,6 +588,13 @@
|
|||
color: oklch(0.72 0.02 257);
|
||||
}
|
||||
|
||||
/* Active tab: stronger underline (DaisyUI --tab-border-color) and font weight */
|
||||
#member-tablist .tab.tab-active,
|
||||
#member-tablist .tab[aria-selected="true"] {
|
||||
--tab-border-color: var(--color-base-content);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
WCAG 2.2 AA: Link contrast - primary and accent
|
||||
============================================ */
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do
|
|||
id="field-visibility-menu"
|
||||
icon="hero-adjustments-horizontal"
|
||||
button_label={gettext("Show/Hide Columns")}
|
||||
button_class="btn-secondary"
|
||||
items={@all_items}
|
||||
checkboxes={true}
|
||||
selected={@selected_fields}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
|||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id={@id} class="mb-8 border shadow-xl card border-base-300">
|
||||
<div id={@id} class="mb-8 max-w-2xl border shadow-xl card border-base-300">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<.button
|
||||
|
|
@ -98,6 +98,20 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
|||
label={gettext("Show in overview")}
|
||||
/>
|
||||
|
||||
<%!-- Buttons: below all form fields, above Danger zone --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button type="button" variant="neutral" phx-click="cancel" phx-target={@myself}>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
>
|
||||
{gettext("Save Data Field")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<%= if @custom_field do %>
|
||||
<%!-- Danger zone: canonical pattern (same as member form) --%>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
|
|
@ -125,15 +139,6 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
|
|||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div class="justify-end mt-4 card-actions">
|
||||
<.button type="button" variant="neutral" phx-click="cancel" phx-target={@myself}>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Data Field")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ defmodule MvWeb.GroupLive.Form do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.form for={@form} id="group-form" phx-change="validate" phx-submit="save">
|
||||
<.form class="max-w-2xl" for={@form} id="group-form" phx-change="validate" phx-submit="save">
|
||||
<.header>
|
||||
<:leading>
|
||||
<.button navigate={return_path(@return_to, @group)} variant="neutral">
|
||||
|
|
@ -86,11 +86,6 @@ defmodule MvWeb.GroupLive.Form do
|
|||
</.button>
|
||||
</:leading>
|
||||
{@page_title}
|
||||
<:actions>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
||||
{gettext("Save")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div class="mt-6 space-y-6">
|
||||
|
|
@ -104,6 +99,20 @@ defmodule MvWeb.GroupLive.Form do
|
|||
/>
|
||||
</div>
|
||||
|
||||
<%!-- Buttons: below all form fields, Abbrechen left (secondary), Speichern right (primary) --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button navigate={return_path(@return_to, @group)} variant="neutral" type="button">
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
>
|
||||
{gettext("Save group")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<%!-- Danger zone: canonical pattern (same as member form) --%>
|
||||
<%= if @group && can?(@current_user, :destroy, @group) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
|
|
|
|||
|
|
@ -118,259 +118,261 @@ defmodule MvWeb.GroupLive.Show do
|
|||
|
||||
<div
|
||||
id="group-show-focus-root"
|
||||
class="mt-6 space-y-6"
|
||||
class="mt-6"
|
||||
phx-hook="FocusRestore"
|
||||
phx-window-keydown={if @show_delete_modal, do: "window_keydown", else: nil}
|
||||
>
|
||||
<%!-- Group Information --%>
|
||||
<div class="max-w-2xl space-y-6 mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Description")}</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<%= if @group.description && String.trim(@group.description) != "" do %>
|
||||
<p class="whitespace-pre-wrap">{@group.description}</p>
|
||||
<% else %>
|
||||
<p class="text-base-content/50 italic">{gettext("No description")}</p>
|
||||
<% end %>
|
||||
<div class="max-w-2xl space-y-6">
|
||||
<%!-- Group Information --%>
|
||||
<div class="space-y-6 mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Description")}</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<%= if @group.description && String.trim(@group.description) != "" do %>
|
||||
<p class="whitespace-pre-wrap">{@group.description}</p>
|
||||
<% else %>
|
||||
<p class="text-base-content/50 italic">{gettext("No description")}</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Members")}</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="mb-4" data-testid="group-show-member-count">
|
||||
{ngettext(
|
||||
"Total: %{count} member",
|
||||
"Total: %{count} members",
|
||||
@group.member_count || 0,
|
||||
count: @group.member_count || 0
|
||||
)}
|
||||
</p>
|
||||
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<div class="mb-4">
|
||||
<%= if assigns[:show_add_member_input] do %>
|
||||
<div class="join w-full">
|
||||
<form phx-change="search_members" class="flex-1">
|
||||
<div class="relative">
|
||||
<div class="input input-bordered join-item w-full flex flex-wrap gap-1 items-center py-1 px-2">
|
||||
<%= for member <- @selected_members do %>
|
||||
<.badge
|
||||
variant="primary"
|
||||
style="outline"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
<.tooltip content={gettext("Remove")} position="top">
|
||||
<.button
|
||||
type="button"
|
||||
variant="icon"
|
||||
size="sm"
|
||||
phx-click="remove_selected_member"
|
||||
phx-value-member_id={member.id}
|
||||
aria-label={
|
||||
gettext("Remove %{name}",
|
||||
name: MvWeb.Helpers.MemberHelpers.display_name(member)
|
||||
)
|
||||
}
|
||||
class="p-0 h-4 w-4 min-h-0"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="size-3" />
|
||||
</.button>
|
||||
</.tooltip>
|
||||
</.badge>
|
||||
<% end %>
|
||||
<input
|
||||
type="text"
|
||||
id="member-search-input"
|
||||
data-testid="group-show-member-search-input"
|
||||
role="combobox"
|
||||
phx-hook="ComboBox"
|
||||
phx-focus="show_member_dropdown"
|
||||
phx-debounce="300"
|
||||
phx-keydown="member_dropdown_keydown"
|
||||
phx-mounted={JS.focus()}
|
||||
value={@member_search_query}
|
||||
placeholder={
|
||||
if Enum.empty?(@selected_members),
|
||||
do: gettext("Search for a member..."),
|
||||
else: ""
|
||||
}
|
||||
class="flex-1 min-w-[120px] border-0 focus:outline-none bg-transparent"
|
||||
name="member_search"
|
||||
aria-label={gettext("Search for a member")}
|
||||
aria-autocomplete="list"
|
||||
aria-controls="member-dropdown"
|
||||
aria-expanded={to_string(@show_member_dropdown)}
|
||||
aria-activedescendant={
|
||||
if @focused_member_index,
|
||||
do: "member-option-#{@focused_member_index}",
|
||||
else: nil
|
||||
}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<%= if length(@available_members) > 0 do %>
|
||||
<div
|
||||
id="member-dropdown"
|
||||
role="listbox"
|
||||
aria-label={gettext("Available members")}
|
||||
class={"absolute z-10 w-full mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg max-h-60 overflow-auto top-full #{if !@show_member_dropdown, do: "hidden"}"}
|
||||
phx-click-away="hide_member_dropdown"
|
||||
>
|
||||
<%= for {member, index} <- Enum.with_index(@available_members) do %>
|
||||
<div
|
||||
id={"member-option-#{index}"}
|
||||
role="option"
|
||||
tabindex="0"
|
||||
aria-selected={to_string(@focused_member_index == index)}
|
||||
phx-click="select_member"
|
||||
phx-value-id={member.id}
|
||||
data-member-id={member.id}
|
||||
class={[
|
||||
"px-4 py-3 cursor-pointer border-b border-base-300 last:border-b-0",
|
||||
if(@focused_member_index == index,
|
||||
do: "bg-base-300",
|
||||
else: "hover:bg-base-200"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<p class="font-medium">
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{member.email || gettext("No email")}
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</form>
|
||||
<.button
|
||||
type="button"
|
||||
variant="primary"
|
||||
phx-click="add_selected_members"
|
||||
data-testid="group-show-add-selected-members-btn"
|
||||
disabled={Enum.empty?(@selected_member_ids)}
|
||||
aria-label={gettext("Add members")}
|
||||
class="join-item"
|
||||
>
|
||||
<.icon name="hero-plus" class="size-5" />
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
variant="neutral"
|
||||
phx-click="hide_add_member_input"
|
||||
aria-label={gettext("Cancel")}
|
||||
class="join-item"
|
||||
>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
</div>
|
||||
<% else %>
|
||||
<.button
|
||||
variant="primary"
|
||||
phx-click="show_add_member_input"
|
||||
aria-label={gettext("Add Member")}
|
||||
>
|
||||
{gettext("Add Member")}
|
||||
</.button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if Enum.empty?(@group.members || []) do %>
|
||||
<p class="text-base-content/50 italic" data-testid="group-show-no-members">
|
||||
{gettext("No members in this group")}
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Members")}</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="mb-4" data-testid="group-show-member-count">
|
||||
{ngettext(
|
||||
"Total: %{count} member",
|
||||
"Total: %{count} members",
|
||||
@group.member_count || 0,
|
||||
count: @group.member_count || 0
|
||||
)}
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto" data-testid="group-show-members-table">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{gettext("Name")}</th>
|
||||
<th>{gettext("Email")}</th>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<th class="w-0">{gettext("Actions")}</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for member <- @group.members do %>
|
||||
<tr>
|
||||
<td>
|
||||
<.link
|
||||
navigate={~p"/members/#{member.id}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<.maybe_value value={member.email} empty_sr_text={gettext("No email")}>
|
||||
<a
|
||||
href={"mailto:#{member.email}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{member.email}
|
||||
</a>
|
||||
</.maybe_value>
|
||||
</td>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<td>
|
||||
<.tooltip content={gettext("Remove")} position="left">
|
||||
<.button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
phx-click="remove_member"
|
||||
phx-value-member_id={member.id}
|
||||
data-testid="group-show-remove-member"
|
||||
aria-label={gettext("Remove member from group")}
|
||||
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<div class="mb-4">
|
||||
<%= if assigns[:show_add_member_input] do %>
|
||||
<div class="join w-full">
|
||||
<form phx-change="search_members" class="flex-1">
|
||||
<div class="relative">
|
||||
<div class="input input-bordered join-item w-full flex flex-wrap gap-1 items-center py-1 px-2">
|
||||
<%= for member <- @selected_members do %>
|
||||
<.badge
|
||||
variant="primary"
|
||||
style="outline"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
</.button>
|
||||
</.tooltip>
|
||||
</td>
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
<.tooltip content={gettext("Remove")} position="top">
|
||||
<.button
|
||||
type="button"
|
||||
variant="icon"
|
||||
size="sm"
|
||||
phx-click="remove_selected_member"
|
||||
phx-value-member_id={member.id}
|
||||
aria-label={
|
||||
gettext("Remove %{name}",
|
||||
name: MvWeb.Helpers.MemberHelpers.display_name(member)
|
||||
)
|
||||
}
|
||||
class="p-0 h-4 w-4 min-h-0"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="size-3" />
|
||||
</.button>
|
||||
</.tooltip>
|
||||
</.badge>
|
||||
<% end %>
|
||||
<input
|
||||
type="text"
|
||||
id="member-search-input"
|
||||
data-testid="group-show-member-search-input"
|
||||
role="combobox"
|
||||
phx-hook="ComboBox"
|
||||
phx-focus="show_member_dropdown"
|
||||
phx-debounce="300"
|
||||
phx-keydown="member_dropdown_keydown"
|
||||
phx-mounted={JS.focus()}
|
||||
value={@member_search_query}
|
||||
placeholder={
|
||||
if Enum.empty?(@selected_members),
|
||||
do: gettext("Search for a member..."),
|
||||
else: ""
|
||||
}
|
||||
class="flex-1 min-w-[120px] border-0 focus:outline-none bg-transparent"
|
||||
name="member_search"
|
||||
aria-label={gettext("Search for a member")}
|
||||
aria-autocomplete="list"
|
||||
aria-controls="member-dropdown"
|
||||
aria-expanded={to_string(@show_member_dropdown)}
|
||||
aria-activedescendant={
|
||||
if @focused_member_index,
|
||||
do: "member-option-#{@focused_member_index}",
|
||||
else: nil
|
||||
}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<%= if length(@available_members) > 0 do %>
|
||||
<div
|
||||
id="member-dropdown"
|
||||
role="listbox"
|
||||
aria-label={gettext("Available members")}
|
||||
class={"absolute z-10 w-full mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg max-h-60 overflow-auto top-full #{if !@show_member_dropdown, do: "hidden"}"}
|
||||
phx-click-away="hide_member_dropdown"
|
||||
>
|
||||
<%= for {member, index} <- Enum.with_index(@available_members) do %>
|
||||
<div
|
||||
id={"member-option-#{index}"}
|
||||
role="option"
|
||||
tabindex="0"
|
||||
aria-selected={to_string(@focused_member_index == index)}
|
||||
phx-click="select_member"
|
||||
phx-value-id={member.id}
|
||||
data-member-id={member.id}
|
||||
class={[
|
||||
"px-4 py-3 cursor-pointer border-b border-base-300 last:border-b-0",
|
||||
if(@focused_member_index == index,
|
||||
do: "bg-base-300",
|
||||
else: "hover:bg-base-200"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<p class="font-medium">
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/70">
|
||||
{member.email || gettext("No email")}
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</form>
|
||||
<.button
|
||||
type="button"
|
||||
variant="primary"
|
||||
phx-click="add_selected_members"
|
||||
data-testid="group-show-add-selected-members-btn"
|
||||
disabled={Enum.empty?(@selected_member_ids)}
|
||||
aria-label={gettext("Add members")}
|
||||
class="join-item"
|
||||
>
|
||||
<.icon name="hero-plus" class="size-5" />
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
variant="neutral"
|
||||
phx-click="hide_add_member_input"
|
||||
aria-label={gettext("Cancel")}
|
||||
class="join-item"
|
||||
>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
</div>
|
||||
<% else %>
|
||||
<.button
|
||||
variant="primary"
|
||||
phx-click="show_add_member_input"
|
||||
aria-label={gettext("Add Member")}
|
||||
>
|
||||
{gettext("Add Member")}
|
||||
</.button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if Enum.empty?(@group.members || []) do %>
|
||||
<p class="text-base-content/50 italic" data-testid="group-show-no-members">
|
||||
{gettext("No members in this group")}
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto" data-testid="group-show-members-table">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{gettext("Name")}</th>
|
||||
<th>{gettext("Email")}</th>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<th class="w-0">{gettext("Actions")}</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for member <- @group.members do %>
|
||||
<tr>
|
||||
<td>
|
||||
<.link
|
||||
navigate={~p"/members/#{member.id}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<.maybe_value value={member.email} empty_sr_text={gettext("No email")}>
|
||||
<a
|
||||
href={"mailto:#{member.email}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{member.email}
|
||||
</a>
|
||||
</.maybe_value>
|
||||
</td>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<td>
|
||||
<.tooltip content={gettext("Remove")} position="left">
|
||||
<.button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
phx-click="remove_member"
|
||||
phx-value-member_id={member.id}
|
||||
data-testid="group-show-remove-member"
|
||||
aria-label={gettext("Remove member from group")}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
</.button>
|
||||
</.tooltip>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
||||
<%= if can?(@current_user, :destroy, @group) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this group cannot be undone. All member-group associations will be permanently removed."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-group-trigger"
|
||||
variant="danger"
|
||||
type="button"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="group-show-delete-btn"
|
||||
aria-label={gettext("Delete group %{name}", name: @group.name)}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete group")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
<%!-- Danger zone: same width as form (max-w-2xl) --%>
|
||||
<%= if can?(@current_user, :destroy, @group) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this group cannot be undone. All member-group associations will be permanently removed."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-group-trigger"
|
||||
variant="danger"
|
||||
type="button"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="group-show-delete-btn"
|
||||
aria-label={gettext("Delete group %{name}", name: @group.name)}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete group")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%!-- Delete Confirmation Modal (WCAG: focus in modal, aria-labelledby) --%>
|
||||
<%= if assigns[:show_delete_modal] do %>
|
||||
|
|
|
|||
|
|
@ -166,12 +166,17 @@ defmodule MvWeb.MemberFieldLive.FormComponent do
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="justify-end mt-4 card-actions">
|
||||
<%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button type="button" variant="neutral" phx-click="cancel" phx-target={@myself}>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Field")}
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
>
|
||||
{gettext("Save datafield")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ defmodule MvWeb.MemberLive.Form do
|
|||
<div class="mt-6 space-y-6">
|
||||
<%!-- Tab navigation: same styling as member show; only Contact Data tab (no Membership Fees on edit) --%>
|
||||
<div
|
||||
id="member-tablist"
|
||||
role="tablist"
|
||||
class="tabs tabs-bordered tabs-lg bg-base-200/60 rounded-box p-1 w-fit"
|
||||
>
|
||||
|
|
@ -259,13 +260,17 @@ defmodule MvWeb.MemberLive.Form do
|
|||
</.form_section>
|
||||
</div>
|
||||
|
||||
<%!-- Bottom Action Buttons --%>
|
||||
<div class="flex justify-end gap-4 mt-6">
|
||||
<.button navigate={return_path(@return_to, @member)} variant="neutral" type="button">
|
||||
<%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.link navigate={return_path(@return_to, @member)} class="btn btn-secondary">
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
||||
{gettext("Save Member")}
|
||||
</.link>
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
{gettext("Save member")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@
|
|||
query={@query}
|
||||
placeholder={gettext("Search...")}
|
||||
/>
|
||||
<.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.MemberFilterComponent}
|
||||
id="member-filter"
|
||||
|
|
@ -86,13 +93,6 @@
|
|||
</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>
|
||||
|
||||
<%!-- On desktop (lg:), only the table area scrolls; header and filters stay visible. On mobile, normal flow. --%>
|
||||
|
|
|
|||
|
|
@ -144,11 +144,6 @@ defmodule MvWeb.MembershipFeeSettingsLive do
|
|||
<:subtitle>
|
||||
{gettext("Configure fee types for membership fees.")}
|
||||
</:subtitle>
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}>
|
||||
<.icon name="hero-plus" /> {gettext("New Membership Fee Type")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<%!-- One card: default setting + fee types table --%>
|
||||
|
|
@ -220,13 +215,6 @@ defmodule MvWeb.MembershipFeeSettingsLive do
|
|||
<% end %>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
||||
<div class="flex-shrink-0 ml-auto border-l border-base-300 pl-6">
|
||||
<.button type="submit" variant="primary">
|
||||
<.icon name="hero-check" class="size-5" />
|
||||
{gettext("Save Settings")}
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="text-sm text-base-content/60 list-disc list-inside space-y-0.5">
|
||||
|
|
@ -237,12 +225,24 @@ defmodule MvWeb.MembershipFeeSettingsLive do
|
|||
)}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<%!-- Save button: below default settings form, no icon (consistent with other Save buttons) --%>
|
||||
<div class="mt-6 flex items-center justify-end">
|
||||
<.button type="submit" phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save default settings")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<%!-- Fee types table: row click opens edit --%>
|
||||
<h2 class="text-lg font-semibold">{gettext("Membership Fee Types")}</h2>
|
||||
<%!-- Fee types section: heading and "New" button on same line --%>
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold">{gettext("Membership Fee Types")}</h2>
|
||||
<.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}>
|
||||
<.icon name="hero-plus" /> {gettext("New Membership Fee Type")}
|
||||
</.button>
|
||||
</div>
|
||||
<.table
|
||||
id="membership_fee_types"
|
||||
rows={@membership_fee_types}
|
||||
|
|
|
|||
|
|
@ -34,16 +34,6 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
|
|||
</.button>
|
||||
</:leading>
|
||||
{@page_title}
|
||||
<:actions>
|
||||
<.button
|
||||
form="membership-fee-type-form"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
{gettext("Save")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.form
|
||||
|
|
@ -139,13 +129,22 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
|
|||
rows="3"
|
||||
/>
|
||||
|
||||
<div class="mt-4">
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
||||
{gettext("Save Membership Fee Type")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @membership_fee_type)} type="button">
|
||||
<%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button
|
||||
navigate={return_path(@return_to, @membership_fee_type)}
|
||||
variant="neutral"
|
||||
type="button"
|
||||
>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
>
|
||||
{gettext("Save membership fee type")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,6 @@ defmodule MvWeb.RoleLive.Form do
|
|||
</.button>
|
||||
</:leading>
|
||||
{@page_title}
|
||||
<:actions>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
||||
{gettext("Save")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div class="mt-6 space-y-6">
|
||||
|
|
@ -85,6 +80,20 @@ defmodule MvWeb.RoleLive.Form do
|
|||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Buttons: below all form fields, Cancel left (secondary), Speichern right (primary) --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button navigate={return_path(@return_to, @role)} variant="neutral" type="button">
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
>
|
||||
{gettext("Save role")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -223,55 +223,57 @@ defmodule MvWeb.RoleLive.Show do
|
|||
phx-hook="FocusRestore"
|
||||
phx-window-keydown={if @show_delete_modal, do: "window_keydown", else: nil}
|
||||
>
|
||||
<.list>
|
||||
<:item title={gettext("Name")}>{@role.name}</:item>
|
||||
<:item title={gettext("Description")}>
|
||||
<%= if @role.description do %>
|
||||
{@role.description}
|
||||
<% else %>
|
||||
<span class="text-base-content/70 italic">{gettext("No description")}</span>
|
||||
<% end %>
|
||||
</:item>
|
||||
<:item title={gettext("Permission Set")}>
|
||||
<.badge variant={permission_set_badge_variant(@role.permission_set_name)}>
|
||||
{@role.permission_set_name}
|
||||
</.badge>
|
||||
</:item>
|
||||
<:item title={gettext("System Role")}>
|
||||
<.badge :if={@role.is_system_role} variant="warning">
|
||||
{gettext("Yes")}
|
||||
</.badge>
|
||||
<.badge :if={!@role.is_system_role} variant="neutral">
|
||||
{gettext("No")}
|
||||
</.badge>
|
||||
</:item>
|
||||
</.list>
|
||||
<div class="max-w-xl mt-6 space-y-6">
|
||||
<.list>
|
||||
<:item title={gettext("Name")}>{@role.name}</:item>
|
||||
<:item title={gettext("Description")}>
|
||||
<%= if @role.description do %>
|
||||
{@role.description}
|
||||
<% else %>
|
||||
<span class="text-base-content/70 italic">{gettext("No description")}</span>
|
||||
<% end %>
|
||||
</:item>
|
||||
<:item title={gettext("Permission Set")}>
|
||||
<.badge variant={permission_set_badge_variant(@role.permission_set_name)}>
|
||||
{@role.permission_set_name}
|
||||
</.badge>
|
||||
</:item>
|
||||
<:item title={gettext("System Role")}>
|
||||
<.badge :if={@role.is_system_role} variant="warning">
|
||||
{gettext("Yes")}
|
||||
</.badge>
|
||||
<.badge :if={!@role.is_system_role} variant="neutral">
|
||||
{gettext("No")}
|
||||
</.badge>
|
||||
</:item>
|
||||
</.list>
|
||||
|
||||
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
||||
<%= if can?(@current_user, :destroy, Mv.Authorization.Role) and not @role.is_system_role do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this role cannot be undone. Users assigned to this role must be reassigned first."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-role-trigger"
|
||||
variant="danger"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="role-delete"
|
||||
aria-label={gettext("Delete role %{name}", name: @role.name)}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete role")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
||||
<%= if can?(@current_user, :destroy, Mv.Authorization.Role) and not @role.is_system_role do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this role cannot be undone. Users assigned to this role must be reassigned first."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-role-trigger"
|
||||
variant="danger"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="role-delete"
|
||||
aria-label={gettext("Delete role %{name}", name: @role.name)}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete role")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%!-- Delete Role Confirmation Modal (WCAG: focus moves into modal, keyboard confirm/cancel) --%>
|
||||
<%= if assigns[:show_delete_modal] do %>
|
||||
|
|
|
|||
|
|
@ -60,16 +60,6 @@ defmodule MvWeb.UserLive.Form do
|
|||
</.button>
|
||||
</:leading>
|
||||
{@page_title}
|
||||
<:actions>
|
||||
<.button
|
||||
form="user-form"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
{gettext("Save User")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div
|
||||
|
|
@ -309,6 +299,20 @@ defmodule MvWeb.UserLive.Form do
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Buttons: below all form fields, above Danger zone --%>
|
||||
<div class="mt-6 flex items-center justify-end gap-4">
|
||||
<.button navigate={return_path(@return_to, @user)} variant="neutral" type="button">
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button
|
||||
type="submit"
|
||||
phx-disable-with={gettext("Saving...")}
|
||||
variant="primary"
|
||||
>
|
||||
{gettext("Save user")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<%!-- Danger zone: canonical pattern (same as member form) --%>
|
||||
<%= if @user && can?(@current_user, :destroy, @user) && !SystemActor.system_user?(@user) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
|
|
@ -378,15 +382,6 @@ defmodule MvWeb.UserLive.Form do
|
|||
</div>
|
||||
</dialog>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4">
|
||||
<.button navigate={return_path(@return_to, @user)} variant="neutral">
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save User")}
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -67,59 +67,61 @@ defmodule MvWeb.UserLive.Show do
|
|||
phx-hook="FocusRestore"
|
||||
phx-window-keydown={if @show_delete_modal, do: "window_keydown", else: nil}
|
||||
>
|
||||
<.list>
|
||||
<:item title={gettext("Email")}>{@user.email}</:item>
|
||||
<:item title={gettext("Role")}>{@user.role.name}</:item>
|
||||
<:item title={gettext("Password Authentication")}>
|
||||
{if MvWeb.Helpers.UserHelpers.has_password?(@user),
|
||||
do: gettext("Enabled"),
|
||||
else: gettext("Not enabled")}
|
||||
</:item>
|
||||
<:item title={gettext("OIDC")}>
|
||||
{if MvWeb.Helpers.UserHelpers.has_oidc?(@user),
|
||||
do: gettext("Linked"),
|
||||
else: gettext("Not linked")}
|
||||
</:item>
|
||||
<:item title={gettext("Linked Member")}>
|
||||
<%= if @user.member do %>
|
||||
<.link
|
||||
navigate={~p"/members/#{@user.member}"}
|
||||
class="text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
<.icon name="hero-users" class="inline w-4 h-4 mr-1" />
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
|
||||
</.link>
|
||||
<% else %>
|
||||
<span class="italic text-base-content/70">{gettext("No member linked")}</span>
|
||||
<% end %>
|
||||
</:item>
|
||||
</.list>
|
||||
<div class="max-w-xl mt-6 space-y-6">
|
||||
<.list>
|
||||
<:item title={gettext("Email")}>{@user.email}</:item>
|
||||
<:item title={gettext("Role")}>{@user.role.name}</:item>
|
||||
<:item title={gettext("Password Authentication")}>
|
||||
{if MvWeb.Helpers.UserHelpers.has_password?(@user),
|
||||
do: gettext("Enabled"),
|
||||
else: gettext("Not enabled")}
|
||||
</:item>
|
||||
<:item title={gettext("OIDC")}>
|
||||
{if MvWeb.Helpers.UserHelpers.has_oidc?(@user),
|
||||
do: gettext("Linked"),
|
||||
else: gettext("Not linked")}
|
||||
</:item>
|
||||
<:item title={gettext("Linked Member")}>
|
||||
<%= if @user.member do %>
|
||||
<.link
|
||||
navigate={~p"/members/#{@user.member}"}
|
||||
class="text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
<.icon name="hero-users" class="inline w-4 h-4 mr-1" />
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(@user.member)}
|
||||
</.link>
|
||||
<% else %>
|
||||
<span class="italic text-base-content/70">{gettext("No member linked")}</span>
|
||||
<% end %>
|
||||
</:item>
|
||||
</.list>
|
||||
|
||||
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
||||
<%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this user cannot be undone. The user account and any linked member association will be affected."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-user-trigger"
|
||||
variant="danger"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="user-delete"
|
||||
aria-label={gettext("Delete user %{email}", email: @user.email)}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete user")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
||||
<%= if can?(@current_user, :destroy, @user) and not Mv.Helpers.SystemActor.system_user?(@user) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this user cannot be undone. The user account and any linked member association will be affected."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-user-trigger"
|
||||
variant="danger"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="user-delete"
|
||||
aria-label={gettext("Delete user %{email}", email: @user.email)}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete user")}
|
||||
</.button>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%!-- Delete User Confirmation Modal (WCAG: focus in modal, keyboard confirm/cancel) --%>
|
||||
<%= if assigns[:show_delete_modal] do %>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue