ProjectDropdown.tsx205 lines · main
1import { useParams } from 'common'
2import { Box, Plus } from 'lucide-react'
3import Link from 'next/link'
4import { useRouter } from 'next/router'
5import type { ComponentProps } from 'react'
6import { useState } from 'react'
7import { Button, CommandGroup, CommandItem } from 'ui'
8import { ShimmeringLoader } from 'ui-patterns'
9
10import { AppLayoutDropdownTriggerButton } from './AppLayoutDropdown'
11import { sanitizeRoute } from './ProjectDropdown.utils'
12import { ProjectRowLink } from './ProjectRowLink'
13import { useEmbeddedCloseHandler } from './useEmbeddedCloseHandler'
14import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
15import PartnerIcon from '@/components/ui/PartnerIcon'
16import { getManagedByFromOrganizationPartner } from '@/data/organizations/managed-by-utils'
17import type { OrgProject } from '@/data/projects/org-projects-infinite-query'
18import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
19import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
20import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
21import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
22import { IS_PLATFORM } from '@/lib/constants'
23import type { ManagedBy } from '@/lib/constants/infrastructure'
24import { useTrack } from '@/lib/telemetry/track'
25
26// --- Sub-components ---
27
28interface ProjectDropdownNewProjectActionsProps {
29 organizationSlug: string | undefined
30 embedded: boolean
31 onClose: () => void
32 onNavigate: (href: string) => void
33}
34
35function ProjectDropdownNewProjectActions({
36 organizationSlug,
37 embedded,
38 onClose,
39 onNavigate,
40}: ProjectDropdownNewProjectActionsProps) {
41 const href = `/new/${organizationSlug}`
42
43 if (embedded) {
44 return (
45 <Button type="default" block size="small" asChild icon={<Plus size={14} strokeWidth={1.5} />}>
46 <Link
47 href={href}
48 onClick={onClose}
49 className="text-xs text-foreground-light hover:text-foreground"
50 >
51 New project
52 </Link>
53 </Button>
54 )
55 }
56
57 return (
58 <CommandGroup>
59 <CommandItem
60 className="cursor-pointer w-full"
61 onSelect={() => {
62 onClose()
63 onNavigate(href)
64 }}
65 onClick={onClose}
66 >
67 <Link href={href} onClick={onClose} className="w-full flex items-center gap-2">
68 <Plus size={14} strokeWidth={1.5} />
69 <p>New project</p>
70 </Link>
71 </CommandItem>
72 </CommandGroup>
73 )
74}
75
76const ProjectDropdownNonPlatformView = ({ projectName }: { projectName: string }) => {
77 return <div className="text-sm px-3 py-1">{projectName}</div>
78}
79
80interface ProjectDropdownPlatformViewProps {
81 projectRef: string | undefined
82 projectName: string
83 projectManagedBy?: ManagedBy
84 selectorProps: Omit<
85 ComponentProps<typeof OrganizationProjectSelector>,
86 'renderTrigger' | 'embedded'
87 >
88}
89
90function ProjectDropdownPlatformView({
91 projectRef,
92 projectName,
93 projectManagedBy,
94 selectorProps,
95}: ProjectDropdownPlatformViewProps) {
96 return (
97 <div className="flex items-center shrink-0">
98 <Link href={`/project/${projectRef}`} className="flex items-center gap-2 shrink-0 text-sm">
99 <Box size={14} strokeWidth={1.5} className="text-foreground-lighter" />
100 <span title={projectName} className="text-foreground max-w-32 lg:max-w-64 truncate">
101 {projectName}
102 </span>
103 {projectManagedBy && <PartnerIcon organization={{ managed_by: projectManagedBy }} />}
104 </Link>
105
106 <OrganizationProjectSelector
107 {...selectorProps}
108 renderTrigger={() => <AppLayoutDropdownTriggerButton className="shrink-0" />}
109 />
110 </div>
111 )
112}
113
114// --- Main component ---
115
116interface ProjectDropdownProps {
117 embedded?: boolean
118 className?: string
119 onClose?: () => void
120}
121
122export const ProjectDropdown = ({
123 embedded = false,
124 className,
125 onClose,
126}: ProjectDropdownProps = {}) => {
127 const router = useRouter()
128 const { ref } = useParams()
129 const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
130 const { data: selectedOrganization } = useSelectedOrganizationQuery()
131
132 const isBranch = project?.parentRef !== project?.ref
133 const { data: parentProject, isPending: isLoadingParentProject } = useProjectDetailQuery(
134 { ref: project?.parent_project_ref },
135 { enabled: isBranch }
136 )
137 const selectedProject = parentProject ?? project
138
139 const projectCreationEnabled = useIsFeatureEnabled('projects:create')
140 const track = useTrack()
141
142 const [open, setOpen] = useState(false)
143 const close = useEmbeddedCloseHandler(embedded, onClose, setOpen)
144 const selectedProjectManagedBy = selectedProject?.integration_source
145 ? getManagedByFromOrganizationPartner(undefined, selectedProject.integration_source)
146 : selectedOrganization?.billing_partner
147 ? selectedOrganization.managed_by
148 : undefined
149
150 if (isLoadingProject || (isBranch && isLoadingParentProject) || !selectedProject) {
151 if (!embedded) return <ShimmeringLoader className="p-2 md:mr-2 md:w-[90px]" />
152 }
153
154 const handleSetOpen = embedded
155 ? (_value: boolean) => onClose?.()
156 : (next: boolean) => {
157 if (next) track('header_project_dropdown_opened')
158 setOpen(next)
159 }
160
161 const selectorProps = {
162 open,
163 setOpen: handleSetOpen,
164 selectedRef: ref,
165 onSelect: (project: { ref: string }) => {
166 const sanitizedRoute = sanitizeRoute(router.route, router.query)
167 const href = sanitizedRoute?.replace('[ref]', project.ref) ?? `/project/${project.ref}`
168 close()
169 router.push(href)
170 },
171 renderRow: (project: Pick<OrgProject, 'ref' | 'name' | 'status' | 'integration_source'>) => (
172 <ProjectRowLink
173 project={project}
174 selectedRef={ref}
175 route={router.route}
176 routerQueries={router.query}
177 />
178 ),
179 renderActions: (_setOpen: (value: boolean) => void, options?: { embedded?: boolean }) =>
180 projectCreationEnabled ? (
181 <ProjectDropdownNewProjectActions
182 organizationSlug={selectedOrganization?.slug}
183 embedded={options?.embedded ?? false}
184 onClose={close}
185 onNavigate={(href) => router.push(href)}
186 />
187 ) : null,
188 }
189
190 if (embedded)
191 return (
192 <OrganizationProjectSelector {...selectorProps} embedded className={className} fetchOnMount />
193 )
194
195 return IS_PLATFORM ? (
196 <ProjectDropdownPlatformView
197 projectRef={project?.ref}
198 projectName={selectedProject?.name ?? ''}
199 projectManagedBy={selectedProjectManagedBy}
200 selectorProps={selectorProps}
201 />
202 ) : (
203 <ProjectDropdownNonPlatformView projectName={selectedProject?.name ?? ''} />
204 )
205}