feat: add groups administration #372
This commit is contained in:
parent
f05fae3ea3
commit
6faa9847f4
9 changed files with 701 additions and 7 deletions
132
lib/mv_web/live/group_live/index.ex
Normal file
132
lib/mv_web/live/group_live/index.ex
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
defmodule MvWeb.GroupLive.Index do
|
||||
@moduledoc """
|
||||
LiveView for displaying and managing the groups list.
|
||||
|
||||
## Features
|
||||
- List all groups with name, description, and member count
|
||||
- Create new groups
|
||||
- Navigate to group details and edit forms
|
||||
- Delete groups (with confirmation)
|
||||
|
||||
## Security
|
||||
- Admin users can create, edit, and delete groups
|
||||
- Read-only users can view groups but not manage them
|
||||
- Non-admin users are redirected
|
||||
"""
|
||||
use MvWeb, :live_view
|
||||
|
||||
import MvWeb.LiveHelpers, only: [current_actor: 1, ash_actor_opts: 1]
|
||||
import MvWeb.Authorization
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
actor = current_actor(socket)
|
||||
|
||||
# Check if user can read groups
|
||||
unless can?(actor, :read, Mv.Membership.Group) do
|
||||
{:ok, redirect(socket, to: ~p"/members")}
|
||||
else
|
||||
groups = load_groups(actor)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, gettext("Groups"))
|
||||
|> assign(:groups, groups)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold">{gettext("Groups")}</h1>
|
||||
<%= if can?(@current_user, :create, Mv.Membership.Group) do %>
|
||||
<.button navigate={~p"/groups/new"} variant="primary">
|
||||
<.icon name="hero-plus" class="size-4 mr-2" />
|
||||
{gettext("Create Group")}
|
||||
</.button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= if Enum.empty?(@groups) do %>
|
||||
<div class="text-center py-12">
|
||||
<p class="text-base-content/70">{gettext("No groups")}</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{gettext("Name")}</th>
|
||||
<th>{gettext("Description")}</th>
|
||||
<th>{gettext("Members")}</th>
|
||||
<th>{gettext("Actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for group <- @groups do %>
|
||||
<tr>
|
||||
<td>
|
||||
<.link navigate={~p"/groups/#{group.slug}"} class="link link-primary">
|
||||
{group.name}
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<%= if group.description do %>
|
||||
{group.description}
|
||||
<% else %>
|
||||
<span class="text-base-content/50 italic">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<%= if group.member_count do %>
|
||||
{group.member_count}
|
||||
<% else %>
|
||||
0
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<.link navigate={~p"/groups/#{group.slug}"} class="btn btn-sm btn-ghost">
|
||||
{gettext("View")}
|
||||
</.link>
|
||||
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
|
||||
<.link
|
||||
navigate={~p"/groups/#{group.slug}/edit"}
|
||||
class="btn btn-sm btn-ghost"
|
||||
>
|
||||
{gettext("Edit")}
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@spec load_groups(map() | nil) :: [Mv.Membership.Group.t()]
|
||||
defp load_groups(actor) do
|
||||
require Ash.Query
|
||||
|
||||
query =
|
||||
Mv.Membership.Group
|
||||
|> Ash.Query.load(:member_count)
|
||||
|
||||
opts = ash_actor_opts(actor)
|
||||
|
||||
case Ash.read(query, opts) do
|
||||
{:ok, groups} ->
|
||||
Enum.sort_by(groups, & &1.name)
|
||||
|
||||
{:error, _} ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue