Skip to main content
Glama

Convex MCP server

Official
by get-convex
MultiSelectCombobox.tsx9.23 kB
import { CheckIcon, ChevronDownIcon, MagnifyingGlassIcon, } from "@radix-ui/react-icons"; import { Combobox } from "@headlessui/react"; import React, { useRef, useState, useEffect } from "react"; import classNames from "classnames"; import { cn } from "@ui/cn"; import { useHoverDirty } from "react-use"; import { test } from "fuzzy"; import { Button } from "@ui/Button"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; export type MultiSelectValue = string[] | "all"; export function MultiSelectCombobox({ options, selectedOptions, setSelectedOptions, unit, unitPlural, label, labelHidden = false, Option, disableSearch = false, processFilterOption = (option) => option, }: { options: string[]; selectedOptions: MultiSelectValue; setSelectedOptions(newValue: MultiSelectValue): void; unit: string; unitPlural: string; label: string; labelHidden?: boolean; Option?: React.ComponentType<{ label: string; inButton: boolean }>; disableSearch?: boolean; processFilterOption?: (option: string) => string; }) { const [query, setQuery] = useState(""); const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>( null, ); // Force tabindex to 0 useEffect(() => { if (referenceElement?.children[0]) { (referenceElement.children[0] as HTMLElement).tabIndex = 0; } }, [referenceElement]); const [isOpen, setIsOpen] = useState(false); const { styles, attributes, update } = usePopper( referenceElement, popperElement, { placement: "bottom-start", modifiers: [ { name: "offset", options: { offset: [0, 4], }, }, ], }, ); // Get the width for the dropdown const getOptionsWidth = () => { if (!referenceElement) return undefined; return `${referenceElement.offsetWidth}px`; }; const filteredOptions = query === "" ? options : options.filter((option) => test(query, processFilterOption(option))); // Convert to internal array representation for Combobox const selectedArray = selectedOptions === "all" ? options : selectedOptions; const count = selectedOptions === "all" ? options.length : selectedOptions.filter((name) => name !== "_other").length; const displayValue = selectedOptions === "all" ? `All ${unitPlural}` : `${count} ${count !== 1 ? unitPlural : unit}`; // Update popper position when dropdown opens useEffect(() => { if (isOpen && update) { void update(); } }, [isOpen, update]); const handleSelectAll = () => { if (selectedOptions === "all") { setSelectedOptions([]); } else { setSelectedOptions("all"); } }; return ( <Combobox as="div" value={selectedArray} onChange={(newSelection) => { // Check if all options are selected and convert to "all" state if (newSelection.length === options.length) { setSelectedOptions("all"); } else { setSelectedOptions(newSelection); } }} multiple > {({ open }) => { // Update isOpen state when open changes if (open !== isOpen) { setIsOpen(open); } return ( <> <Combobox.Label className={classNames( "flex gap-1 text-sm font-semibold", labelHidden ? "hidden" : "mb-2", )} hidden={labelHidden} > {label} </Combobox.Label> <div className="relative"> <div ref={setReferenceElement} className={cn("relative flex items-center")} > <Combobox.Button className={classNames( "flex gap-2 w-full justify-between", "truncate relative rounded-md py-1.5 px-1.5 text-left text-sm text-content-primary disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-background-secondary", "border", "focus:border-border-selected focus:outline-hidden bg-background-secondary hover:bg-background-tertiary", open && "border-border-selected", )} > {displayValue} <ChevronDownIcon className={cn( "relative z-30 -ml-6 h-5 w-5 text-content-primary transition-all", open && "rotate-180", )} /> </Combobox.Button> </div> {open && createPortal( <div ref={setPopperElement} style={{ ...styles.popper, width: getOptionsWidth(), }} {...attributes.popper} className="z-50" > <Combobox.Options static className="scrollbar max-h-60 w-fit max-w-80 min-w-full overflow-auto rounded-md border bg-background-secondary pb-1 text-xs shadow-sm focus:outline-hidden" > <div className="min-w-fit"> {!disableSearch && ( <div className="sticky top-0 left-0 z-20 flex w-full items-center gap-1 border-b bg-background-secondary px-2 pt-1"> <MagnifyingGlassIcon className="h-4 w-4 text-content-secondary" /> <Combobox.Input onChange={(event) => setQuery(event.target.value)} value={query} placeholder={`Search ${unitPlural}...`} className={classNames( "placeholder:text-content-tertiary relative w-full py-1.5 text-left text-xs text-content-primary disabled:bg-background-tertiary disabled:text-content-secondary disabled:cursor-not-allowed", "focus:outline-hidden bg-background-secondary", )} /> </div> )} {/* eslint-disable-next-line react/forbid-elements */} <button type="button" className="w-full cursor-pointer p-2 pl-7 text-left text-content-primary hover:bg-background-tertiary" onClick={handleSelectAll} > {selectedOptions === "all" ? "Deselect all" : "Select all"} </button> {filteredOptions.map((option) => ( <ComboboxOption key={option} value={option} label={ Option ? ( <Option label={option} inButton={false} /> ) : ( option ) } onOnly={() => { setSelectedOptions([option]); }} /> ))} </div> </Combobox.Options> </div>, document.body, )} </div> </> ); }} </Combobox> ); } function ComboboxOption({ value, label, onOnly, }: { value: string; label: React.ReactNode | string; onOnly: () => void; }) { const onlyRefs = useRef(null); const isHoveringOnly = useHoverDirty(onlyRefs); return ( <Combobox.Option value={value} className={({ active }) => classNames( "w-fit min-w-full flex gap-1 cursor-pointer select-none p-2 text-content-primary group", active && "bg-background-tertiary", ) } disabled={isHoveringOnly} > {({ selected }) => ( <> {selected ? ( <CheckIcon className="h-4 min-w-[1rem] text-neutral-7 dark:text-neutral-4" aria-hidden="true" /> ) : ( <span className="min-w-[1rem]" /> )} <span className={classNames( "flex gap-2 w-full whitespace-nowrap", selected && "font-semibold", )} > {label} <Button ref={onlyRefs} className="invisible text-xs font-normal text-content-secondary group-hover:visible hover:underline" variant="unstyled" onClick={onOnly} > only </Button> </span> </> )} </Combobox.Option> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server