Merge branch 'main' into feat/421_accessibility
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
commit
73382c2c3f
49 changed files with 3415 additions and 1950 deletions
|
|
@ -39,18 +39,18 @@ defmodule MvWeb.GroupLive.Show do
|
|||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"slug" => slug}, _url, socket) do
|
||||
def handle_params(%{"slug" => slug} = params, _url, socket) do
|
||||
actor = current_actor(socket)
|
||||
|
||||
# Check if user can read groups
|
||||
if can?(actor, :read, Mv.Membership.Group) do
|
||||
load_group_by_slug(socket, slug, actor)
|
||||
load_group_by_slug(socket, slug, actor, params)
|
||||
else
|
||||
{:noreply, redirect(socket, to: ~p"/members")}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_group_by_slug(socket, slug, actor) do
|
||||
defp load_group_by_slug(socket, slug, actor, params) do
|
||||
# Load group with members and member_count
|
||||
# Using explicit load ensures efficient preloading of members relationship
|
||||
require Ash.Query
|
||||
|
|
@ -68,10 +68,16 @@ defmodule MvWeb.GroupLive.Show do
|
|||
|> redirect(to: ~p"/groups")}
|
||||
|
||||
{:ok, group} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, group.name)
|
||||
|> assign(:group, group)}
|
||||
open_delete = params["confirm_delete"] == "1" && can?(actor, :destroy, group)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, group.name)
|
||||
|> assign(:group, group)
|
||||
|> assign(:show_delete_modal, open_delete)
|
||||
|> assign(:name_confirmation, "")
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, _error} ->
|
||||
{:noreply,
|
||||
|
|
@ -85,51 +91,44 @@ defmodule MvWeb.GroupLive.Show do
|
|||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_user={@current_user}>
|
||||
<%!-- Header with Back button, Name, and Edit/Delete buttons --%>
|
||||
<div class="flex items-center justify-between gap-4 pb-4">
|
||||
<.button navigate={~p"/groups"} aria-label={gettext("Back to groups list")}>
|
||||
<.icon name="hero-arrow-left" class="size-4" />
|
||||
{gettext("Back")}
|
||||
</.button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-center flex-1">
|
||||
{@group.name}
|
||||
</h1>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<.header>
|
||||
<:leading>
|
||||
<.button
|
||||
navigate={~p"/groups"}
|
||||
variant="neutral"
|
||||
aria-label={gettext("Back to groups list")}
|
||||
>
|
||||
<.icon name="hero-arrow-left" class="size-4" />
|
||||
{gettext("Back")}
|
||||
</.button>
|
||||
</:leading>
|
||||
{@group.name}
|
||||
<:actions>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<.button
|
||||
variant="primary"
|
||||
navigate={~p"/groups/#{@group.slug}/edit"}
|
||||
data-testid="group-show-edit-btn"
|
||||
>
|
||||
{gettext("Edit")}
|
||||
<.icon name="hero-pencil-square" /> {gettext("Edit group")}
|
||||
</.button>
|
||||
<% end %>
|
||||
<%= if can?(@current_user, :destroy, @group) do %>
|
||||
<.button
|
||||
class="btn-error"
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="group-show-delete-btn"
|
||||
>
|
||||
{gettext("Delete")}
|
||||
</.button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<%!-- Group Information --%>
|
||||
<div class="max-w-2xl space-y-6 mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Description")}</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<%= if @group.description && String.trim(@group.description) != "" do %>
|
||||
<p class="whitespace-pre-wrap">{@group.description}</p>
|
||||
<% else %>
|
||||
<p class="text-base-content/50 italic">{gettext("No description")}</p>
|
||||
<% end %>
|
||||
<div class="mt-6 space-y-6">
|
||||
<%!-- Group Information --%>
|
||||
<div class="max-w-2xl space-y-6 mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Description")}</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<%= if @group.description && String.trim(@group.description) != "" do %>
|
||||
<p class="whitespace-pre-wrap">{@group.description}</p>
|
||||
<% else %>
|
||||
<p class="text-base-content/50 italic">{gettext("No description")}</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold mb-2">{gettext("Members")}</h2>
|
||||
|
|
@ -150,22 +149,26 @@ defmodule MvWeb.GroupLive.Show do
|
|||
<form phx-change="search_members" class="flex-1">
|
||||
<div class="relative">
|
||||
<div class="input input-bordered join-item w-full flex flex-wrap gap-1 items-center py-1 px-2">
|
||||
<%= for member <- @selected_members do %>
|
||||
<%= for member <- @selected_members do %>
|
||||
<.badge variant="primary" style="outline" class="flex items-center gap-1">
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs p-0 h-4 w-4 min-h-0"
|
||||
phx-click="remove_selected_member"
|
||||
phx-value-member_id={member.id}
|
||||
aria-label={
|
||||
gettext("Remove %{name}",
|
||||
name: MvWeb.Helpers.MemberHelpers.display_name(member)
|
||||
)
|
||||
}
|
||||
>
|
||||
<.icon name="hero-x-mark" class="size-3" />
|
||||
</button>
|
||||
<.tooltip content={gettext("Remove")} position="top">
|
||||
<.button
|
||||
type="button"
|
||||
variant="icon"
|
||||
size="sm"
|
||||
phx-click="remove_selected_member"
|
||||
phx-value-member_id={member.id}
|
||||
aria-label={
|
||||
gettext("Remove %{name}",
|
||||
name: MvWeb.Helpers.MemberHelpers.display_name(member)
|
||||
)
|
||||
}
|
||||
class="p-0 h-4 w-4 min-h-0"
|
||||
>
|
||||
<.icon name="hero-x-mark" class="size-3" />
|
||||
</.button>
|
||||
</.tooltip>
|
||||
</.badge>
|
||||
<% end %>
|
||||
<input
|
||||
|
|
@ -236,24 +239,26 @@ defmodule MvWeb.GroupLive.Show do
|
|||
<% end %>
|
||||
</div>
|
||||
</form>
|
||||
<button
|
||||
<.button
|
||||
type="button"
|
||||
class="btn btn-primary join-item"
|
||||
variant="primary"
|
||||
phx-click="add_selected_members"
|
||||
data-testid="group-show-add-selected-members-btn"
|
||||
disabled={Enum.empty?(@selected_member_ids)}
|
||||
aria-label={gettext("Add members")}
|
||||
class="join-item"
|
||||
>
|
||||
<.icon name="hero-plus" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
class="btn join-item"
|
||||
variant="neutral"
|
||||
phx-click="hide_add_member_input"
|
||||
aria-label={gettext("Cancel")}
|
||||
class="join-item"
|
||||
>
|
||||
{gettext("Cancel")}
|
||||
</button>
|
||||
</.button>
|
||||
</div>
|
||||
<% else %>
|
||||
<.button
|
||||
|
|
@ -268,135 +273,164 @@ defmodule MvWeb.GroupLive.Show do
|
|||
<% end %>
|
||||
|
||||
<%= if Enum.empty?(@group.members || []) do %>
|
||||
<p class="text-base-content/50 italic" data-testid="group-show-no-members">
|
||||
{gettext("No members in this group")}
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto" data-testid="group-show-members-table">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{gettext("Name")}</th>
|
||||
<th>{gettext("Email")}</th>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<th class="w-0">{gettext("Actions")}</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for member <- @group.members do %>
|
||||
<p class="text-base-content/50 italic" data-testid="group-show-no-members">
|
||||
{gettext("No members in this group")}
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="overflow-x-auto" data-testid="group-show-members-table">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<.link
|
||||
navigate={~p"/members/#{member.id}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<%= if member.email do %>
|
||||
<a
|
||||
href={"mailto:#{member.email}"}
|
||||
class="text-blue-700 hover:text-blue-800 underline"
|
||||
>
|
||||
{member.email}
|
||||
</a>
|
||||
<% else %>
|
||||
<span class="text-base-content/50 italic">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<th>{gettext("Name")}</th>
|
||||
<th>{gettext("Email")}</th>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm text-error"
|
||||
phx-click="remove_member"
|
||||
phx-value-member_id={member.id}
|
||||
data-testid="group-show-remove-member"
|
||||
aria-label={gettext("Remove member from group")}
|
||||
data-tooltip={gettext("Remove")}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
</button>
|
||||
</td>
|
||||
<th class="w-0">{gettext("Actions")}</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for member <- @group.members do %>
|
||||
<tr>
|
||||
<td>
|
||||
<.link
|
||||
navigate={~p"/members/#{member.id}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{MvWeb.Helpers.MemberHelpers.display_name(member)}
|
||||
</.link>
|
||||
</td>
|
||||
<td>
|
||||
<%= if member.email do %>
|
||||
<a
|
||||
href={"mailto:#{member.email}"}
|
||||
class="link link-primary"
|
||||
>
|
||||
{member.email}
|
||||
</a>
|
||||
<% else %>
|
||||
<span class="text-base-content/50 italic">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<%= if can?(@current_user, :update, @group) do %>
|
||||
<td>
|
||||
<.tooltip content={gettext("Remove")} position="left">
|
||||
<.button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
phx-click="remove_member"
|
||||
phx-value-member_id={member.id}
|
||||
data-testid="group-show-remove-member"
|
||||
aria-label={gettext("Remove member from group")}
|
||||
>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
</.button>
|
||||
</.tooltip>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Delete Confirmation Modal --%>
|
||||
<%= if assigns[:show_delete_modal] do %>
|
||||
<dialog id="delete-group-modal" class="modal modal-open" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold mb-4">{gettext("Delete Group")}</h3>
|
||||
<p class="mb-4">
|
||||
{gettext("Are you sure you want to delete this group? This action cannot be undone.")}
|
||||
</p>
|
||||
<%= if @group.member_count && @group.member_count > 0 do %>
|
||||
<div class="alert alert-warning mb-4">
|
||||
<.icon name="hero-exclamation-triangle" class="size-5" />
|
||||
<span>
|
||||
{ngettext(
|
||||
"This group has %{count} member. All member-group associations will be permanently deleted.",
|
||||
"This group has %{count} members. All member-group associations will be permanently deleted.",
|
||||
@group.member_count,
|
||||
count: @group.member_count
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<label for="group-name-confirmation" class="label">
|
||||
<span class="label-text">
|
||||
{gettext("To confirm deletion, please enter the group name:")}
|
||||
</span>
|
||||
</label>
|
||||
<div class="p-2 mb-2 font-mono text-lg font-bold break-all rounded bg-base-200">
|
||||
{@group.name}
|
||||
</div>
|
||||
<form phx-change="update_name_confirmation">
|
||||
<input
|
||||
id="group-name-confirmation"
|
||||
name="name"
|
||||
type="text"
|
||||
value={@name_confirmation || ""}
|
||||
placeholder={gettext("Enter the group name to confirm")}
|
||||
autocomplete="off"
|
||||
phx-debounce="200"
|
||||
class="w-full input input-bordered"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<button
|
||||
<%!-- Danger zone: canonical pattern (same as member show) --%>
|
||||
<%= if can?(@current_user, :destroy, @group) do %>
|
||||
<section class="mt-8 mb-6" aria-labelledby="danger-zone-heading">
|
||||
<h2 id="danger-zone-heading" class="text-lg font-semibold mb-3 text-error">
|
||||
{gettext("Danger zone")}
|
||||
</h2>
|
||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
{gettext(
|
||||
"Deleting this group cannot be undone. All member-group associations will be permanently removed."
|
||||
)}
|
||||
</p>
|
||||
<.button
|
||||
variant="danger"
|
||||
type="button"
|
||||
class="btn"
|
||||
phx-click="cancel_delete"
|
||||
aria-label={gettext("Cancel")}
|
||||
phx-click="open_delete_modal"
|
||||
data-testid="group-show-delete-btn"
|
||||
aria-label={gettext("Delete group %{name}", name: @group.name)}
|
||||
>
|
||||
{gettext("Cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-error"
|
||||
phx-click="confirm_delete"
|
||||
phx-value-slug={@group.slug}
|
||||
disabled={(@name_confirmation || "") != @group.name}
|
||||
aria-label={gettext("Delete group")}
|
||||
>
|
||||
{gettext("Delete")}
|
||||
</button>
|
||||
<.icon name="hero-trash" class="size-4" />
|
||||
{gettext("Delete group")}
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<%!-- Delete Confirmation Modal --%>
|
||||
<%= if assigns[:show_delete_modal] do %>
|
||||
<dialog id="delete-group-modal" class="modal modal-open" role="dialog">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold mb-4">{gettext("Delete Group")}</h3>
|
||||
<p class="mb-4">
|
||||
{gettext("Are you sure you want to delete this group? This action cannot be undone.")}
|
||||
</p>
|
||||
<%= if @group.member_count && @group.member_count > 0 do %>
|
||||
<div class="alert alert-warning mb-4">
|
||||
<.icon name="hero-exclamation-triangle" class="size-5" />
|
||||
<span>
|
||||
{ngettext(
|
||||
"This group has %{count} member. All member-group associations will be permanently deleted.",
|
||||
"This group has %{count} members. All member-group associations will be permanently deleted.",
|
||||
@group.member_count,
|
||||
count: @group.member_count
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<label for="group-name-confirmation" class="label">
|
||||
<span class="label-text">
|
||||
{gettext("To confirm deletion, please enter the group name:")}
|
||||
</span>
|
||||
</label>
|
||||
<div class="p-2 mb-2 font-mono text-lg font-bold break-all rounded bg-base-200">
|
||||
{@group.name}
|
||||
</div>
|
||||
<form phx-change="update_name_confirmation">
|
||||
<input
|
||||
id="group-name-confirmation"
|
||||
name="name"
|
||||
type="text"
|
||||
value={@name_confirmation || ""}
|
||||
placeholder={gettext("Enter the group name to confirm")}
|
||||
autocomplete="off"
|
||||
phx-debounce="200"
|
||||
class="w-full input input-bordered"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<.button
|
||||
type="button"
|
||||
variant="neutral"
|
||||
phx-click="cancel_delete"
|
||||
aria-label={gettext("Cancel")}
|
||||
>
|
||||
{gettext("Cancel")}
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
variant="danger"
|
||||
phx-click="confirm_delete"
|
||||
phx-value-slug={@group.slug}
|
||||
disabled={(@name_confirmation || "") != @group.name}
|
||||
aria-label={gettext("Delete group")}
|
||||
>
|
||||
{gettext("Delete")}
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<% end %>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
|
@ -900,7 +934,7 @@ defmodule MvWeb.GroupLive.Show do
|
|||
:ok ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, gettext("Group deleted successfully."))
|
||||
|> put_flash(:success, gettext("Group deleted successfully."))
|
||||
|> redirect(to: ~p"/groups")}
|
||||
|
||||
{:error, error} ->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue