OrgSelector.tsx 6.3 KB

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