// Adapted from: web/src/components/ui/multi-select.tsx import * as React from "react"; import { CheckIcon, ChevronDown, ScanSearchIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command"; import { RepoSetSearchScope, RepoSearchScope, SearchScope } from "../../types"; import { SearchScopeIcon } from "../searchScopeIcon"; interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes { repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; selectedSearchScopes: SearchScope[]; onSelectedSearchScopesChange: (items: SearchScope[]) => void; className?: string; isOpen: boolean; onOpenChanged: (isOpen: boolean) => void; } export const SearchScopeSelector = React.forwardRef< HTMLButtonElement, SearchScopeSelectorProps >( ( { repos, searchContexts, className, selectedSearchScopes, onSelectedSearchScopesChange, isOpen, onOpenChanged, ...props }, ref ) => { const scrollContainerRef = React.useRef(null); const scrollPosition = React.useRef(0); const [hasSearchInput, setHasSearchInput] = React.useState(false); const handleInputKeyDown = ( event: React.KeyboardEvent ) => { if (event.key === "Enter") { onOpenChanged(true); } else if (event.key === "Backspace" && !event.currentTarget.value) { const newSelectedItems = [...selectedSearchScopes]; newSelectedItems.pop(); onSelectedSearchScopesChange(newSelectedItems); } }; const toggleItem = (item: SearchScope) => { // Store current scroll position before state update if (scrollContainerRef.current) { scrollPosition.current = scrollContainerRef.current.scrollTop; } const isSelected = selectedSearchScopes.some( (selected) => selected.type === item.type && selected.value === item.value ); const newSelectedItems = isSelected ? selectedSearchScopes.filter( (selected) => !(selected.type === item.type && selected.value === item.value) ) : [...selectedSearchScopes, item]; onSelectedSearchScopesChange(newSelectedItems); }; const handleClear = () => { onSelectedSearchScopesChange([]); }; const handleSelectAll = () => { onSelectedSearchScopesChange(allSearchScopeItems); }; const handleTogglePopover = () => { onOpenChanged(!isOpen); }; const allSearchScopeItems = React.useMemo(() => { const repoSetSearchScopeItems: RepoSetSearchScope[] = searchContexts.map(context => ({ type: 'reposet' as const, value: context.name, name: context.name, repoCount: context.repoNames.length })); const repoSearchScopeItems: RepoSearchScope[] = repos.map(repo => ({ type: 'repo' as const, value: repo.repoName, name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName, codeHostType: repo.codeHostType, })); return [...repoSetSearchScopeItems, ...repoSearchScopeItems]; }, [repos, searchContexts]); const sortedSearchScopeItems = React.useMemo(() => { return allSearchScopeItems .map((item) => ({ item, isSelected: selectedSearchScopes.some( (selected) => selected.type === item.type && selected.value === item.value ) })) .sort((a, b) => { // Selected items first if (a.isSelected && !b.isSelected) return -1; if (!a.isSelected && b.isSelected) return 1; // Then reposets before repos if (a.item.type === 'reposet' && b.item.type === 'repo') return -1; if (a.item.type === 'repo' && b.item.type === 'reposet') return 1; return 0; }) }, [allSearchScopeItems, selectedSearchScopes]); // Restore scroll position after re-render React.useEffect(() => { if (scrollContainerRef.current && scrollPosition.current > 0) { scrollContainerRef.current.scrollTop = scrollPosition.current; } }, [sortedSearchScopeItems]); return ( onOpenChanged(false)} > setHasSearchInput(!!value)} /> No results found. {!hasSearchInput && (
Select all
)} {sortedSearchScopeItems.map(({ item, isSelected }) => { return ( toggleItem(item)} className="cursor-pointer" >
{item.name} {item.type === 'reposet' && ( {item.repoCount} repo{item.repoCount === 1 ? '' : 's'} )}
); })}
{selectedSearchScopes.length > 0 && ( <> Clear )}
); } ); SearchScopeSelector.displayName = "SearchScopeSelector";