Skip to main content
Glama
SearchableMultiSelect.tsx3.72 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { PillsInputFieldProps, PillsInputProps } from '@mantine/core'; import { CheckIcon, Combobox, Group, Pill, PillsInput, useCombobox } from '@mantine/core'; import type { JSX } from 'react'; import { useMemo, useState } from 'react'; const MAX_DISPLAYED_OPTIONS = 8; interface SearchableMultiSelectProps { readonly pillInputProps?: PillsInputProps; readonly inputProps?: PillsInputFieldProps; readonly data: string[]; readonly onChange?: (value: string[]) => void; } export function SearchableMultiSelect({ pillInputProps, inputProps, data, onChange, }: SearchableMultiSelectProps): JSX.Element { const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption(), onDropdownOpen: () => combobox.updateSelectedOptionIndex('active'), }); const [search, setSearch] = useState(''); const [value, setValue] = useState<string[]>([]); const handleValueSelect = (val: string): void => { const newValue = value.includes(val) ? value.filter((v) => v !== val) : [...value, val]; setValue(newValue); if (onChange) { onChange(newValue); } }; const handleValueRemove = (val: string): void => setValue((current) => current.filter((v) => v !== val)); const valueDisplay = useMemo( () => value.map((item) => ( <Pill key={item} withRemoveButton onRemove={() => handleValueRemove(item)}> {item} </Pill> )), [value] ); const filteredOptions = useMemo(() => { const needle = search.trim().toLowerCase(); return data.filter((item) => item.toLowerCase().includes(needle)); }, [data, search]); const displayedOptions = useMemo(() => { const result: JSX.Element[] = new Array(Math.min(filteredOptions.length, MAX_DISPLAYED_OPTIONS)); for (let i = 0; i < result.length; i++) { const item = filteredOptions[i]; result[i] = ( <Combobox.Option value={item} key={item} active={value.includes(item)}> <Group gap="sm"> {value.includes(item) ? <CheckIcon size={12} /> : null} <span>{item}</span> </Group> </Combobox.Option> ); } return result; }, [filteredOptions, value]); return ( <Combobox store={combobox} onOptionSubmit={handleValueSelect} withinPortal={false}> <Combobox.DropdownTarget> <PillsInput onClick={() => combobox.openDropdown()} {...pillInputProps}> <Pill.Group> {valueDisplay} <Combobox.EventsTarget> <PillsInput.Field onFocus={() => combobox.openDropdown()} onBlur={() => { combobox.closeDropdown(); setSearch(''); }} value={search} onChange={(event) => { combobox.updateSelectedOptionIndex(); setSearch(event.currentTarget.value); }} onKeyDown={(event) => { if (event.key === 'Backspace' && search.length === 0 && value.length > 0) { event.preventDefault(); handleValueRemove(value[value.length - 1]); } }} {...inputProps} /> </Combobox.EventsTarget> </Pill.Group> </PillsInput> </Combobox.DropdownTarget> <Combobox.Dropdown> <Combobox.Options> {displayedOptions.length > 0 ? displayedOptions : <Combobox.Empty>Nothing found...</Combobox.Empty>} </Combobox.Options> </Combobox.Dropdown> </Combobox> ); }

Latest Blog Posts

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/medplum/medplum'

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