Groups ui team ui
This commit is contained in:
parent
17cb689753
commit
7e1266a965
156
app/api/users/[userId]/groups/route.ts
Normal file
156
app/api/users/[userId]/groups/route.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "@/app/api/auth/options";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
async function getAdminToken() {
|
||||
try {
|
||||
const tokenResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
client_id: process.env.KEYCLOAK_CLIENT_ID!,
|
||||
client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await tokenResponse.json();
|
||||
if (!tokenResponse.ok || !data.access_token) {
|
||||
console.error('Token Error:', data);
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.access_token;
|
||||
} catch (error) {
|
||||
console.error('Token Error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: Request, props: { params: Promise<{ userId: string }> }) {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await getAdminToken();
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get user groups
|
||||
const groupsResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}/groups`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!groupsResponse.ok) {
|
||||
const errorData = await groupsResponse.json();
|
||||
console.error("Failed to get user groups:", errorData);
|
||||
return NextResponse.json({ error: "Failed to get user groups" }, { status: groupsResponse.status });
|
||||
}
|
||||
|
||||
const groups = await groupsResponse.json();
|
||||
return NextResponse.json(groups);
|
||||
} catch (error) {
|
||||
console.error("Error in get user groups:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request, props: { params: Promise<{ userId: string }> }) {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { groupId } = await req.json();
|
||||
|
||||
const token = await getAdminToken();
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Add user to group
|
||||
const addResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}/groups/${groupId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!addResponse.ok) {
|
||||
const errorData = await addResponse.json();
|
||||
console.error("Failed to add user to group:", errorData);
|
||||
return NextResponse.json({ error: "Failed to add user to group" }, { status: addResponse.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error in add user to group:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request, props: { params: Promise<{ userId: string }> }) {
|
||||
const params = await props.params;
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const groupId = searchParams.get('groupId');
|
||||
|
||||
if (!groupId) {
|
||||
return NextResponse.json({ error: "groupId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const token = await getAdminToken();
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: "Erreur d'authentification" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Remove user from group
|
||||
const removeResponse = await fetch(
|
||||
`${process.env.KEYCLOAK_BASE_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${params.userId}/groups/${groupId}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!removeResponse.ok) {
|
||||
const errorData = await removeResponse.json();
|
||||
console.error("Failed to remove user from group:", errorData);
|
||||
return NextResponse.json({ error: "Failed to remove user from group" }, { status: removeResponse.status });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error in remove user from group:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2 } from "lucide-react";
|
||||
import { Search, Plus, MoreHorizontal, Trash2, Edit2, Users, UserPlus, X, Check, Loader2, FolderKanban } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
// Types
|
||||
interface User {
|
||||
@ -25,7 +33,7 @@ interface Group {
|
||||
}
|
||||
|
||||
type ActiveTab = "users" | "groups";
|
||||
type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" };
|
||||
type EditMode = null | { type: "user" | "group"; id: string; action: "edit" | "roles" | "members" | "groups" };
|
||||
|
||||
export default function EquipePage() {
|
||||
const { toast } = useToast();
|
||||
@ -46,6 +54,21 @@ export default function EquipePage() {
|
||||
// Group members state
|
||||
const [groupMembers, setGroupMembers] = useState<User[]>([]);
|
||||
const [availableUsers, setAvailableUsers] = useState<User[]>([]);
|
||||
|
||||
// User groups state
|
||||
const [userGroups, setUserGroups] = useState<Group[]>([]);
|
||||
const [availableGroups, setAvailableGroups] = useState<Group[]>([]);
|
||||
|
||||
// New user dialog state
|
||||
const [newUserDialogOpen, setNewUserDialogOpen] = useState(false);
|
||||
const [newUserData, setNewUserData] = useState({
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
roles: [] as string[],
|
||||
});
|
||||
|
||||
// Fetch data on mount
|
||||
useEffect(() => {
|
||||
@ -114,6 +137,23 @@ export default function EquipePage() {
|
||||
setEditData({ roles: user.roles || [] });
|
||||
};
|
||||
|
||||
const handleManageUserGroups = async (user: User) => {
|
||||
setActionLoading(user.id);
|
||||
try {
|
||||
const groupsRes = await fetch(`/api/users/${user.id}/groups`);
|
||||
if (groupsRes.ok) {
|
||||
const userGroupsData = await groupsRes.json();
|
||||
setUserGroups(Array.isArray(userGroupsData) ? userGroupsData : []);
|
||||
setAvailableGroups(groups.filter(g => !userGroupsData.some((ug: Group) => ug.id === g.id)));
|
||||
}
|
||||
setEditMode({ type: "user", id: user.id, action: "groups" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Impossible de charger les groupes", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const saveUserEdit = async (userId: string) => {
|
||||
setActionLoading(userId);
|
||||
try {
|
||||
@ -319,6 +359,107 @@ export default function EquipePage() {
|
||||
setEditData({});
|
||||
setGroupMembers([]);
|
||||
setAvailableUsers([]);
|
||||
setUserGroups([]);
|
||||
setAvailableGroups([]);
|
||||
};
|
||||
|
||||
const createUser = async () => {
|
||||
setActionLoading("new-user");
|
||||
try {
|
||||
const response = await fetch("/api/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...newUserData,
|
||||
roles: newUserData.roles,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to create user");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setUsers(prev => [...prev, data.user]);
|
||||
setNewUserDialogOpen(false);
|
||||
setNewUserData({
|
||||
username: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
roles: [],
|
||||
});
|
||||
toast({ title: "Succès", description: "Utilisateur créé" });
|
||||
fetchData(); // Refresh data to get updated counts
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: error instanceof Error ? error.message : "Échec de la création",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const addUserToGroup = async (groupId: string) => {
|
||||
if (!editMode || editMode.action !== "groups") return;
|
||||
|
||||
setActionLoading(groupId);
|
||||
try {
|
||||
const response = await fetch(`/api/users/${editMode.id}/groups`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ groupId })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to add user to group");
|
||||
|
||||
const group = availableGroups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
setUserGroups(prev => [...prev, group]);
|
||||
setAvailableGroups(prev => prev.filter(g => g.id !== groupId));
|
||||
setGroups(prev => prev.map(g =>
|
||||
g.id === groupId ? { ...g, membersCount: g.membersCount + 1 } : g
|
||||
));
|
||||
}
|
||||
|
||||
toast({ title: "Succès", description: "Utilisateur ajouté au groupe" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec de l'ajout", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const removeUserFromGroup = async (groupId: string) => {
|
||||
if (!editMode || editMode.action !== "groups") return;
|
||||
|
||||
setActionLoading(groupId);
|
||||
try {
|
||||
const response = await fetch(`/api/users/${editMode.id}/groups?groupId=${groupId}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to remove user from group");
|
||||
|
||||
const group = userGroups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
setUserGroups(prev => prev.filter(g => g.id !== groupId));
|
||||
setAvailableGroups(prev => [...prev, group]);
|
||||
setGroups(prev => prev.map(g =>
|
||||
g.id === groupId ? { ...g, membersCount: Math.max(0, g.membersCount - 1) } : g
|
||||
));
|
||||
}
|
||||
|
||||
toast({ title: "Succès", description: "Utilisateur retiré du groupe" });
|
||||
} catch (error) {
|
||||
toast({ title: "Erreur", description: "Échec du retrait", variant: "destructive" });
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -327,16 +468,131 @@ export default function EquipePage() {
|
||||
<div className="bg-white border-b border-gray-100 py-3 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-gray-800 text-base font-medium">Gestion des équipes</h1>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Rechercher..."
|
||||
className="h-9 pl-9 pr-3 py-2 text-sm bg-white text-gray-800 border-gray-200 rounded-md w-60"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
{activeTab === "users" && (
|
||||
<Dialog open={newUserDialogOpen} onOpenChange={setNewUserDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" className="h-9">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Ajouter un utilisateur
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nouvel utilisateur</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="username">Nom d'utilisateur *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={newUserData.username}
|
||||
onChange={(e) => setNewUserData({ ...newUserData, username: e.target.value })}
|
||||
className="mt-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="firstName">Prénom *</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={newUserData.firstName}
|
||||
onChange={(e) => setNewUserData({ ...newUserData, firstName: e.target.value })}
|
||||
className="mt-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="lastName">Nom *</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={newUserData.lastName}
|
||||
onChange={(e) => setNewUserData({ ...newUserData, lastName: e.target.value })}
|
||||
className="mt-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">Email *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={newUserData.email}
|
||||
onChange={(e) => setNewUserData({ ...newUserData, email: e.target.value })}
|
||||
className="mt-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">Mot de passe *</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={newUserData.password}
|
||||
onChange={(e) => setNewUserData({ ...newUserData, password: e.target.value })}
|
||||
className="mt-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-2 block">Rôles</Label>
|
||||
<div className="max-h-40 overflow-y-auto space-y-2 border rounded-md p-3">
|
||||
{roles.map(role => (
|
||||
<label
|
||||
key={role.id}
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newUserData.roles.includes(role.name)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setNewUserData({ ...newUserData, roles: [...newUserData.roles, role.name] });
|
||||
} else {
|
||||
setNewUserData({ ...newUserData, roles: newUserData.roles.filter(r => r !== role.name) });
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{role.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setNewUserDialogOpen(false)}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={createUser}
|
||||
disabled={actionLoading === "new-user" || !newUserData.username || !newUserData.email || !newUserData.password}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{actionLoading === "new-user" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
Créer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Rechercher..."
|
||||
className="h-9 pl-9 pr-3 py-2 text-sm bg-white text-gray-800 border-gray-200 rounded-md w-60"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
@ -436,21 +692,33 @@ export default function EquipePage() {
|
||||
onClick={() => handleEditRoles(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Gérer les rôles"
|
||||
>
|
||||
<UserPlus className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleManageUserGroups(user)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Gérer les groupes"
|
||||
>
|
||||
{actionLoading === user.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<FolderKanban className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteUser(user.id, user.email)}
|
||||
disabled={actionLoading === user.id}
|
||||
className="h-8 w-8 p-0 hover:text-red-600"
|
||||
title="Supprimer"
|
||||
>
|
||||
{actionLoading === user.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
<Trash2 className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
@ -550,6 +818,7 @@ export default function EquipePage() {
|
||||
{editMode.action === "edit" && "Modifier"}
|
||||
{editMode.action === "roles" && "Gérer les rôles"}
|
||||
{editMode.action === "members" && "Gérer les membres"}
|
||||
{editMode.action === "groups" && "Gérer les groupes"}
|
||||
</h3>
|
||||
<Button variant="ghost" size="sm" onClick={cancelEdit} className="h-8 w-8 p-0">
|
||||
<X className="h-4 w-4" />
|
||||
@ -662,6 +931,77 @@ export default function EquipePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manage User Groups Form */}
|
||||
{editMode.type === "user" && editMode.action === "groups" && (
|
||||
<div className="space-y-4">
|
||||
{/* Current Groups */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600 mb-2 block">
|
||||
Groupes actuels ({userGroups.length})
|
||||
</label>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1">
|
||||
{userGroups.length === 0 ? (
|
||||
<p className="text-xs text-gray-400 py-2">Aucun groupe</p>
|
||||
) : (
|
||||
userGroups.map(group => (
|
||||
<div key={group.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||
<span className="text-sm text-gray-700 truncate">
|
||||
{group.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeUserFromGroup(group.id)}
|
||||
disabled={actionLoading === group.id}
|
||||
className="h-6 w-6 p-0 hover:text-red-600"
|
||||
>
|
||||
{actionLoading === group.id ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<X className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Groups */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600 mb-2 block">
|
||||
Ajouter à des groupes
|
||||
</label>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1">
|
||||
{availableGroups.length === 0 ? (
|
||||
<p className="text-xs text-gray-400 py-2">L'utilisateur est dans tous les groupes</p>
|
||||
) : (
|
||||
availableGroups.map(group => (
|
||||
<div key={group.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||
<span className="text-sm text-gray-700 truncate">
|
||||
{group.name}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => addUserToGroup(group.id)}
|
||||
disabled={actionLoading === group.id}
|
||||
className="h-6 w-6 p-0 hover:text-green-600"
|
||||
>
|
||||
{actionLoading === group.id ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manage Members Form */}
|
||||
{editMode.type === "group" && editMode.action === "members" && (
|
||||
<div className="space-y-4">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user