// Adapted from: web/src/components/ui/multi-select.tsx import * as React from "react"; import { CheckIcon, ChevronDown, FolderIcon, LayersIcon, LibraryBigIcon, } from "lucide-react"; import Image from "next/image"; import { cn, getCodeHostIcon } 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"; export type RepoContextItem = { type: 'repo'; value: string; name: string; codeHostType: string; } export type SearchContextItem = { type: 'context'; value: string; name: string; repoCount: number; } export type ContextItem = RepoContextItem | SearchContextItem; interface ContextSelectorProps extends React.ButtonHTMLAttributes { repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; selectedItems: ContextItem[]; onSelectedItemsChange: (items: ContextItem[]) => void; className?: string; isOpen: boolean; onOpenChanged: (isOpen: boolean) => void; } export const ContextSelector = React.forwardRef< HTMLButtonElement, ContextSelectorProps >( ( { repos, searchContexts, onSelectedItemsChange, className, selectedItems, isOpen, onOpenChanged, ...props }, ref ) => { const scrollContainerRef = React.useRef(null); const scrollPosition = React.useRef(0); const handleInputKeyDown = ( event: React.KeyboardEvent ) => { if (event.key === "Enter") { onOpenChanged(true); } else if (event.key === "Backspace" && !event.currentTarget.value) { const newSelectedItems = [...selectedItems]; newSelectedItems.pop(); onSelectedItemsChange(newSelectedItems); } }; const toggleItem = (item: ContextItem) => { // Store current scroll position before state update if (scrollContainerRef.current) { scrollPosition.current = scrollContainerRef.current.scrollTop; } const isSelected = selectedItems.some( (selected) => selected.type === item.type && selected.value === item.value ); const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo"; let newSelectedItems: ContextItem[]; if (isSelected) { newSelectedItems = selectedItems.filter( (selected) => !(selected.type === item.type && selected.value === item.value) ); } else { // Limit selected context to 1 in demo mode if (isDemoMode) { newSelectedItems = [item]; } else { newSelectedItems = [...selectedItems, item]; } } onSelectedItemsChange(newSelectedItems); }; const handleClear = () => { onSelectedItemsChange([]); }; const handleTogglePopover = () => { onOpenChanged(!isOpen); }; const allItems = React.useMemo(() => { const contextItems: ContextItem[] = searchContexts.map(context => ({ type: 'context' as const, value: context.name, name: context.name, repoCount: context.repoNames.length })); const repoItems: ContextItem[] = repos.map(repo => ({ type: 'repo' as const, value: repo.repoName, name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName, codeHostType: repo.codeHostType, })); return [...contextItems, ...repoItems]; }, [repos, searchContexts]); const sortedItems = React.useMemo(() => { return allItems .map((item) => ({ item, isSelected: selectedItems.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 contexts before repos if (a.item.type === 'context' && b.item.type === 'repo') return -1; if (a.item.type === 'repo' && b.item.type === 'context') return 1; return 0; }) }, [allItems, selectedItems]); // Restore scroll position after re-render React.useEffect(() => { if (scrollContainerRef.current && scrollPosition.current > 0) { scrollContainerRef.current.scrollTop = scrollPosition.current; } }, [sortedItems]); return ( onOpenChanged(false)} > No results found. {sortedItems.map(({ item, isSelected }) => { return ( toggleItem(item)} className="cursor-pointer" >
{item.type === 'context' ? ( ) : ( // Render code host icon for repos (() => { const codeHostIcon = item.codeHostType ? getCodeHostIcon(item.codeHostType) : null; return codeHostIcon ? ( {`${item.codeHostType} ) : ( ); })() )}
{item.name} {item.type === 'context' && ( {item.repoCount} repo{item.repoCount === 1 ? '' : 's'} )}
); })}
{selectedItems.length > 0 && ( <> Clear )}
); } ); ContextSelector.displayName = "ContextSelector";