feat: make organization selector larger and searchable (#5052)

Replaces the organization dropdown with a Popover + Command combobox: wider panel and a search input to filter organizations by name.

Closes #5043
This commit is contained in:
Mauricio Siu
2026-08-11 20:55:26 -06:00
committed by GitHub
parent 34c5b688ac
commit 3cad115155
2 changed files with 160 additions and 130 deletions
@@ -14,7 +14,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
@@ -103,16 +102,19 @@ export function AddOrganization({ organizationId }: Props) {
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{organizationId ? (
<DropdownMenuItem
className="group cursor-pointer hover:bg-blue-500/10"
onSelect={(e) => e.preventDefault()}
<Button
type="button"
variant="ghost"
size="icon"
className="group hover:bg-blue-500/10"
title="Edit organization"
>
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
</DropdownMenuItem>
</Button>
) : (
<DropdownMenuItem
className="gap-2 p-2"
onSelect={(e) => e.preventDefault()}
<button
type="button"
className="flex w-full items-center gap-2 rounded-md p-2 text-left text-sm hover:bg-accent hover:text-accent-foreground"
>
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
@@ -120,7 +122,7 @@ export function AddOrganization({ organizationId }: Props) {
<div className="font-medium text-muted-foreground">
Add organization
</div>
</DropdownMenuItem>
</button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
+149 -121
View File
@@ -53,14 +53,26 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import {
SIDEBAR_COOKIE_NAME,
@@ -565,6 +577,8 @@ function SidebarLogo() {
const [_activeTeam, setActiveTeam] = useState<
typeof activeOrganization | null
>(null);
const [organizationSelectorOpen, setOrganizationSelectorOpen] =
useState(false);
useEffect(() => {
if (activeOrganization) {
@@ -587,8 +601,11 @@ function SidebarLogo() {
>
{/* Organization Logo and Selector */}
<SidebarMenuItem className={"w-full"}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Popover
open={organizationSelectorOpen}
onOpenChange={setOrganizationSelectorOpen}
>
<PopoverTrigger asChild>
<SidebarMenuButton
size={isCollapsed ? "sm" : "lg"}
className={cn(
@@ -637,145 +654,156 @@ function SidebarLogo() {
className={cn("ml-auto", isCollapsed && "hidden")}
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-64 rounded-lg max-h-[min(70vh,28rem)] flex flex-col"
</PopoverTrigger>
<PopoverContent
className="w-96 p-0"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground shrink-0">
Organizations
</DropdownMenuLabel>
<div className="overflow-y-auto overflow-x-hidden min-h-0 -mx-1 px-1">
{organizations?.map((org) => {
const isDefault = org.members?.[0]?.isDefault ?? false;
return (
<div
className="flex flex-row items-center justify-between gap-1"
key={org.name}
>
<DropdownMenuItem
onClick={async () => {
await authClient.organization.setActive({
organizationId: org.id,
});
window.location.reload();
}}
className="flex min-w-0 flex-1 gap-2 p-2"
>
<div className="flex size-6 shrink-0 items-center justify-center rounded-sm border">
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-4" : "size-5",
)}
logoUrl={org.logo ?? undefined}
/>
</div>
<span className="truncate">{org.name}</span>
</DropdownMenuItem>
<div className="flex shrink-0 items-center gap-2">
<Button
variant="ghost"
size="icon"
className={cn(
"group",
isDefault
? "hover:bg-yellow-500/10"
: "hover:bg-blue-500/10",
)}
isLoading={isSettingDefault && !isDefault}
disabled={isDefault}
onClick={async (e) => {
if (isDefault) return;
e.stopPropagation();
await setDefaultOrganization({
<Command>
<CommandInput
placeholder="Search organizations..."
className="h-9"
/>
<CommandList className="max-h-[min(60vh,24rem)]">
<CommandEmpty>No organizations found.</CommandEmpty>
<CommandGroup heading="Organizations">
{organizations?.map((org) => {
const isDefault = org.members?.[0]?.isDefault ?? false;
return (
<CommandItem
key={org.id}
value={org.name}
onSelect={async () => {
setOrganizationSelectorOpen(false);
await authClient.organization.setActive({
organizationId: org.id,
})
.then(() => {
refetch();
toast.success("Default organization updated");
})
.catch((error) => {
toast.error(
error?.message ||
"Error setting default organization",
);
});
});
window.location.reload();
}}
title={
isDefault
? "Default organization"
: "Set as default"
}
className="flex items-center justify-between gap-1"
>
{isDefault ? (
<Star
fill="#eab308"
stroke="#eab308"
className="size-4 text-yellow-500"
/>
) : (
<Star
fill="none"
stroke="currentColor"
className="size-4 text-gray-400 group-hover:text-blue-500 transition-colors"
/>
)}
</Button>
{org.ownerId === session?.user?.id && (
<>
<AddOrganization organizationId={org.id} />
<DialogAction
title="Delete Organization"
description="Are you sure you want to delete this organization?"
type="destructive"
onClick={async () => {
await deleteOrganization({
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex size-6 shrink-0 items-center justify-center rounded-sm border">
<Logo
className={cn(
"transition-all",
state === "collapsed" ? "size-4" : "size-5",
)}
logoUrl={org.logo ?? undefined}
/>
</div>
<span className="truncate">{org.name}</span>
</div>
<div
className="flex shrink-0 items-center gap-2"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<Button
variant="ghost"
size="icon"
className={cn(
"group",
isDefault
? "hover:bg-yellow-500/10"
: "hover:bg-blue-500/10",
)}
isLoading={isSettingDefault && !isDefault}
disabled={isDefault}
onClick={async (e) => {
if (isDefault) return;
e.stopPropagation();
await setDefaultOrganization({
organizationId: org.id,
})
.then(() => {
refetch();
toast.success(
"Organization deleted successfully",
"Default organization updated",
);
})
.catch((error) => {
toast.error(
error?.message ||
"Error deleting organization",
"Error setting default organization",
);
});
}}
title={
isDefault
? "Default organization"
: "Set as default"
}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</>
)}
</div>
</div>
);
})}
</div>
{(user?.role === "owner" ||
user?.role === "admin" ||
isCloud) && (
<>
<DropdownMenuSeparator />
<AddOrganization />
</>
)}
</DropdownMenuContent>
</DropdownMenu>
{isDefault ? (
<Star
fill="#eab308"
stroke="#eab308"
className="size-4 text-yellow-500"
/>
) : (
<Star
fill="none"
stroke="currentColor"
className="size-4 text-gray-400 group-hover:text-blue-500 transition-colors"
/>
)}
</Button>
{org.ownerId === session?.user?.id && (
<>
<AddOrganization organizationId={org.id} />
<DialogAction
title="Delete Organization"
description="Are you sure you want to delete this organization?"
type="destructive"
onClick={async () => {
await deleteOrganization({
organizationId: org.id,
})
.then(() => {
refetch();
toast.success(
"Organization deleted successfully",
);
})
.catch((error) => {
toast.error(
error?.message ||
"Error deleting organization",
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</>
)}
</div>
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
{(user?.role === "owner" ||
user?.role === "admin" ||
isCloud) && (
<div className="border-t p-1">
<AddOrganization />
</div>
)}
</Command>
</PopoverContent>
</Popover>
</SidebarMenuItem>
{/* Notification Bell */}