defmodule MvWeb.RoleLive.Form do @moduledoc """ LiveView form for creating and editing roles. ## Features - Create new roles - Edit existing roles (name, description, permission_set_name) - Custom dropdown for permission_set_name with badges - Form validation ## Security Only admins can access this page (enforced by authorization). """ use MvWeb, :live_view alias Mv.Authorization.PermissionSets import MvWeb.RoleLive.Helpers, only: [format_error: 1] on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} @impl true def render(assigns) do ~H""" <.header> {@page_title} <:subtitle>{gettext("Use this form to manage roles in your database.")} <.form class="max-w-xl" for={@form} id="role-form" phx-change="validate" phx-submit="save"> <.input field={@form[:name]} type="text" label={gettext("Name")} required /> <.input field={@form[:description]} type="textarea" label={gettext("Description")} rows="3" />
<%= if @form.errors[:permission_set_name] do %> <%= for error <- List.wrap(@form.errors[:permission_set_name]) do %> <% {msg, _opts} = if is_tuple(error), do: error, else: {error, []} %>

<.icon name="hero-exclamation-circle" class="size-5" /> {msg}

<% end %> <% end %>
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit"> {gettext("Save Role")} <.button navigate={return_path(@return_to, @role)} type="button"> {gettext("Cancel")}
""" end @impl true def mount(params, _session, socket) do case params["id"] do nil -> action = gettext("New") page_title = action <> " " <> gettext("Role") {:ok, socket |> assign(:return_to, return_to(params["return_to"])) |> assign(:role, nil) |> assign(:page_title, page_title) |> assign_form()} id -> try do case Ash.get( Mv.Authorization.Role, id, domain: Mv.Authorization, actor: socket.assigns[:current_user] ) do {:ok, role} -> action = gettext("Edit") page_title = action <> " " <> gettext("Role") {:ok, socket |> assign(:return_to, return_to(params["return_to"])) |> assign(:role, role) |> assign(:page_title, page_title) |> assign_form()} {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} -> {:ok, socket |> put_flash(:error, gettext("Role not found.")) |> redirect(to: ~p"/admin/roles")} {:error, error} -> {:ok, socket |> put_flash(:error, format_error(error)) |> redirect(to: ~p"/admin/roles")} end rescue e in [Ash.Error.Invalid] -> # Handle exceptions that Ash.get might throw (e.g., policy violations) case e do %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]} -> {:ok, socket |> put_flash(:error, gettext("Role not found.")) |> redirect(to: ~p"/admin/roles")} _ -> reraise e, __STACKTRACE__ end end end end @spec return_to(String.t() | nil) :: String.t() defp return_to("show"), do: "show" defp return_to(_), do: "index" @impl true def handle_event("validate", %{"role" => role_params}, socket) do validated_form = AshPhoenix.Form.validate(socket.assigns.form, role_params) {:noreply, assign(socket, form: validated_form)} end def handle_event("save", %{"role" => role_params}, socket) do case AshPhoenix.Form.submit(socket.assigns.form, params: role_params) do {:ok, role} -> notify_parent({:saved, role}) redirect_path = if socket.assigns.return_to == "show" do ~p"/admin/roles/#{role.id}" else ~p"/admin/roles" end socket = socket |> put_flash(:info, gettext("Role saved successfully.")) |> push_navigate(to: redirect_path) {:noreply, socket} {:error, form} -> {:noreply, assign(socket, form: form)} end end @spec notify_parent(any()) :: any() defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) @spec assign_form(Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t() defp assign_form(%{assigns: %{role: role, current_user: actor}} = socket) do form = if role do AshPhoenix.Form.for_update(role, :update_role, domain: Mv.Authorization, as: "role", actor: actor ) else AshPhoenix.Form.for_create( Mv.Authorization.Role, :create_role, domain: Mv.Authorization, as: "role", actor: actor ) end assign(socket, form: to_form(form)) end defp all_permission_sets do PermissionSets.all_permission_sets() |> Enum.map(&Atom.to_string/1) end defp format_permission_set_option("own_data"), do: gettext("own_data - Access only to own data") defp format_permission_set_option("read_only"), do: gettext("read_only - Read access to all data") defp format_permission_set_option("normal_user"), do: gettext("normal_user - Create/Read/Update access") defp format_permission_set_option("admin"), do: gettext("admin - Unrestricted access") defp format_permission_set_option(set), do: set @spec return_path(String.t(), Mv.Authorization.Role.t() | nil) :: String.t() defp return_path("index", _role), do: ~p"/admin/roles" defp return_path("show", role) when not is_nil(role), do: ~p"/admin/roles/#{role.id}" defp return_path("show", _role), do: ~p"/admin/roles" defp return_path(_, role) when not is_nil(role), do: ~p"/admin/roles/#{role.id}" defp return_path(_, _role), do: ~p"/admin/roles" end