From eb81d5f7cbdeb641649bffa7da91a547671e07ed Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 13 Jan 2026 14:05:49 +0100 Subject: [PATCH] refactor: Simplify UserLive.Form handle_event and improve error handling - Extract handle_member_linking, perform_member_link_action helpers - Extract handle_save_success, get_action_name, handle_member_link_error - Replace hardcoded strings with gettext translations - Use submit_form wrapper for consistent actor handling - Group all handle_event/3 clauses together - Add early return in load_members_for_linking if actor is nil --- lib/mv_web/live/user_live/form.ex | 134 +++++++++++++++++------------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index a7e56e4..1cc6f6a 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -34,7 +34,7 @@ defmodule MvWeb.UserLive.Form do use MvWeb, :live_view on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} - import MvWeb.LiveHelpers, only: [current_actor: 1] + import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3] @impl true def render(assigns) do @@ -305,6 +305,7 @@ defmodule MvWeb.UserLive.Form do {:noreply, socket} end + @impl true def handle_event("validate", %{"user" => user_params}, socket) do validated_form = AshPhoenix.Form.validate(socket.assigns.form, user_params) @@ -322,70 +323,30 @@ defmodule MvWeb.UserLive.Form do {:noreply, socket} end + @impl true def handle_event("save", %{"user" => user_params}, socket) do actor = current_actor(socket) # First save the user without member changes - case AshPhoenix.Form.submit(socket.assigns.form, - params: user_params, - action_opts: [actor: actor] - ) do + case submit_form(socket.assigns.form, user_params, actor) do {:ok, user} -> - # Then handle member linking/unlinking as a separate step - actor = current_actor(socket) - - result = - cond do - # Selected member ID takes precedence (new link) - socket.assigns.selected_member_id -> - Mv.Accounts.update_user(user, %{member: %{id: socket.assigns.selected_member_id}}, - actor: actor - ) - - # Unlink flag is set - socket.assigns[:unlink_member] -> - Mv.Accounts.update_user(user, %{member: nil}, actor: actor) - - # No changes to member relationship - true -> - {:ok, user} - end - - case result do - {:ok, updated_user} -> - notify_parent({:saved, updated_user}) - - socket = - socket - |> put_flash(:info, "User #{socket.assigns.form.source.type}d successfully") - |> push_navigate(to: return_path(socket.assigns.return_to, updated_user)) - - {:noreply, socket} - - {:error, error} -> - # Show user-friendly error from member linking/unlinking - error_message = extract_error_message(error) - - {:noreply, - put_flash( - socket, - :error, - gettext("Failed to link member: %{error}", error: error_message) - )} - end + handle_member_linking(socket, user, actor) {:error, form} -> {:noreply, assign(socket, form: form)} end end + @impl true def handle_event("show_member_dropdown", _params, socket) do {:noreply, assign(socket, show_member_dropdown: true)} end + @impl true def handle_event("hide_member_dropdown", _params, socket) do {:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)} end + @impl true def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do return_if_dropdown_closed(socket, fn -> max_index = length(socket.assigns.available_members) - 1 @@ -402,6 +363,7 @@ defmodule MvWeb.UserLive.Form do end) end + @impl true def handle_event("member_dropdown_keydown", %{"key" => "ArrowUp"}, socket) do return_if_dropdown_closed(socket, fn -> current = socket.assigns.focused_member_index @@ -417,23 +379,27 @@ defmodule MvWeb.UserLive.Form do end) end + @impl true def handle_event("member_dropdown_keydown", %{"key" => "Enter"}, socket) do return_if_dropdown_closed(socket, fn -> select_focused_member(socket) end) end + @impl true def handle_event("member_dropdown_keydown", %{"key" => "Escape"}, socket) do return_if_dropdown_closed(socket, fn -> {:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)} end) end + @impl true def handle_event("member_dropdown_keydown", _params, socket) do # Ignore other keys {:noreply, socket} end + @impl true def handle_event("search_members", %{"member_search" => query}, socket) do socket = socket @@ -445,6 +411,7 @@ defmodule MvWeb.UserLive.Form do {:noreply, socket} end + @impl true def handle_event("select_member", %{"id" => member_id}, socket) do # Find the selected member to get their name selected_member = Enum.find(socket.assigns.available_members, &(&1.id == member_id)) @@ -461,27 +428,82 @@ defmodule MvWeb.UserLive.Form do |> assign(:selected_member_name, member_name) |> assign(:unlink_member, false) |> assign(:show_member_dropdown, false) - |> assign(:member_search_query, member_name) - |> push_event("set-input-value", %{id: "member-search-input", value: member_name}) + |> assign(:focused_member_index, nil) {:noreply, socket} end + @impl true def handle_event("unlink_member", _params, socket) do - # Set flag to unlink member on save - # Clear all member selection state and keep dropdown hidden socket = socket - |> assign(:unlink_member, true) |> assign(:selected_member_id, nil) |> assign(:selected_member_name, nil) - |> assign(:member_search_query, "") + |> assign(:unlink_member, true) |> assign(:show_member_dropdown, false) - |> load_initial_members() + |> assign(:focused_member_index, nil) {:noreply, socket} end + defp handle_member_linking(socket, user, actor) do + result = perform_member_link_action(socket, user, actor) + + case result do + {:ok, updated_user} -> + handle_save_success(socket, updated_user) + + {:error, error} -> + handle_member_link_error(socket, error) + end + end + + defp perform_member_link_action(socket, user, actor) do + cond do + # Selected member ID takes precedence (new link) + socket.assigns.selected_member_id -> + Mv.Accounts.update_user(user, %{member: %{id: socket.assigns.selected_member_id}}, + actor: actor + ) + + # Unlink flag is set + socket.assigns[:unlink_member] -> + Mv.Accounts.update_user(user, %{member: nil}, actor: actor) + + # No changes to member relationship + true -> + {:ok, user} + end + end + + defp handle_save_success(socket, updated_user) do + notify_parent({:saved, updated_user}) + + action = get_action_name(socket.assigns.form.source.type) + + socket = + socket + |> put_flash(:info, gettext("User %{action} successfully", action: action)) + |> push_navigate(to: return_path(socket.assigns.return_to, updated_user)) + + {:noreply, socket} + end + + defp get_action_name(:create), do: gettext("created") + defp get_action_name(:update), do: gettext("updated") + defp get_action_name(other), do: to_string(other) + + defp handle_member_link_error(socket, error) do + error_message = extract_error_message(error) + + {:noreply, + put_flash( + socket, + :error, + gettext("Failed to link member: %{error}", error: error_message) + )} + end + @spec notify_parent(any()) :: any() defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) @@ -597,10 +619,10 @@ defmodule MvWeb.UserLive.Form do case List.first(errors) do %{message: message} when is_binary(message) -> message %{field: field, message: message} -> "#{field}: #{message}" - _ -> "Unknown error" + _ -> gettext("Unknown error") end end defp extract_error_message(error) when is_binary(error), do: error - defp extract_error_message(_), do: "Unknown error" + defp extract_error_message(_), do: gettext("Unknown error") end