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
This commit is contained in:
Moritz 2026-01-13 14:05:49 +01:00
parent a22081f288
commit eb81d5f7cb
Signed by: moritz
GPG key ID: 1020A035E5DD0824

View file

@ -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