mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-28 21:49:40 +02:00
feat: refactor team management page by removing invite handling logic and enhancing UI components for better user experience
This commit is contained in:
parent
e3cabe8772
commit
802b4fe44e
5 changed files with 149 additions and 164 deletions
|
|
@ -13,6 +13,7 @@ import {
|
||||||
Clock,
|
Clock,
|
||||||
Copy,
|
Copy,
|
||||||
Hash,
|
Hash,
|
||||||
|
Link2,
|
||||||
ShieldUser,
|
ShieldUser,
|
||||||
Trash2,
|
Trash2,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
|
|
@ -136,17 +137,6 @@ function getAvatarInitials(member: Membership): string {
|
||||||
return "U";
|
return "U";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInviteInitials(invite: Invite): string {
|
|
||||||
if (invite.name) {
|
|
||||||
const parts = invite.name.trim().split(/\s+/);
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
||||||
}
|
|
||||||
return invite.name.slice(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
return "IN";
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
export default function TeamManagementPage() {
|
export default function TeamManagementPage() {
|
||||||
|
|
@ -247,7 +237,7 @@ export default function TeamManagementPage() {
|
||||||
const nonOwnerMembers = useMemo(() => members.filter((m) => !m.is_owner), [members]);
|
const nonOwnerMembers = useMemo(() => members.filter((m) => !m.is_owner), [members]);
|
||||||
|
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const totalItems = nonOwnerMembers.length + activeInvites.length;
|
const totalItems = nonOwnerMembers.length;
|
||||||
const lastPage = Math.max(0, Math.ceil(totalItems / PAGE_SIZE) - 1);
|
const lastPage = Math.max(0, Math.ceil(totalItems / PAGE_SIZE) - 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -263,14 +253,6 @@ export default function TeamManagementPage() {
|
||||||
);
|
);
|
||||||
}, [nonOwnerMembers, pageIndex]);
|
}, [nonOwnerMembers, pageIndex]);
|
||||||
|
|
||||||
const paginatedInvites = useMemo(() => {
|
|
||||||
const start = pageIndex * PAGE_SIZE;
|
|
||||||
const end = start + PAGE_SIZE;
|
|
||||||
const inviteStart = Math.max(0, start - nonOwnerMembers.length);
|
|
||||||
const inviteEnd = Math.max(0, end - nonOwnerMembers.length);
|
|
||||||
return activeInvites.slice(inviteStart, inviteEnd);
|
|
||||||
}, [activeInvites, nonOwnerMembers.length, pageIndex]);
|
|
||||||
|
|
||||||
const displayStart = totalItems > 0 ? pageIndex * PAGE_SIZE + 1 : 0;
|
const displayStart = totalItems > 0 ? pageIndex * PAGE_SIZE + 1 : 0;
|
||||||
const displayEnd = Math.min((pageIndex + 1) * PAGE_SIZE, totalItems);
|
const displayEnd = Math.min((pageIndex + 1) * PAGE_SIZE, totalItems);
|
||||||
const canPrev = pageIndex > 0;
|
const canPrev = pageIndex > 0;
|
||||||
|
|
@ -358,7 +340,8 @@ export default function TeamManagementPage() {
|
||||||
<div className="container max-w-5xl mx-auto p-4 md:p-6 lg:p-8 pt-20 md:pt-24 lg:pt-28">
|
<div className="container max-w-5xl mx-auto p-4 md:p-6 lg:p-8 pt-20 md:pt-24 lg:pt-28">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header row: Invite button on left, member count on right */}
|
{/* Header row: Invite button on left, member count on right */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
{canInvite && (
|
{canInvite && (
|
||||||
<CreateInviteDialog
|
<CreateInviteDialog
|
||||||
roles={roles}
|
roles={roles}
|
||||||
|
|
@ -366,7 +349,13 @@ export default function TeamManagementPage() {
|
||||||
searchSpaceId={searchSpaceId}
|
searchSpaceId={searchSpaceId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!canInvite && <div />}
|
{canInvite && activeInvites.length > 0 && (
|
||||||
|
<AllInvitesDialog
|
||||||
|
invites={activeInvites}
|
||||||
|
onRevokeInvite={handleRevokeInvite}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{members.length} {members.length === 1 ? "member" : "members"}
|
{members.length} {members.length === 1 ? "member" : "members"}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -424,16 +413,7 @@ export default function TeamManagementPage() {
|
||||||
index={owners.length + index}
|
index={owners.length + index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{paginatedInvites.map((invite, index) => (
|
{members.length === 0 && (
|
||||||
<InviteRow
|
|
||||||
key={`invite-${invite.id}`}
|
|
||||||
invite={invite}
|
|
||||||
canRevoke={canInvite}
|
|
||||||
onRevokeInvite={handleRevokeInvite}
|
|
||||||
index={owners.length + paginatedMembers.length + index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{members.length === 0 && activeInvites.length === 0 && (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="text-center py-12">
|
<TableCell colSpan={3} className="text-center py-12">
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
|
@ -650,117 +630,6 @@ function MemberRow({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Invite Row ============
|
|
||||||
|
|
||||||
function InviteRow({
|
|
||||||
invite,
|
|
||||||
canRevoke,
|
|
||||||
onRevokeInvite,
|
|
||||||
index,
|
|
||||||
}: {
|
|
||||||
invite: Invite;
|
|
||||||
canRevoke: boolean;
|
|
||||||
onRevokeInvite: (inviteId: number) => Promise<boolean>;
|
|
||||||
index: number;
|
|
||||||
}) {
|
|
||||||
const initials = getInviteInitials(invite);
|
|
||||||
const avatarColor = getAvatarColor(invite.invite_code);
|
|
||||||
const displayName = invite.name || "Unnamed Invite";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.tr
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1, transition: { duration: 0.2, delay: index * 0.02 } }}
|
|
||||||
className="border-b border-border/40 transition-colors hover:bg-muted/30"
|
|
||||||
>
|
|
||||||
<TableCell className="w-[45%] py-2.5 px-4 md:px-6 max-w-0 border-r border-border/40">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-10 w-10 rounded-full flex items-center justify-center text-white font-medium text-sm shrink-0 opacity-60",
|
|
||||||
avatarColor
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{initials}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="font-medium text-sm truncate text-muted-foreground">{displayName}</p>
|
|
||||||
{invite.role?.name && (
|
|
||||||
<p className="text-xs text-muted-foreground/70 truncate">
|
|
||||||
Will join as {invite.role.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="hidden md:table-cell w-[25%] py-2.5 text-sm text-foreground border-r border-border/40">
|
|
||||||
Never
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="w-[30%] text-right py-2.5 px-4 md:px-6">
|
|
||||||
{canRevoke ? (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground/60"
|
|
||||||
>
|
|
||||||
Invited
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" onCloseAutoFocus={(e) => e.preventDefault()}>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
const link = `${window.location.origin}/invite/${invite.invite_code}`;
|
|
||||||
navigator.clipboard.writeText(link);
|
|
||||||
toast.success("Invite link copied");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
Copy invite link
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="text-destructive focus:text-destructive"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
Revoke invite
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Revoke invite?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will permanently delete this invite link. Anyone with this link
|
|
||||||
will no longer be able to join.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={() => onRevokeInvite(invite.id)}
|
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
||||||
>
|
|
||||||
Revoke
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm text-muted-foreground/60">Invited</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</motion.tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Create Invite Dialog ============
|
// ============ Create Invite Dialog ============
|
||||||
|
|
||||||
function CreateInviteDialog({
|
function CreateInviteDialog({
|
||||||
|
|
@ -843,8 +712,8 @@ function CreateInviteDialog({
|
||||||
Invite members
|
Invite members
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none">
|
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none" onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||||
{createdInvite ? (
|
{createdInvite ? (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
|
@ -989,3 +858,119 @@ function CreateInviteDialog({
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ All Invites Dialog ============
|
||||||
|
|
||||||
|
function AllInvitesDialog({
|
||||||
|
invites,
|
||||||
|
onRevokeInvite,
|
||||||
|
}: {
|
||||||
|
invites: Invite[];
|
||||||
|
onRevokeInvite: (inviteId: number) => Promise<boolean>;
|
||||||
|
}) {
|
||||||
|
const [copiedId, setCopiedId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const copyLink = (invite: Invite) => {
|
||||||
|
const link = `${window.location.origin}/invite/${invite.invite_code}`;
|
||||||
|
navigator.clipboard.writeText(link);
|
||||||
|
setCopiedId(invite.id);
|
||||||
|
toast.success("Invite link copied");
|
||||||
|
setTimeout(() => setCopiedId(null), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<Link2 className="h-4 w-4 rotate-315" />
|
||||||
|
Active invites
|
||||||
|
<span className="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-muted text-xs font-medium">
|
||||||
|
{invites.length}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-lg p-4 md:p-6 select-none">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Active Invite Links
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{invites.length} active {invites.length === 1 ? "invite" : "invites"}. Copy a link or revoke access.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="max-h-[320px] overflow-y-auto -mx-1 px-1 space-y-3 py-2">
|
||||||
|
{invites.map((invite) => (
|
||||||
|
<div
|
||||||
|
key={invite.id}
|
||||||
|
className="rounded-lg border border-border/40 p-3 space-y-2.5 transition-colors hover:bg-muted/30"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">
|
||||||
|
{invite.name || "Unnamed invite"}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-x-2 text-xs text-muted-foreground shrink-0">
|
||||||
|
{invite.role?.name && (
|
||||||
|
<span className="rounded bg-muted px-1.5 py-0.5">{invite.role.name}</span>
|
||||||
|
)}
|
||||||
|
{invite.max_uses != null && (
|
||||||
|
<span className="flex items-center gap-1 rounded bg-muted px-1.5 py-0.5">
|
||||||
|
<Hash className="h-3 w-3" />
|
||||||
|
{invite.uses_count}/{invite.max_uses}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{invite.expires_at && (
|
||||||
|
<span className="flex items-center gap-1 rounded bg-muted px-1.5 py-0.5">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{new Date(invite.expires_at).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-destructive/60 hover:text-destructive">
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Revoke invite?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will permanently delete this invite link. Anyone with this link
|
||||||
|
will no longer be able to join.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => onRevokeInvite(invite.id)}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 rounded-md bg-muted p-2">
|
||||||
|
<code className="flex-1 min-w-0 text-sm select-all overflow-x-auto whitespace-nowrap">
|
||||||
|
{typeof window !== "undefined"
|
||||||
|
? `${window.location.origin}/invite/${invite.invite_code}`
|
||||||
|
: `/invite/${invite.invite_code}`}
|
||||||
|
</code>
|
||||||
|
<Button variant="outline" size="sm" className="shrink-0" onClick={() => copyLink(invite)}>
|
||||||
|
{copiedId === invite.id ? (
|
||||||
|
<Check className="h-4 w-4 text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export function Sidebar({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-full flex-col bg-sidebar text-sidebar-foreground overflow-hidden",
|
"relative flex h-full flex-col bg-sidebar text-sidebar-foreground overflow-hidden select-none",
|
||||||
isCollapsed ? "w-[60px] transition-all duration-200" : "",
|
isCollapsed ? "w-[60px] transition-all duration-200" : "",
|
||||||
!isCollapsed && !isResizing ? "transition-all duration-200" : "",
|
!isCollapsed && !isResizing ? "transition-all duration-200" : "",
|
||||||
className
|
className
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronsUpDown, Settings, Users } from "lucide-react";
|
import { ChevronsUpDown, Settings, UserPen } from "lucide-react";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -51,14 +51,14 @@ export function SidebarHeader({
|
||||||
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<ChevronsUpDown className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
<DropdownMenuContent align="start" className="w-48">
|
||||||
<DropdownMenuItem onClick={onManageMembers}>
|
<DropdownMenuItem onClick={onManageMembers}>
|
||||||
<Users className="mr-2 h-4 w-4" />
|
<UserPen className="h-4 w-4" />
|
||||||
{t("manage_members")}
|
{t("manage_members")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={onSettings}>
|
<DropdownMenuItem onClick={onSettings}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{t("search_space_settings")}
|
{t("search_space_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ export function SidebarUserProfile({
|
||||||
<TooltipContent side="right">{displayName}</TooltipContent>
|
<TooltipContent side="right">{displayName}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-56" side="right" align="center" sideOffset={8}>
|
<DropdownMenuContent className="w-48" side="right" align="center" sideOffset={8}>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -191,14 +191,14 @@ export function SidebarUserProfile({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onUserSettings}>
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{setTheme && (
|
{setTheme && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
<Sun className="h-4 w-4" />
|
||||||
{t("theme")}
|
{t("theme")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -216,7 +216,7 @@ export function SidebarUserProfile({
|
||||||
isSelected && "text-primary"
|
isSelected && "text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="mr-2 h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
<span className="flex-1">{t(themeOption.value)}</span>
|
<span className="flex-1">{t(themeOption.value)}</span>
|
||||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -229,7 +229,7 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Languages className="mr-2 h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
{t("language")}
|
{t("language")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -262,7 +262,7 @@ export function SidebarUserProfile({
|
||||||
{isLoggingOut ? (
|
{isLoggingOut ? (
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" className="mr-2" />
|
||||||
) : (
|
) : (
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{isLoggingOut ? t("loggingOut") : t("logout")}
|
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -299,7 +299,7 @@ export function SidebarUserProfile({
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-56" side="top" align="center" sideOffset={4}>
|
<DropdownMenuContent className="w-48" side="top" align="center" sideOffset={4}>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
<UserAvatar avatarUrl={user.avatarUrl} initials={initials} bgColor={bgColor} />
|
||||||
|
|
@ -313,14 +313,14 @@ export function SidebarUserProfile({
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onUserSettings}>
|
<DropdownMenuItem onClick={onUserSettings}>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{setTheme && (
|
{setTheme && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Sun className="mr-2 h-4 w-4" />
|
<Sun className="h-4 w-4" />
|
||||||
{t("theme")}
|
{t("theme")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -338,7 +338,7 @@ export function SidebarUserProfile({
|
||||||
isSelected && "text-primary"
|
isSelected && "text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="mr-2 h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
<span className="flex-1">{t(themeOption.value)}</span>
|
<span className="flex-1">{t(themeOption.value)}</span>
|
||||||
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
{isSelected && <Check className="h-4 w-4 shrink-0" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -351,7 +351,7 @@ export function SidebarUserProfile({
|
||||||
|
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
<Languages className="mr-2 h-4 w-4" />
|
<Languages className="h-4 w-4" />
|
||||||
{t("language")}
|
{t("language")}
|
||||||
</DropdownMenuSubTrigger>
|
</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
|
@ -384,7 +384,7 @@ export function SidebarUserProfile({
|
||||||
{isLoggingOut ? (
|
{isLoggingOut ? (
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" className="mr-2" />
|
||||||
) : (
|
) : (
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{isLoggingOut ? t("loggingOut") : t("logout")}
|
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ export function ModelSelector({
|
||||||
size="sm"
|
size="sm"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={cn("h-8 gap-2 px-3 text-sm border-border/60", className)}
|
className={cn("h-8 gap-2 px-3 text-sm border-border/60 select-none", className)}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -280,7 +280,7 @@ export function ModelSelector({
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-[280px] md:w-[360px] p-0 rounded-lg shadow-lg border-border/60"
|
className="w-[280px] md:w-[360px] p-0 rounded-lg shadow-lg border-border/60 select-none"
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue