feat: add groups administration #372
This commit is contained in:
parent
f05fae3ea3
commit
6faa9847f4
9 changed files with 701 additions and 7 deletions
201
lib/mv_web/live/group_live/form.ex
Normal file
201
lib/mv_web/live/group_live/form.ex
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
defmodule MvWeb.GroupLive.Form do
|
||||
@moduledoc """
|
||||
LiveView form for creating and editing groups.
|
||||
|
||||
## Features
|
||||
- Create new groups with name and description
|
||||
- Edit existing group details (name and description)
|
||||
- Slug is automatically generated and immutable
|
||||
- Form validation with visual feedback
|
||||
|
||||
## Security
|
||||
- Only admin users can create/edit groups
|
||||
- Non-admin users are redirected
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
import MvWeb.LiveHelpers, only: [current_actor: 1, submit_form: 3]
|
||||
import MvWeb.Authorization
|
||||
|
||||
alias Mv.Membership
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
actor = current_actor(socket)
|
||||
|
||||
# Check authorization
|
||||
action = if params["slug"], do: :update, else: :create
|
||||
resource = Mv.Membership.Group
|
||||
|
||||
unless can?(actor, action, resource) do
|
||||
{:ok, redirect(socket, to: ~p"/groups")}
|
||||
else
|
||||
socket =
|
||||
case params["slug"] do
|
||||
nil ->
|
||||
# New group
|
||||
socket
|
||||
|> assign(:group, nil)
|
||||
|> assign(:page_title, gettext("Create Group"))
|
||||
|> assign(:return_to, "index")
|
||||
|
||||
slug ->
|
||||
# Edit existing group
|
||||
case Membership.get_group_by_slug(slug, actor: actor) do
|
||||
{:ok, nil} ->
|
||||
socket
|
||||
|> put_flash(:error, gettext("Group not found."))
|
||||
|> redirect(to: ~p"/groups")
|
||||
|
||||
{:ok, group} ->
|
||||
socket
|
||||
|> assign(:group, group)
|
||||
|> assign(:page_title, gettext("Edit Group"))
|
||||
|> assign(:return_to, "show")
|
||||
|
||||
{:error, _error} ->
|
||||
socket
|
||||
|> put_flash(:error, gettext("Failed to load group."))
|
||||
|> redirect(to: ~p"/groups")
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, assign_form(socket)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
# Handle slug-based routing for edit
|
||||
case params do
|
||||
%{"slug" => slug} when is_binary(slug) ->
|
||||
actor = current_actor(socket)
|
||||
|
||||
case Membership.get_group_by_slug(slug, actor: actor) do
|
||||
{:ok, nil} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, gettext("Group not found."))
|
||||
|> redirect(to: ~p"/groups")}
|
||||
|
||||
{:ok, group} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:group, group)
|
||||
|> assign(:page_title, gettext("Edit Group"))
|
||||
|> assign(:return_to, "show")
|
||||
|> assign_form()}
|
||||
|
||||
{:error, _error} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, gettext("Failed to load group."))
|
||||
|> redirect(to: ~p"/groups")}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<.form for={@form} id="group-form" phx-change="validate" phx-submit="save">
|
||||
<%!-- Header with Back button, Title, and Save button --%>
|
||||
<div class="flex items-center justify-between gap-4 pb-4">
|
||||
<.button navigate={return_path(@return_to, @group)} type="button">
|
||||
<.icon name="hero-arrow-left" class="size-4" />
|
||||
{gettext("Back")}
|
||||
</.button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-center flex-1">
|
||||
{@page_title}
|
||||
</h1>
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary" type="submit">
|
||||
{gettext("Save")}
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<.input field={@form[:name]} label={gettext("Name")} required />
|
||||
<.input
|
||||
field={@form[:description]}
|
||||
type="textarea"
|
||||
label={gettext("Description")}
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"group" => group_params}, socket) do
|
||||
validated_form = AshPhoenix.Form.validate(socket.assigns.form, group_params)
|
||||
{:noreply, assign(socket, form: validated_form)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"group" => group_params}, socket) do
|
||||
actor = current_actor(socket)
|
||||
|
||||
case submit_form(socket.assigns.form, group_params, actor) do
|
||||
{:ok, group} ->
|
||||
notify_parent({:saved, group})
|
||||
|
||||
redirect_path =
|
||||
if socket.assigns.return_to == "show" do
|
||||
~p"/groups/#{group.slug}"
|
||||
else
|
||||
~p"/groups"
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, gettext("Group saved successfully."))
|
||||
|> push_navigate(to: redirect_path)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: assigns} = socket) do
|
||||
group = assigns.group
|
||||
actor = assigns.current_user
|
||||
|
||||
form =
|
||||
if group do
|
||||
AshPhoenix.Form.for_update(
|
||||
group,
|
||||
:update,
|
||||
api: Membership,
|
||||
as: "group",
|
||||
actor: actor
|
||||
)
|
||||
else
|
||||
AshPhoenix.Form.for_create(
|
||||
Mv.Membership.Group,
|
||||
:create,
|
||||
api: Membership,
|
||||
as: "group",
|
||||
actor: actor
|
||||
)
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
|
||||
defp return_path("index", _group), do: ~p"/groups"
|
||||
defp return_path("show", group) when not is_nil(group), do: ~p"/groups/#{group.slug}"
|
||||
defp return_path("show", _group), do: ~p"/groups"
|
||||
defp return_path(_, group) when not is_nil(group), do: ~p"/groups/#{group.slug}"
|
||||
defp return_path(_, _group), do: ~p"/groups"
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue