style: consistent back button and some translations
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
carla 2026-02-25 16:25:13 +01:00
parent 91cf7cca6a
commit 0f12befd11
26 changed files with 747 additions and 710 deletions

View file

@ -188,7 +188,7 @@ defmodule MvWeb.Components.FieldVisibilityDropdownComponent do
defp find_custom_field_name(id, _field_string, custom_fields) do
case Enum.find(custom_fields, fn cf -> to_string(cf.id) == id end) do
nil -> gettext("Custom Field %{id}", id: id)
nil -> gettext("Datafield %{id}", id: id)
custom_field -> custom_field.name
end
end

View file

@ -252,7 +252,7 @@ defmodule MvWeb.Components.MemberFilterComponent do
<!-- Custom Fields Group -->
<div :if={length(@boolean_custom_fields) > 0} class="mb-2">
<div class="text-xs font-semibold opacity-70 mb-2 uppercase tracking-wider">
{gettext("Custom Fields")}
{gettext("Individual datafields")}
</div>
<div class="max-h-60 overflow-y-auto pr-2">
<fieldset

View file

@ -107,7 +107,7 @@ defmodule MvWeb.CustomFieldLive.FormComponent do
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
<p class="text-base-content/70 mb-4">
{gettext(
"Deleting this data field cannot be undone. All custom field values for this field will be permanently removed."
"Deleting this data field cannot be undone. All datafield values for this field will be permanently removed."
)}
</p>
<.button

View file

@ -158,7 +158,7 @@ defmodule MvWeb.CustomFieldLive.IndexComponent do
phx-target={@myself}
disabled={@slug_confirmation != @custom_field_to_delete.slug}
>
{gettext("Delete Custom Field and All Values")}
{gettext("Delete Datafields and All Values")}
</.button>
</div>
</div>

View file

@ -95,7 +95,7 @@ defmodule MvWeb.GlobalSettingsLive do
</div>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Settings")}
{gettext("Save Name")}
</.button>
</.form>
</.form_section>

View file

@ -79,12 +79,14 @@ defmodule MvWeb.GroupLive.Form do
<Layouts.app flash={@flash} current_user={@current_user}>
<.form for={@form} id="group-form" phx-change="validate" phx-submit="save">
<.header>
{@page_title}
<:actions>
<:leading>
<.button navigate={return_path(@return_to, @group)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{@page_title}
<:actions>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save")}
</.button>

View file

@ -50,7 +50,7 @@ defmodule MvWeb.GroupLive.Show do
end
end
defp load_group_by_slug(socket, slug, actor, params \\ %{}) do
defp load_group_by_slug(socket, slug, actor, params) do
# Load group with members and member_count
# Using explicit load ensures efficient preloading of members relationship
require Ash.Query
@ -92,8 +92,7 @@ defmodule MvWeb.GroupLive.Show do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{@group.name}
<:actions>
<:leading>
<.button
navigate={~p"/groups"}
variant="neutral"
@ -102,13 +101,16 @@ defmodule MvWeb.GroupLive.Show do
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{@group.name}
<:actions>
<%= if can?(@current_user, :update, @group) do %>
<.button
variant="primary"
navigate={~p"/groups/#{@group.slug}/edit"}
data-testid="group-show-edit-btn"
>
{gettext("Edit group")}
<.icon name="hero-pencil-square" /> {gettext("Edit group")}
</.button>
<% end %>
</:actions>

View file

@ -39,16 +39,18 @@ defmodule MvWeb.MemberLive.Form do
<Layouts.app flash={@flash} current_user={@current_user}>
<.form for={@form} id="member-form" phx-change="validate" phx-submit="save">
<.header>
<:leading>
<.button navigate={return_path(@return_to, @member)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
<%= if @member do %>
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
<% else %>
{gettext("New Member")}
<% end %>
<:actions>
<.button navigate={return_path(@return_to, @member)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save")}
</.button>
@ -408,32 +410,33 @@ defmodule MvWeb.MemberLive.Form do
member = socket.assigns.member
actor = current_actor(socket)
if is_nil(member) do
{:noreply, put_flash(socket, :error, gettext("Member not found"))}
else
if to_string(id) != to_string(member.id) do
cond do
is_nil(member) ->
{:noreply, put_flash(socket, :error, gettext("Member not found"))}
else
case Ash.destroy(member, actor: actor) do
:ok ->
{:noreply,
socket
|> put_flash(:success, gettext("Member deleted successfully"))
|> push_navigate(to: ~p"/members")}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to delete this member")
)}
to_string(id) != to_string(member.id) ->
{:noreply, put_flash(socket, :error, gettext("Member not found"))}
{:error, error} ->
Logger.warning("Member delete failed: member_id=#{member.id} error=#{inspect(error)}")
{:noreply, put_flash(socket, :error, format_destroy_error(error))}
end
end
true ->
handle_member_delete_destroy(socket, member, actor)
end
end
defp handle_member_delete_destroy(socket, member, actor) do
case Ash.destroy(member, actor: actor) do
:ok ->
{:noreply,
socket
|> put_flash(:success, gettext("Member deleted successfully"))
|> push_navigate(to: ~p"/members")}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(socket, :error, gettext("You do not have permission to delete this member"))}
{:error, error} ->
Logger.warning("Member delete failed: member_id=#{member.id} error=#{inspect(error)}")
{:noreply, put_flash(socket, :error, format_destroy_error(error))}
end
end

View file

@ -31,8 +31,7 @@ defmodule MvWeb.MemberLive.Show do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
<:actions>
<:leading>
<.button
navigate={~p"/members?highlight=#{@member.id}"}
variant="neutral"
@ -41,13 +40,16 @@ defmodule MvWeb.MemberLive.Show do
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{MvWeb.Helpers.MemberHelpers.display_name(@member)}
<:actions>
<%= if can?(@current_user, :update, @member) do %>
<.button
variant="primary"
navigate={~p"/members/#{@member}/edit?return_to=show"}
data-testid="member-edit"
>
{gettext("Edit member")}
<.icon name="hero-pencil-square" /> {gettext("Edit member")}
</.button>
<% end %>
</:actions>

View file

@ -27,10 +27,26 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
<:leading>
<.button navigate={return_path(@return_to, @membership_fee_type)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{@page_title}
<:subtitle>
{gettext("Use this form to manage membership fee types in your database.")}
</:subtitle>
<:actions>
<.button
form="membership-fee-type-form"
phx-disable-with={gettext("Saving...")}
variant="primary"
type="submit"
>
{gettext("Save")}
</.button>
</:actions>
</.header>
<.form

View file

@ -23,13 +23,15 @@ defmodule MvWeb.RoleLive.Form do
<Layouts.app flash={@flash} current_user={@current_user}>
<.form class="max-w-xl" for={@form} id="role-form" phx-change="validate" phx-submit="save">
<.header>
{@page_title}
<:subtitle>{gettext("Use this form to manage roles in your database.")}</:subtitle>
<:actions>
<:leading>
<.button navigate={return_path(@return_to, @role)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{@page_title}
<:subtitle>{gettext("Use this form to manage roles in your database.")}</:subtitle>
<:actions>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
{gettext("Save")}
</.button>

View file

@ -161,10 +161,7 @@ defmodule MvWeb.RoleLive.Show do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{gettext("Role")} {@role.name}
<:subtitle>{gettext("Role details and permissions.")}</:subtitle>
<:actions>
<:leading>
<.button
navigate={~p"/admin/roles"}
variant="neutral"
@ -173,13 +170,18 @@ defmodule MvWeb.RoleLive.Show do
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{gettext("Role")} {@role.name}
<:subtitle>{gettext("Role details and permissions.")}</:subtitle>
<:actions>
<%= if can?(@current_user, :update, Mv.Authorization.Role) do %>
<.button
variant="primary"
navigate={~p"/admin/roles/#{@role}/edit"}
data-testid="role-show-edit-btn"
>
{gettext("Edit role")}
<.icon name="hero-pencil-square" /> {gettext("Edit role")}
</.button>
<% end %>
</:actions>

View file

@ -46,8 +46,24 @@ defmodule MvWeb.UserLive.Form do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
<:leading>
<.button navigate={return_path(@return_to, @user)} variant="neutral">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{@page_title}
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
<:actions>
<.button
form="user-form"
phx-disable-with={gettext("Saving...")}
variant="primary"
type="submit"
>
{gettext("Save User")}
</.button>
</:actions>
</.header>
<.form class="max-w-xl" for={@form} id="user-form" phx-change="validate" phx-submit="save">
@ -300,7 +316,8 @@ defmodule MvWeb.UserLive.Form do
phx-click="delete"
phx-value-id={@user.id}
data-confirm={
gettext("Are you sure you want to delete the user %{email}? This action cannot be undone.",
gettext(
"Are you sure you want to delete the user %{email}? This action cannot be undone.",
email: @user.email
)
}
@ -442,36 +459,18 @@ defmodule MvWeb.UserLive.Form do
user = socket.assigns.user
actor = current_actor(socket)
if is_nil(user) do
{:noreply, put_flash(socket, :error, gettext("User not found"))}
else
if to_string(id) != to_string(user.id) do
cond do
is_nil(user) ->
{:noreply, put_flash(socket, :error, gettext("User not found"))}
else
if Mv.Helpers.SystemActor.system_user?(user) do
{:noreply,
put_flash(socket, :error, gettext("System user cannot be deleted."))}
else
case Ash.destroy(user, domain: Mv.Accounts, actor: actor) do
:ok ->
{:noreply,
socket
|> put_flash(:success, gettext("User deleted successfully"))
|> push_navigate(to: ~p"/users")}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to delete this user")
)}
to_string(id) != to_string(user.id) ->
{:noreply, put_flash(socket, :error, gettext("User not found"))}
{:error, error} ->
{:noreply, put_flash(socket, :error, format_ash_error(error))}
end
end
end
Mv.Helpers.SystemActor.system_user?(user) ->
{:noreply, put_flash(socket, :error, gettext("System user cannot be deleted."))}
true ->
handle_user_delete_destroy(socket, user, actor)
end
end
@ -585,6 +584,23 @@ defmodule MvWeb.UserLive.Form do
{:noreply, socket}
end
defp handle_user_delete_destroy(socket, user, actor) do
case Ash.destroy(user, domain: Mv.Accounts, actor: actor) do
:ok ->
{:noreply,
socket
|> put_flash(:success, gettext("User deleted successfully"))
|> push_navigate(to: ~p"/users")}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(socket, :error, gettext("You do not have permission to delete this user"))}
{:error, error} ->
{:noreply, put_flash(socket, :error, format_ash_error(error))}
end
end
defp handle_member_linking(socket, user, actor) do
result = perform_member_link_action(socket, user, actor)

View file

@ -7,15 +7,10 @@ defmodule MvWeb.UserLive.Index do
- Sort users by email (default)
- Navigate to user details (row click) and edit from details header
- Delete only via Danger zone on user show/edit
- Bulk selection for future batch operations
## Relationships
Displays linked member information when a user is connected to a member account.
## Events
- `select_user` - Toggle individual user selection
- `select_all` - Toggle selection of all visible users
## Security
User deletion requires admin permissions (enforced by Ash policies).
"""
@ -42,24 +37,7 @@ defmodule MvWeb.UserLive.Index do
|> assign(:page_title, gettext("Listing Users"))
|> assign(:sort_field, :email)
|> assign(:sort_order, :asc)
|> assign(:users, sorted)
|> assign(:selected_users, [])}
end
# Selects one user in the list of users
@impl true
def handle_event("select_user", %{"id" => id}, socket) do
# Normalize ID to string for consistent comparison
id_str = to_string(id)
selected =
if id_str in socket.assigns.selected_users do
List.delete(socket.assigns.selected_users, id_str)
else
[id_str | socket.assigns.selected_users]
end
{:noreply, assign(socket, :selected_users, selected)}
|> assign(:users, sorted)}
end
# Sorts the list of users according to a field, when you click on the column header
@ -86,24 +64,6 @@ defmodule MvWeb.UserLive.Index do
|> assign(:users, sorted_users)}
end
# Selects all users in the list of users
@impl true
def handle_event("select_all", _params, socket) do
users = socket.assigns.users
# Normalize IDs to strings for consistent comparison
all_ids = Enum.map(users, &to_string(&1.id))
selected =
if Enum.sort(socket.assigns.selected_users) == Enum.sort(all_ids) do
[]
else
all_ids
end
{:noreply, assign(socket, :selected_users, selected)}
end
defp toggle_order(:asc), do: :desc
defp toggle_order(:desc), do: :asc
defp sort_fun(:asc), do: &<=/2

View file

@ -19,33 +19,6 @@
sort_field={@sort_field}
sort_order={@sort_order}
>
<:col
:let={user}
label={
~H"""
<.input
type="checkbox"
name="select_all"
phx-click="select_all"
checked={Enum.sort(@selected_users) == Enum.map(@users, &to_string(&1.id)) |> Enum.sort()}
aria-label={gettext("Select all users")}
role="checkbox"
/>
"""
}
>
<.input
type="checkbox"
name={to_string(user.id)}
phx-click="select_user"
phx-value-id={to_string(user.id)}
checked={to_string(user.id) in @selected_users}
phx-capture-click
phx-stop-propagation
aria-label={gettext("Select user")}
role="checkbox"
/>
</:col>
<:col
:let={user}
sort_field={:email}

View file

@ -34,14 +34,20 @@ defmodule MvWeb.UserLive.Show do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
<:leading>
<.button
navigate={~p"/users"}
variant="neutral"
aria-label={gettext("Back to users list")}
>
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back")}
</.button>
</:leading>
{gettext("User")} {@user.email}
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
<:actions>
<.button navigate={~p"/users"} variant="neutral" aria-label={gettext("Back to users list")}>
<.icon name="hero-arrow-left" />
<span class="sr-only">{gettext("Back to users list")}</span>
</.button>
<%= if can?(@current_user, :update, @user) do %>
<.button
variant="primary"
@ -99,7 +105,8 @@ defmodule MvWeb.UserLive.Show do
phx-click="delete"
phx-value-id={@user.id}
data-confirm={
gettext("Are you sure you want to delete the user %{email}? This action cannot be undone.",
gettext(
"Are you sure you want to delete the user %{email}? This action cannot be undone.",
email: @user.email
)
}
@ -141,33 +148,32 @@ defmodule MvWeb.UserLive.Show do
user = socket.assigns.user
actor = current_actor(socket)
if to_string(id) != to_string(user.id) do
{:noreply, put_flash(socket, :error, gettext("User not found"))}
else
if Mv.Helpers.SystemActor.system_user?(user) do
cond do
to_string(id) != to_string(user.id) ->
{:noreply, put_flash(socket, :error, gettext("User not found"))}
Mv.Helpers.SystemActor.system_user?(user) ->
{:noreply, put_flash(socket, :error, gettext("System user cannot be deleted."))}
true ->
handle_user_delete_destroy(socket, user, actor)
end
end
defp handle_user_delete_destroy(socket, user, actor) do
case Ash.destroy(user, domain: Mv.Accounts, actor: actor) do
:ok ->
{:noreply,
put_flash(socket, :error, gettext("System user cannot be deleted."))}
else
case Ash.destroy(user, domain: Mv.Accounts, actor: actor) do
:ok ->
{:noreply,
socket
|> put_flash(:success, gettext("User deleted successfully"))
|> push_navigate(to: ~p"/users")}
socket
|> put_flash(:success, gettext("User deleted successfully"))
|> push_navigate(to: ~p"/users")}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(
socket,
:error,
gettext("You do not have permission to delete this user")
)}
{:error, %Ash.Error.Forbidden{}} ->
{:noreply,
put_flash(socket, :error, gettext("You do not have permission to delete this user"))}
{:error, error} ->
{:noreply,
put_flash(socket, :error, format_ash_error(error))}
end
end
{:error, error} ->
{:noreply, put_flash(socket, :error, format_ash_error(error))}
end
end
end