mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 13:21:11 +00:00
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:
@@ -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]">
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user