From 584442076e67eda9b95a45e930d45ed3781e5215 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 19 Jan 2026 12:47:17 +0100 Subject: [PATCH] fix: add error message to form --- lib/mv_web/live/member_live/form.ex | 83 ++++++++++- .../member_live/form_error_handling_test.exs | 140 ++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 test/mv_web/member_live/form_error_handling_test.exs diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex index 3c68b21..d384a45 100644 --- a/lib/mv_web/live/member_live/form.ex +++ b/lib/mv_web/live/member_live/form.ex @@ -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) diff --git a/test/mv_web/member_live/form_error_handling_test.exs b/test/mv_web/member_live/form_error_handling_test.exs new file mode 100644 index 0000000..4f76fca --- /dev/null +++ b/test/mv_web/member_live/form_error_handling_test.exs @@ -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