OrgSelector.tsx172 lines · main
| 1 | import { useBreakpoint, useParams } from 'common' |
| 2 | import { Boxes, ChevronsUpDown, Plus } from 'lucide-react' |
| 3 | import Link from 'next/link' |
| 4 | import { useRouter } from 'next/router' |
| 5 | import { useState } from 'react' |
| 6 | import { |
| 7 | Command, |
| 8 | CommandEmpty, |
| 9 | CommandGroup, |
| 10 | CommandInput, |
| 11 | CommandItem, |
| 12 | CommandList, |
| 13 | CommandSeparator, |
| 14 | Popover, |
| 15 | PopoverContent, |
| 16 | PopoverTrigger, |
| 17 | ScrollArea, |
| 18 | SidebarMenu, |
| 19 | SidebarMenuButton, |
| 20 | SidebarMenuItem, |
| 21 | } from 'ui' |
| 22 | import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' |
| 23 | |
| 24 | import { OrgSelectorSheet } from './OrgSelectorSheet' |
| 25 | import { OrgCommandItem } from '@/components/layouts/AppLayout/OrgCommandItem' |
| 26 | import { useOrganizationsQuery } from '@/data/organizations/organizations-query' |
| 27 | import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query' |
| 28 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 29 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 30 | |
| 31 | export function OrgSelector() { |
| 32 | const router = useRouter() |
| 33 | const { slug: routeSlug } = useParams() |
| 34 | const { data: selectedOrganization } = useSelectedOrganizationQuery() |
| 35 | const { data: organizations, isPending: isLoadingOrganizations } = useOrganizationsQuery() |
| 36 | const organizationCreationEnabled = useIsFeatureEnabled('organizations:create') |
| 37 | |
| 38 | const [open, setOpen] = useState(false) |
| 39 | |
| 40 | const slug = selectedOrganization?.slug |
| 41 | const isPlatformOrg = selectedOrganization?.plan?.id === 'platform' |
| 42 | const selectedOrgInitial = selectedOrganization?.name?.trim().charAt(0).toUpperCase() || 'O' |
| 43 | const { data: projects } = useOrgProjectsInfiniteQuery( |
| 44 | { slug, limit: 1 }, |
| 45 | { enabled: Boolean(slug) && !isPlatformOrg } |
| 46 | ) |
| 47 | |
| 48 | const numProjects = projects?.pages[0]?.pagination.count |
| 49 | const projectsLabel = isPlatformOrg |
| 50 | ? 'Platform' |
| 51 | : typeof numProjects === 'number' |
| 52 | ? `${numProjects} project${numProjects === 1 ? '' : 's'}` |
| 53 | : 'No projects' |
| 54 | |
| 55 | const isMobile = useBreakpoint('md') |
| 56 | |
| 57 | if (isLoadingOrganizations) return <ShimmeringLoader className="ml-1 w-[120px]" /> |
| 58 | |
| 59 | const triggerButton = ( |
| 60 | <SidebarMenuButton |
| 61 | size="lg" |
| 62 | className="data-open:bg-sidebar-accent data-open:text-sidebar-accent-foreground gap-2 h-auto text-left group px-1.5 py-1 touch-manipulation" |
| 63 | onClick={isMobile ? () => setOpen(true) : undefined} |
| 64 | > |
| 65 | <span className="flex w-8 aspect-square shrink-0 items-center justify-center rounded-sm border bg-surface-100 text-xs font-medium text-foreground-lighter"> |
| 66 | {selectedOrgInitial} |
| 67 | </span> |
| 68 | <div className="flex min-w-0 flex-1 flex-col text-left -mb-0.5"> |
| 69 | <div className="truncate text-foreground font-medium leading-tight min-w-[100px] max-w-[250px]"> |
| 70 | {selectedOrganization?.name ?? 'Select organization'} |
| 71 | </div> |
| 72 | {selectedOrganization && ( |
| 73 | <div className="flex items-center gap-1 truncate text-foreground-light leading-tight text-xs"> |
| 74 | <Boxes className="shrink-0 size-3" strokeWidth={1.5} /> |
| 75 | <span>{projectsLabel}</span> |
| 76 | </div> |
| 77 | )} |
| 78 | </div> |
| 79 | <ChevronsUpDown |
| 80 | strokeWidth={1} |
| 81 | className="ml-auto text-foreground-light md:hidden md:group-hover:block w-4! h-4!" |
| 82 | /> |
| 83 | </SidebarMenuButton> |
| 84 | ) |
| 85 | |
| 86 | if (isMobile) { |
| 87 | return ( |
| 88 | <> |
| 89 | <SidebarMenu className="shrink"> |
| 90 | <SidebarMenuItem> |
| 91 | {isLoadingOrganizations ? <ShimmeringLoader className="p-2 w-[90px]" /> : triggerButton} |
| 92 | </SidebarMenuItem> |
| 93 | </SidebarMenu> |
| 94 | <OrgSelectorSheet |
| 95 | open={open} |
| 96 | onOpenChange={setOpen} |
| 97 | onClose={() => setOpen(false)} |
| 98 | selectedOrganization={selectedOrganization ?? null} |
| 99 | /> |
| 100 | </> |
| 101 | ) |
| 102 | } |
| 103 | |
| 104 | return ( |
| 105 | <SidebarMenu> |
| 106 | <SidebarMenuItem> |
| 107 | <Popover open={open} onOpenChange={setOpen} modal={false}> |
| 108 | <PopoverTrigger asChild>{triggerButton}</PopoverTrigger> |
| 109 | <PopoverContent className="p-0" side="bottom" align="start"> |
| 110 | <Command> |
| 111 | <CommandInput placeholder="Find organization..." /> |
| 112 | <CommandList> |
| 113 | <CommandEmpty>No organizations found</CommandEmpty> |
| 114 | <CommandGroup> |
| 115 | <ScrollArea |
| 116 | className={(organizations || []).length > 7 ? 'h-full md:h-[210px]' : ''} |
| 117 | > |
| 118 | {organizations?.map((org) => ( |
| 119 | <OrgCommandItem |
| 120 | key={org.slug} |
| 121 | org={org} |
| 122 | selectedSlug={slug} |
| 123 | routePathname={router.pathname} |
| 124 | hasRouteSlug={!!routeSlug} |
| 125 | onClose={() => setOpen(false)} |
| 126 | /> |
| 127 | ))} |
| 128 | </ScrollArea> |
| 129 | </CommandGroup> |
| 130 | <CommandSeparator /> |
| 131 | <CommandGroup> |
| 132 | <CommandItem |
| 133 | className="cursor-pointer w-full" |
| 134 | onSelect={() => { |
| 135 | setOpen(false) |
| 136 | router.push('/organizations') |
| 137 | }} |
| 138 | onClick={() => setOpen(false)} |
| 139 | > |
| 140 | <Link href="/organizations" className="flex items-center gap-2 w-full"> |
| 141 | <p>All Organizations</p> |
| 142 | </Link> |
| 143 | </CommandItem> |
| 144 | </CommandGroup> |
| 145 | {organizationCreationEnabled && ( |
| 146 | <> |
| 147 | <CommandSeparator /> |
| 148 | <CommandGroup> |
| 149 | <CommandItem |
| 150 | className="cursor-pointer w-full" |
| 151 | onSelect={() => { |
| 152 | setOpen(false) |
| 153 | router.push('/new') |
| 154 | }} |
| 155 | onClick={() => setOpen(false)} |
| 156 | > |
| 157 | <Link href="/new" className="flex items-center gap-2 w-full"> |
| 158 | <Plus size={14} strokeWidth={1.5} /> |
| 159 | <p>New organization</p> |
| 160 | </Link> |
| 161 | </CommandItem> |
| 162 | </CommandGroup> |
| 163 | </> |
| 164 | )} |
| 165 | </CommandList> |
| 166 | </Command> |
| 167 | </PopoverContent> |
| 168 | </Popover> |
| 169 | </SidebarMenuItem> |
| 170 | </SidebarMenu> |
| 171 | ) |
| 172 | } |