Collection of small UI Improvements closes #511 #527

Merged
moritz merged 7 commits from issue/mitgliederverwaltung-511 into main 2026-06-15 15:44:58 +02:00
7 changed files with 105 additions and 36 deletions
Showing only changes of commit 856ea4279c - Show all commits

View file

@ -0,0 +1,34 @@
defmodule MvWeb.Helpers.AshErrorHelpers do
@moduledoc """
Shared formatting for Ash errors surfaced as flash messages in the
member show LiveComponents.
Centralizes the translation of `Ash.Error.Invalid` / `Ash.Error.Forbidden`
(and plain string/unknown errors) into user-facing text so the components do
not each carry their own copy.
"""
use Gettext, backend: MvWeb.Gettext
@doc """
Turns an Ash error into a human-readable, localized string.
- `Ash.Error.Invalid` joins the individual error messages, falling back to
`inspect/1` for sub-errors that carry no `:message`.
- `Ash.Error.Forbidden` a localized "not allowed" message.
- a binary passed through unchanged (already a ready-to-show message).
- anything else a localized generic error message.
"""
def format_error(%Ash.Error.Invalid{errors: errors}) do
Enum.map_join(errors, ", ", fn
%{message: message} -> message
other -> inspect(other)
end)
end
def format_error(%Ash.Error.Forbidden{}) do
gettext("You are not allowed to perform this action.")
end
def format_error(error) when is_binary(error), do: error
def format_error(_error), do: gettext("An error occurred")
end

View file

@ -15,6 +15,7 @@ defmodule MvWeb.MemberLive.Show.DeactivateComponent do
use MvWeb, :live_component use MvWeb, :live_component
import MvWeb.Authorization, only: [can?: 3] import MvWeb.Authorization, only: [can?: 3]
import MvWeb.Helpers.AshErrorHelpers, only: [format_error: 1]
alias Mv.Membership alias Mv.Membership
alias MvWeb.Helpers.MemberHelpers alias MvWeb.Helpers.MemberHelpers
@ -187,17 +188,4 @@ defmodule MvWeb.MemberLive.Show.DeactivateComponent do
|> assign(:show_modal, false) |> assign(:show_modal, false)
|> assign(:error, nil) |> assign(:error, nil)
end end
defp format_error(%Ash.Error.Invalid{errors: errors}) do
Enum.map_join(errors, ", ", fn
%{message: message} -> message
other -> inspect(other)
end)
end
defp format_error(%Ash.Error.Forbidden{}) do
gettext("You are not allowed to perform this action.")
end
defp format_error(_error), do: gettext("An error occurred")
end end

View file

@ -15,6 +15,7 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
require Ash.Query require Ash.Query
import MvWeb.LiveHelpers, only: [current_actor: 1] import MvWeb.LiveHelpers, only: [current_actor: 1]
import MvWeb.Authorization, only: [can?: 3] import MvWeb.Authorization, only: [can?: 3]
import MvWeb.Helpers.AshErrorHelpers, only: [format_error: 1]
alias Mv.Membership alias Mv.Membership
alias Mv.MembershipFees alias Mv.MembershipFees
@ -1144,17 +1145,6 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
defp format_status_label(:unpaid), do: gettext("Unpaid") defp format_status_label(:unpaid), do: gettext("Unpaid")
defp format_status_label(:suspended), do: gettext("Suspended") defp format_status_label(:suspended), do: gettext("Suspended")
defp format_error(%Ash.Error.Invalid{} = error) do
Enum.map_join(error.errors, ", ", fn e -> e.message end)
end
defp format_error(%Ash.Error.Forbidden{}) do
gettext("You are not allowed to perform this action.")
end
defp format_error(error) when is_binary(error), do: error
defp format_error(_error), do: gettext("An error occurred")
defp validate_cycle_not_exists(cycles, cycle_start) do defp validate_cycle_not_exists(cycles, cycle_start) do
if Enum.any?(cycles, &(&1.cycle_start == cycle_start)) do if Enum.any?(cycles, &(&1.cycle_start == cycle_start)) do
{:error, :cycle_exists} {:error, :cycle_exists}

View file

@ -191,8 +191,7 @@ msgstr "Betrag"
msgid "An account with this email already exists. Please verify your password to link your OIDC account." msgid "An account with this email already exists. Please verify your password to link your OIDC account."
msgstr "Ein Konto mit dieser E-Mail existiert bereits. Bitte gib dein Passwort ein, um dein OIDC-Konto zu verknüpfen." msgstr "Ein Konto mit dieser E-Mail existiert bereits. Bitte gib dein Passwort ein, um dein OIDC-Konto zu verknüpfen."
#: lib/mv_web/live/member_live/show/deactivate_component.ex #: lib/mv_web/helpers/ash_error_helpers.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex #: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex #: lib/mv_web/live/role_live/helpers.ex
@ -3932,8 +3931,7 @@ msgstr "Du hattest bereits einen offenen Antrag. Hier ist ein neuer Bestätigung
msgid "You are about to delete all %{count} cycles for this member." msgid "You are about to delete all %{count} cycles for this member."
msgstr "Du bist dabei, alle %{count} Zyklen für dieses Mitglied zu löschen." msgstr "Du bist dabei, alle %{count} Zyklen für dieses Mitglied zu löschen."
#: lib/mv_web/live/member_live/show/deactivate_component.ex #: lib/mv_web/helpers/ash_error_helpers.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You are not allowed to perform this action." msgid "You are not allowed to perform this action."
msgstr "Du hast keine Berechtigung, diese Aktion auszuführen." msgstr "Du hast keine Berechtigung, diese Aktion auszuführen."

View file

@ -192,8 +192,7 @@ msgstr ""
msgid "An account with this email already exists. Please verify your password to link your OIDC account." msgid "An account with this email already exists. Please verify your password to link your OIDC account."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/deactivate_component.ex #: lib/mv_web/helpers/ash_error_helpers.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex #: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex #: lib/mv_web/live/role_live/helpers.ex
@ -3932,8 +3931,7 @@ msgstr ""
msgid "You are about to delete all %{count} cycles for this member." msgid "You are about to delete all %{count} cycles for this member."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/deactivate_component.ex #: lib/mv_web/helpers/ash_error_helpers.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You are not allowed to perform this action." msgid "You are not allowed to perform this action."
msgstr "" msgstr ""

View file

@ -192,8 +192,7 @@ msgstr ""
msgid "An account with this email already exists. Please verify your password to link your OIDC account." msgid "An account with this email already exists. Please verify your password to link your OIDC account."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/deactivate_component.ex #: lib/mv_web/helpers/ash_error_helpers.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#: lib/mv_web/live/membership_fee_settings_live.ex #: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex #: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex #: lib/mv_web/live/role_live/helpers.ex
@ -3932,8 +3931,7 @@ msgstr "You already had a pending request. Here is a new confirmation link."
msgid "You are about to delete all %{count} cycles for this member." msgid "You are about to delete all %{count} cycles for this member."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show/deactivate_component.ex #: lib/mv_web/helpers/ash_error_helpers.ex
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "You are not allowed to perform this action." msgid "You are not allowed to perform this action."
msgstr "" msgstr ""

View file

@ -0,0 +1,63 @@
defmodule MvWeb.Helpers.AshErrorHelpersTest do
@moduledoc """
Tests for format_error/1, the shared Ash error formatter used by the
member show LiveComponents.
"""
use Mv.DataCase, async: true
import MvWeb.Helpers.AshErrorHelpers
describe "format_error/1" do
test "joins messages of an Ash.Error.Invalid with commas" do
error = %Ash.Error.Invalid{
errors: [%{message: "exit_date must be after join_date"}, %{message: "another"}]
}
assert format_error(error) == "exit_date must be after join_date, another"
end
test "falls back to inspect for invalid sub-errors without a message" do
error = %Ash.Error.Invalid{errors: [:boom]}
assert format_error(error) == inspect(:boom)
end
test "returns a localized message for an Ash.Error.Forbidden" do
assert format_error(%Ash.Error.Forbidden{}) =~ "allowed"
end
test "passes through a plain binary unchanged" do
assert format_error("custom message") == "custom message"
end
test "returns a generic localized message for anything else" do
assert format_error(:unexpected) == "An error occurred"
end
test "renders the genuine update_member validation error as a user-facing message" do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, member} =
Mv.Membership.create_member(
%{
first_name: "Exit",
last_name: "Validation",
email: "exit-validation-#{System.unique_integer([:positive])}@example.com",
join_date: ~D[2024-01-01]
},
actor: system_actor
)
# exit_date earlier than join_date triggers the resource validation
{:error, %Ash.Error.Invalid{} = error} =
Mv.Membership.update_member(member, %{exit_date: ~D[2023-12-31]}, actor: system_actor)
formatted = format_error(error)
# The real Ash sub-error must surface its localized :message, not an inspect()'d struct.
assert formatted == "cannot be before join date"
refute formatted =~ "InvalidAttribute"
refute formatted =~ "%{"
end
end
end