Collection of small UI Improvements closes #511 #527
7 changed files with 105 additions and 36 deletions
34
lib/mv_web/helpers/ash_error_helpers.ex
Normal file
34
lib/mv_web/helpers/ash_error_helpers.ex
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
63
test/mv_web/helpers/ash_error_helpers_test.exs
Normal file
63
test/mv_web/helpers/ash_error_helpers_test.exs
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue