From afbbf6ff254eacbcedcbaaa004f702a0290611a1 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:42:59 +0800 Subject: [PATCH] feat(settings): redesign members tab with dropdown menu Replace inline select + delete button with a three-dot dropdown menu per member. Adds role descriptions, owner self-demotion protection, and a cleaner list layout with ring border. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../settings/_components/members-tab.tsx | 181 +++++++++++------- 1 file changed, 108 insertions(+), 73 deletions(-) diff --git a/apps/web/app/(dashboard)/settings/_components/members-tab.tsx b/apps/web/app/(dashboard)/settings/_components/members-tab.tsx index fd9f23c6..fd101e08 100644 --- a/apps/web/app/(dashboard)/settings/_components/members-tab.tsx +++ b/apps/web/app/(dashboard)/settings/_components/members-tab.tsx @@ -1,12 +1,12 @@ "use client"; import { useState } from "react"; -import { Crown, Shield, User, Plus, Trash2, Users } from "lucide-react"; -import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { Crown, Shield, User, Plus, MoreHorizontal, UserMinus, Users } from "lucide-react"; import type { MemberWithUser, MemberRole } from "@/shared/types"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { AlertDialog, AlertDialogContent, @@ -24,15 +24,25 @@ import { SelectContent, SelectItem, } from "@/components/ui/select"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} from "@/components/ui/dropdown-menu"; import { toast } from "sonner"; import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore } from "@/features/workspace"; import { api } from "@/shared/api"; -const roleConfig: Record = { - owner: { label: "Owner", icon: Crown }, - admin: { label: "Admin", icon: Shield }, - member: { label: "Member", icon: User }, +const roleConfig: Record = { + owner: { label: "Owner", icon: Crown, description: "Full access, manage all settings" }, + admin: { label: "Admin", icon: Shield, description: "Manage members and settings" }, + member: { label: "Member", icon: User, description: "Create and work on issues" }, }; function MemberRow({ @@ -54,59 +64,82 @@ function MemberRow({ }) { const rc = roleConfig[member.role]; const RoleIcon = rc.icon; - const canEditRole = canManage && (!isSelf || canManageOwners) && (member.role !== "owner" || canManageOwners); + const canEditRole = canManage && !isSelf && (member.role !== "owner" || canManageOwners); const canRemove = canManage && !isSelf && (member.role !== "owner" || canManageOwners); + const showMenu = canEditRole || canRemove; return ( - - -
- {member.name - .split(" ") - .map((w) => w[0]) - .join("") - .toUpperCase() - .slice(0, 2)} -
-
-
{member.name}
-
{member.email}
-
- {canEditRole ? ( - - ) : ( -
- - {rc.label} -
- )} - {canRemove && ( - - - - - } - /> - Remove member - - )} -
-
+
+
+ {member.name + .split(" ") + .map((w) => w[0]) + .join("") + .toUpperCase() + .slice(0, 2)} +
+
+
{member.name}
+
{member.email}
+
+ {showMenu && ( + + + + + } + /> + + {canEditRole && ( + + + + Change role + + + {(Object.entries(roleConfig) as [MemberRole, (typeof roleConfig)[MemberRole]][]).map( + ([role, config]) => { + if (role === "owner" && !canManageOwners) return null; + const Icon = config.icon; + return ( + onRoleChange(role)} + > + +
+ {config.label} + + {config.description} + +
+ {member.role === role && ( + + )} +
+ ); + } + )} +
+
+ )} + {canEditRole && canRemove && } + {canRemove && ( + + + Remove from workspace + + )} +
+
+ )} + + + {rc.label} + +
); } @@ -228,23 +261,25 @@ export function MembersTab() { )} -
- {members.map((m) => ( - handleRoleChange(m.id, role)} - onRemove={() => handleRemoveMember(m)} - /> - ))} - {members.length === 0 && ( -

No members found.

- )} -
+ {members.length > 0 ? ( +
+ {members.map((m, i) => ( +
0 ? "border-t border-border/50" : ""}> + handleRoleChange(m.id, role)} + onRemove={() => handleRemoveMember(m)} + /> +
+ ))} +
+ ) : ( +

No members found.

+ )} { if (!v) setConfirmAction(null); }}>