fix: add error message to form

This commit is contained in:
Simon 2026-01-19 12:47:17 +01:00
parent c31392e4fe
commit 584442076e
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
2 changed files with 222 additions and 1 deletions

View file

@ -295,11 +295,14 @@ defmodule MvWeb.MemberLive.Form do
handle_save_success(socket, member)
{:error, form} ->
{:noreply, assign(socket, form: form)}
handle_save_error(socket, form)
end
rescue
_e in [Ash.Error.Forbidden, Ash.Error.Forbidden.Policy] ->
handle_save_forbidden(socket)
e ->
handle_save_exception(socket, e)
end
end
@ -321,6 +324,13 @@ defmodule MvWeb.MemberLive.Form do
{:noreply, socket}
end
defp handle_save_error(socket, form) do
# Always show a flash message when save fails
# Field-level validation errors are displayed in form fields, but flash provides additional feedback
error_message = extract_error_message(form)
{:noreply, socket |> assign(form: form) |> put_flash(:error, error_message)}
end
defp handle_save_forbidden(socket) do
# Handle policy violations that aren't properly displayed in forms
# AshPhoenix.Form doesn't implement FormData.Error protocol for Forbidden errors
@ -332,6 +342,77 @@ defmodule MvWeb.MemberLive.Form do
{:noreply, put_flash(socket, :error, error_message)}
end
defp handle_save_exception(socket, exception) do
# Handle unexpected exceptions (database errors, network issues, etc.)
require Logger
Logger.error("Unexpected error saving member: #{inspect(exception)}")
action = get_action_name(socket.assigns.form.source.type)
error_message = gettext("Failed to %{action} member.", action: action)
{:noreply, put_flash(socket, :error, error_message)}
end
# Extracts a user-friendly error message from form errors
defp extract_error_message(form) do
# Try to extract message from source errors first
source_errors = get_source_errors(form)
case source_errors do
[%Ash.Error.Invalid{errors: errors} | _] when is_list(errors) ->
# Extract first error message
case List.first(errors) do
%{message: message} when is_binary(message) ->
gettext("Validation failed: %{message}", message: message)
%{field: field, message: message} when is_binary(message) ->
gettext("Validation failed: %{field} %{message}", field: field, message: message)
_ ->
gettext("Validation failed. Please check your input.")
end
[error | _] ->
# Try to extract message from other error types
case error do
%{message: message} when is_binary(message) -> message
error when is_struct(error) ->
# Try to use Ash.ErrorKind protocol if available
try do
Ash.ErrorKind.message(error)
rescue
Protocol.UndefinedError -> gettext("Failed to save member. Please try again.")
end
_ -> gettext("Failed to save member. Please try again.")
end
_ ->
# Check if there are any field errors in the form
if has_form_errors?(form) do
gettext("Please correct the errors in the form and try again.")
else
gettext("Failed to save member. Please try again.")
end
end
end
# Checks if form has any errors
defp has_form_errors?(form) do
case Map.get(form, :errors) do
errors when is_list(errors) and length(errors) > 0 -> true
_ -> false
end
end
# Extracts source-level errors from form (Ash errors, etc.)
defp get_source_errors(form) do
case form.source do
%{errors: errors} when is_list(errors) -> errors
%Ash.Changeset{errors: errors} when is_list(errors) -> errors
_ -> []
end
end
defp get_action_name(:create), do: gettext("create")
defp get_action_name(:update), do: gettext("update")
defp get_action_name(other), do: to_string(other)

View file

@ -0,0 +1,140 @@
defmodule MvWeb.MemberLive.FormErrorHandlingTest do
@moduledoc """
Tests for error handling in the member form, specifically flash message display.
"""
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
alias Mv.Membership.Member
require Ash.Query
describe "error handling - flash messages" do
test "shows flash message when member creation fails with validation error", %{conn: conn} do
# Create a member with the same email to trigger uniqueness error
{:ok, _existing_member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Existing",
last_name: "Member",
email: "duplicate@example.com"
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members/new")
# Try to create member with duplicate email
form_data = %{
"member[first_name]" => "New",
"member[last_name]" => "Member",
"member[email]" => "duplicate@example.com"
}
html =
view
|> form("#member-form", form_data)
|> render_submit()
# Should show flash error message
assert has_element?(view, "#flash-group")
assert html =~ "error" or html =~ "Error" or html =~ "Fehler" or
html =~ "failed" or html =~ "fehlgeschlagen" or
html =~ "Validation failed" or html =~ "Validierung fehlgeschlagen"
end
test "shows flash message when member creation fails with missing required fields", %{
conn: conn
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members/new")
# Submit form with missing required fields (e.g., email)
form_data = %{
"member[first_name]" => "Test",
"member[last_name]" => "User"
# email is missing
}
html =
view
|> form("#member-form", form_data)
|> render_submit()
# Should show flash error message
assert has_element?(view, "#flash-group")
assert html =~ "error" or html =~ "Error" or html =~ "Fehler" or
html =~ "failed" or html =~ "fehlgeschlagen" or
html =~ "Validation failed" or html =~ "Validierung fehlgeschlagen" or
html =~ "Please correct" or html =~ "Bitte korrigieren"
end
test "shows flash message when member update fails", %{conn: conn} do
# Create a member to edit
{:ok, member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Original",
last_name: "Member",
email: "original@example.com"
})
|> Ash.create()
# Create another member with different email
{:ok, _other_member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Other",
last_name: "Member",
email: "other@example.com"
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members/#{member.id}/edit")
# Try to update with duplicate email
form_data = %{
"member[first_name]" => "Updated",
"member[last_name]" => "Member",
"member[email]" => "other@example.com"
}
html =
view
|> form("#member-form", form_data)
|> render_submit()
# Should show flash error message
assert has_element?(view, "#flash-group")
assert html =~ "error" or html =~ "Error" or html =~ "Fehler" or
html =~ "failed" or html =~ "fehlgeschlagen" or
html =~ "Validation failed" or html =~ "Validierung fehlgeschlagen"
end
test "form still displays field-level validation errors when flash message is shown", %{
conn: conn
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members/new")
# Submit form with invalid email format
form_data = %{
"member[first_name]" => "Test",
"member[last_name]" => "User",
"member[email]" => "invalid-email-format"
}
html =
view
|> form("#member-form", form_data)
|> render_submit()
# Should show both flash message and field-level error
assert has_element?(view, "#flash-group")
# Field-level errors should also be visible in the form
assert html =~ "email" or html =~ "Email"
end
end
end