Member Resource Policies closes #345 #346
1 changed files with 78 additions and 56 deletions
|
|
@ -34,7 +34,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
|
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
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
|
|
@ -305,6 +305,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||||
validated_form = AshPhoenix.Form.validate(socket.assigns.form, user_params)
|
validated_form = AshPhoenix.Form.validate(socket.assigns.form, user_params)
|
||||||
|
|
||||||
|
|
@ -322,70 +323,30 @@ defmodule MvWeb.UserLive.Form do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("save", %{"user" => user_params}, socket) do
|
def handle_event("save", %{"user" => user_params}, socket) do
|
||||||
actor = current_actor(socket)
|
actor = current_actor(socket)
|
||||||
# First save the user without member changes
|
# First save the user without member changes
|
||||||
case AshPhoenix.Form.submit(socket.assigns.form,
|
case submit_form(socket.assigns.form, user_params, actor) do
|
||||||
params: user_params,
|
|
||||||
action_opts: [actor: actor]
|
|
||||||
) do
|
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
# Then handle member linking/unlinking as a separate step
|
handle_member_linking(socket, user, actor)
|
||||||
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
|
|
||||||
|
|
||||||
{:error, form} ->
|
{:error, form} ->
|
||||||
{:noreply, assign(socket, form: form)}
|
{:noreply, assign(socket, form: form)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("show_member_dropdown", _params, socket) do
|
def handle_event("show_member_dropdown", _params, socket) do
|
||||||
{:noreply, assign(socket, show_member_dropdown: true)}
|
{:noreply, assign(socket, show_member_dropdown: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("hide_member_dropdown", _params, socket) do
|
def handle_event("hide_member_dropdown", _params, socket) do
|
||||||
{:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)}
|
{:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do
|
def handle_event("member_dropdown_keydown", %{"key" => "ArrowDown"}, socket) do
|
||||||
return_if_dropdown_closed(socket, fn ->
|
return_if_dropdown_closed(socket, fn ->
|
||||||
max_index = length(socket.assigns.available_members) - 1
|
max_index = length(socket.assigns.available_members) - 1
|
||||||
|
|
@ -402,6 +363,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", %{"key" => "ArrowUp"}, socket) do
|
def handle_event("member_dropdown_keydown", %{"key" => "ArrowUp"}, socket) do
|
||||||
return_if_dropdown_closed(socket, fn ->
|
return_if_dropdown_closed(socket, fn ->
|
||||||
current = socket.assigns.focused_member_index
|
current = socket.assigns.focused_member_index
|
||||||
|
|
@ -417,23 +379,27 @@ defmodule MvWeb.UserLive.Form do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", %{"key" => "Enter"}, socket) do
|
def handle_event("member_dropdown_keydown", %{"key" => "Enter"}, socket) do
|
||||||
return_if_dropdown_closed(socket, fn ->
|
return_if_dropdown_closed(socket, fn ->
|
||||||
select_focused_member(socket)
|
select_focused_member(socket)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", %{"key" => "Escape"}, socket) do
|
def handle_event("member_dropdown_keydown", %{"key" => "Escape"}, socket) do
|
||||||
return_if_dropdown_closed(socket, fn ->
|
return_if_dropdown_closed(socket, fn ->
|
||||||
{:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)}
|
{:noreply, assign(socket, show_member_dropdown: false, focused_member_index: nil)}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("member_dropdown_keydown", _params, socket) do
|
def handle_event("member_dropdown_keydown", _params, socket) do
|
||||||
# Ignore other keys
|
# Ignore other keys
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("search_members", %{"member_search" => query}, socket) do
|
def handle_event("search_members", %{"member_search" => query}, socket) do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|
|
@ -445,6 +411,7 @@ defmodule MvWeb.UserLive.Form do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("select_member", %{"id" => member_id}, socket) do
|
def handle_event("select_member", %{"id" => member_id}, socket) do
|
||||||
# Find the selected member to get their name
|
# Find the selected member to get their name
|
||||||
selected_member = Enum.find(socket.assigns.available_members, &(&1.id == member_id))
|
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(:selected_member_name, member_name)
|
||||||
|> assign(:unlink_member, false)
|
|> assign(:unlink_member, false)
|
||||||
|> assign(:show_member_dropdown, false)
|
|> assign(:show_member_dropdown, false)
|
||||||
|> assign(:member_search_query, member_name)
|
|> assign(:focused_member_index, nil)
|
||||||
|> push_event("set-input-value", %{id: "member-search-input", value: member_name})
|
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_event("unlink_member", _params, socket) do
|
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 =
|
||||||
socket
|
socket
|
||||||
|> assign(:unlink_member, true)
|
|
||||||
|> assign(:selected_member_id, nil)
|
|> assign(:selected_member_id, nil)
|
||||||
|> assign(:selected_member_name, nil)
|
|> assign(:selected_member_name, nil)
|
||||||
|> assign(:member_search_query, "")
|
|> assign(:unlink_member, true)
|
||||||
|> assign(:show_member_dropdown, false)
|
|> assign(:show_member_dropdown, false)
|
||||||
|> load_initial_members()
|
|> assign(:focused_member_index, nil)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
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()
|
@spec notify_parent(any()) :: any()
|
||||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||||
|
|
||||||
|
|
@ -597,10 +619,10 @@ defmodule MvWeb.UserLive.Form do
|
||||||
case List.first(errors) do
|
case List.first(errors) do
|
||||||
%{message: message} when is_binary(message) -> message
|
%{message: message} when is_binary(message) -> message
|
||||||
%{field: field, message: message} -> "#{field}: #{message}"
|
%{field: field, message: message} -> "#{field}: #{message}"
|
||||||
_ -> "Unknown error"
|
_ -> gettext("Unknown error")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_error_message(error) when is_binary(error), do: error
|
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue