import { keepPreviousData } from '@tanstack/react-query' import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks' import { OrgProject, useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { Check, ChevronsUpDown, HelpCircle } from 'lucide-react' import { ReactNode, useEffect, useMemo, useRef, useState } from 'react' import { Button, cn, Command_Shadcn_, CommandGroup_Shadcn_, CommandInput_Shadcn_, CommandItem_Shadcn_, CommandList_Shadcn_, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, ScrollArea, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' interface OrganizationProjectSelectorSelectorProps { slug?: string open?: boolean selectedRef?: string | null searchPlaceholder?: string sameWidthAsTrigger?: boolean checkPosition?: 'right' | 'left' setOpen?: (value: boolean) => void renderRow?: (project: OrgProject) => ReactNode renderTrigger?: ({ isLoading, project, }: { isLoading: boolean project?: OrgProject }) => ReactNode renderActions?: (setOpen: (value: boolean) => void) => ReactNode onSelect?: (project: OrgProject) => void onInitialLoad?: (projects: OrgProject[]) => void isOptionDisabled?: (project: OrgProject) => boolean fetchOnMount?: boolean modal?: boolean } export const OrganizationProjectSelector = ({ slug: _slug, open: _open, setOpen: _setOpen, selectedRef, searchPlaceholder = 'Find project...', sameWidthAsTrigger = false, checkPosition = 'right', renderRow, renderTrigger, renderActions, onSelect, onInitialLoad, isOptionDisabled, fetchOnMount = false, modal = false, }: OrganizationProjectSelectorSelectorProps) => { const { data: organization } = useSelectedOrganizationQuery() const slug = _slug ?? organization?.slug const [openInternal, setOpenInternal] = useState(false) const open = _open ?? openInternal const setOpen = _setOpen ?? setOpenInternal const [search, setSearch] = useState('') const debouncedSearch = useDebounce(search, 500) const scrollRootRef = useRef(null) const [sentinelRef, entry] = useIntersectionObserver({ root: scrollRootRef.current, threshold: 0, rootMargin: '0px', }) const { data, error: projectsError, isLoading: isLoadingProjects, isError: isErrorProjects, isSuccess: isSuccessProjects, isFetching, isFetchingNextPage, hasNextPage, fetchNextPage, } = useOrgProjectsInfiniteQuery( { slug, search: search.length === 0 ? search : debouncedSearch }, { enabled: fetchOnMount || open, placeholderData: keepPreviousData } ) const projects = useMemo(() => data?.pages.flatMap((page) => page.projects), [data?.pages]) || [] const selectedProject = projects.find((p) => p.ref === selectedRef) useEffect(() => { if ( !isLoadingProjects && !isFetching && entry?.isIntersecting && hasNextPage && !isFetchingNextPage ) { fetchNextPage() } }, [ entry?.isIntersecting, hasNextPage, isFetching, isFetchingNextPage, isLoadingProjects, fetchNextPage, ]) useEffect(() => { // isLoadingProjects is true only during initial load. If the variables for the query change (slug), isLoadingProjects // will be true again. if (!isLoadingProjects && isSuccessProjects) { onInitialLoad?.(projects) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingProjects, isSuccessProjects]) return ( {renderTrigger ? ( renderTrigger({ isLoading: isLoadingProjects || isFetching, project: selectedProject }) ) : ( )} setSearch('')} /> {isLoadingProjects ? ( <>
) : isErrorProjects ? (

Failed to retrieve projects

Error: {projectsError?.message}
) : ( <> {search.length > 0 && projects.length === 0 && (

No projects found based on your search

)} 7 ? 'h-[210px]' : ''}> {projects?.map((project) => ( { onSelect?.(project) setOpen(false) }} onClick={() => setOpen(false)} disabled={!!isOptionDisabled ? isOptionDisabled(project) : false} > {!!renderRow ? ( renderRow(project) ) : (
{checkPosition === 'left' && project.ref === selectedRef && ( )} {project.name} {checkPosition === 'right' && project.ref === selectedRef && ( )}
)}
))}
{hasNextPage && (
)} )} {!!renderActions && ( <> {/* [Joshen] Not using CommandSeparator to persist this while searching */}
{renderActions(setOpen)} )} ) }