import { keepPreviousData } from '@tanstack/react-query' import { useDebounce, useIntersectionObserver } from '@uidotdev/usehooks' import { ChevronsUpDown, HelpCircle } from 'lucide-react' import { ReactNode, useEffect, useId, useMemo, useRef, useState } from 'react' import { Button, cn, Command, CommandGroup, CommandInput, CommandList, Popover, PopoverContent, PopoverTrigger, ScrollArea, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { EmbeddedProjectList } from './OrganizationProjectSelector/EmbeddedProjectList' import { ProjectCommandItem } from './OrganizationProjectSelector/ProjectCommandItem' import { OrgProject, useOrgProjectsInfiniteQuery, } from '@/data/projects/org-projects-infinite-query' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' 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, listboxId, open, }: { isLoading: boolean project?: OrgProject listboxId: string open: boolean }) => ReactNode renderActions?: (setOpen: (value: boolean) => void, options?: { embedded?: boolean }) => ReactNode onSelect?: (project: OrgProject) => void onInitialLoad?: (projects: OrgProject[]) => void isOptionDisabled?: (project: OrgProject) => boolean fetchOnMount?: boolean modal?: boolean /** When true, render only the command list (no popover/trigger). For use inside sheet or popover. */ embedded?: boolean className?: string } 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, embedded = false, className, }: OrganizationProjectSelectorSelectorProps) => { const { data: organization } = useSelectedOrganizationQuery() const slug = _slug ?? organization?.slug const [openInternal, setOpenInternal] = useState(false) const open = _open ?? openInternal const setOpen = _setOpen ?? setOpenInternal const listboxId = useId() 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]) function renderListContent() { if (isLoadingProjects) { return ( <>
) } if (isErrorProjects) { return (

Failed to retrieve projects

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

No projects found based on your search

) } if (projects.length === 0) { return

No projects found

} if (embedded) { return ( setOpen(false)} renderRow={renderRow} checkPosition={checkPosition} isOptionDisabled={isOptionDisabled} sentinelRef={sentinelRef} hasNextPage={!!hasNextPage} /> ) } return ( 7 ? 'h-full md:h-[210px]' : ''}> {projects?.map((project) => ( setOpen(false)} renderRow={renderRow} checkPosition={checkPosition} isOptionDisabled={isOptionDisabled} /> ))}
{hasNextPage && (
)} ) } const commandContent = ( {embedded && !!renderActions && (
{renderActions(setOpen, { embedded: true })}
)} setSearch('')} wrapperClassName={embedded ? 'shrink-0 border-b' : undefined} className="text-base sm:text-sm" /> {renderListContent()} {!!renderActions && !embedded && ( <>
{renderActions(setOpen)} )} ) if (embedded) { return commandContent } return ( {renderTrigger ? ( renderTrigger({ isLoading: isLoadingProjects || isFetching, project: selectedProject, listboxId, open, }) ) : ( )} {commandContent} ) }