Skip to main content
Glama

mcp-google-sheets

multi-select.tsx16.9 kB
'use client'; // Used form here https://github.com/shadcn-ui/ui/pull/2773/files import * as PopoverPrimitive from '@radix-ui/react-popover'; import { Primitive } from '@radix-ui/react-primitive'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import { t } from 'i18next'; // Use t function from react-i18next import { Check, ChevronsUpDown, RefreshCcw, X } from 'lucide-react'; import React, { ComponentPropsWithoutRef } from 'react'; import { createPortal } from 'react-dom'; import { SelectUtilButton } from '@/components/custom/select-util-button'; import { cn } from '@/lib/utils'; import { Badge } from '../ui/badge'; import { Button } from '../ui/button'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '../ui/command'; import { ScrollArea } from '../ui/scroll-area'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../ui/tooltip'; export interface MultiSelectOptionItem { value: unknown; label?: React.ReactNode; } interface MultiSelectContextValue { value: string[]; open: boolean; onSelect(value: string, item: MultiSelectOptionItem): void; onDeselect(value: string, item: MultiSelectOptionItem): void; onSearch?(keyword: string | undefined): void; filter?: boolean | ((keyword: string, current: string) => boolean); disabled?: boolean; maxCount?: number; itemCache: Map<string, MultiSelectOptionItem>; } const MultiSelectContext = React.createContext< MultiSelectContextValue | undefined >(undefined); const useMultiSelect = () => { const context = React.useContext(MultiSelectContext); if (!context) { throw new Error( t('useMultiSelect must be used within MultiSelectProvider'), ); } return context; }; type MultiSelectProps = React.ComponentPropsWithoutRef< typeof PopoverPrimitive.Root > & { value?: string[]; onValueChange?(value: string[], items: MultiSelectOptionItem[]): void; onSelect?(value: string, item: MultiSelectOptionItem): void; onDeselect?(value: string, item: MultiSelectOptionItem): void; defaultValue?: string[]; onSearch?(keyword: string | undefined): void; filter?: boolean | ((keyword: string, current: string) => boolean); disabled?: boolean; maxCount?: number; }; const MultiSelect: React.FC<MultiSelectProps> = ({ value: valueProp, onValueChange: onValueChangeProp, onDeselect: onDeselectProp, onSelect: onSelectProp, defaultValue, open: openProp, onOpenChange, defaultOpen, onSearch, filter, disabled, maxCount, ...popoverProps }) => { const itemCache = React.useRef( new Map<string, MultiSelectOptionItem>(), ).current; const handleValueChange = React.useCallback( (state: string[]) => { if (onValueChangeProp) { const items = state.map((value) => itemCache.get(value)!); onValueChangeProp(state, items); } }, [onValueChangeProp], ); const [value, setValue] = useControllableState({ prop: valueProp, defaultProp: defaultValue, onChange: handleValueChange, }); const [open, setOpen] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChange, }); const handleSelect = React.useCallback( (value: string, item: MultiSelectOptionItem) => { setValue((prev) => { if (prev?.includes(value)) { return prev; } onSelectProp?.(value, item); return prev ? [...prev, value] : [value]; }); }, [onSelectProp, setValue], ); const handleDeselect = React.useCallback( (value: string, item: MultiSelectOptionItem) => { setValue((prev) => { if (!prev || !prev.includes(value)) { return prev; } onDeselectProp?.(value, item); return prev.filter((v) => v !== value); }); }, [onDeselectProp, setValue], ); const contextValue = React.useMemo(() => { return { value: value || [], open: open || false, onSearch, filter, disabled, maxCount, onSelect: handleSelect, onDeselect: handleDeselect, itemCache, }; }, [ value, open, onSearch, filter, disabled, maxCount, handleSelect, handleDeselect, itemCache, ]); return ( <MultiSelectContext.Provider value={contextValue}> <PopoverPrimitive.Root {...popoverProps} open={open} onOpenChange={setOpen} /> </MultiSelectContext.Provider> ); }; MultiSelect.displayName = 'MultiSelect'; type MultiSelectTriggerElement = React.ElementRef<typeof Primitive.button>; type MultiSelectTriggerProps = ComponentPropsWithoutRef< typeof Primitive.button > & { showDeselect?: boolean; onDeselect?: () => void; showRefresh?: boolean; onRefresh?: () => void; loading?: boolean; }; const PreventClick = (e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); e.stopPropagation(); }; const MultiSelectTrigger = React.forwardRef< MultiSelectTriggerElement, MultiSelectTriggerProps >( ( { className, children, showDeselect, onDeselect, loading, ...props }, forwardedRef, ) => { const { disabled } = useMultiSelect(); return ( <PopoverPrimitive.Trigger ref={forwardedRef as any} asChild> <Button variant="outline" aria-disabled={disabled} disabled={disabled} role="combobox" loading={loading} className={cn( 'flex min-h-10 w-full items-center justify-between cursor-pointer gap-2 whitespace-nowrap rounded-sm border border-input bg-transparent px-4 py-1 text-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring [&>span]:line-clamp-1', { 'cursor-not-allowed opacity-80': disabled, 'cursor-pointer': !disabled, }, className, )} onClick={disabled ? PreventClick : props.onClick} onTouchStart={disabled ? PreventClick : props.onTouchStart} > {children} <div className="flex gap-2 items-center"> {showDeselect && ( <SelectUtilButton tooltipText={t('Unset')} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onDeselect?.(); }} Icon={X} ></SelectUtilButton> )} {props.showRefresh && ( <SelectUtilButton tooltipText={t('Refresh')} onClick={props.onRefresh} Icon={RefreshCcw} ></SelectUtilButton> )} <ChevronsUpDown aria-hidden className="h-4 w-4 opacity-50 shrink-0" /> </div> </Button> </PopoverPrimitive.Trigger> ); }, ); MultiSelectTrigger.displayName = 'MultiSelectTrigger'; interface MultiSelectValueProps extends ComponentPropsWithoutRef<typeof Primitive.div> { placeholder?: string; maxDisplay?: number; maxItemLength?: number; } const MultiSelectValue = React.forwardRef< React.ElementRef<typeof Primitive.div>, MultiSelectValueProps >( ( { className, placeholder, maxDisplay, maxItemLength, ...props }, forwardRef, ) => { const { value, itemCache, onDeselect, disabled } = useMultiSelect(); const [firstRendered, setFirstRendered] = React.useState(false); const remainingPiecesCount = maxDisplay && value.length > maxDisplay ? value.length - maxDisplay : 0; const renderItems = remainingPiecesCount ? value.slice(0, maxDisplay) : value; React.useLayoutEffect(() => { setFirstRendered(true); }, []); if (!value.length || !firstRendered) { return ( <span className="pointer-events-none text-muted-foreground opacity-80"> {placeholder} </span> ); } return ( <TooltipProvider delayDuration={300}> <div className={cn( 'flex flex-1 overflow-x-hidden flex-wrap items-center gap-1.5', className, )} {...props} ref={forwardRef} > {renderItems.map((value) => { const item = itemCache.get(value); const content = item?.label || value; const child = maxItemLength && typeof content === 'string' && content.length > maxItemLength ? `${content.slice(0, maxItemLength)}...` : content; const el = ( <Badge variant="outline" key={value} className={cn('pr-1.5 group/multi-select-badge rounded-full', { 'cursor-pointer': !disabled, 'cursor-not-allowed opacity-80': disabled, })} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onDeselect(value, item!); }} > <span>{child}</span> {!disabled && ( <X className="h-3 w-3 ml-1 text-muted-foreground group-hover/multi-select-badge:text-foreground" /> )} </Badge> ); if (child !== content) { return ( <Tooltip key={value}> <TooltipTrigger className="inline-flex">{el}</TooltipTrigger> <TooltipContent side="bottom" align="start" className="z-[51]" > {content} </TooltipContent> </Tooltip> ); } return el; })} {remainingPiecesCount ? ( <span className="text-muted-foreground text-xs leading-4 py-.5"> {t('+{remainingPiecesCount} more', { remainingPiecesCount: remainingPiecesCount, })} </span> ) : null} </div> </TooltipProvider> ); }, ); MultiSelectValue.displayName = 'MultiSelectValue'; const MultiSelectSearch = React.forwardRef< React.ElementRef<typeof CommandInput>, ComponentPropsWithoutRef<typeof CommandInput> >((props, ref) => { const { onSearch } = useMultiSelect(); return <CommandInput ref={ref} {...props} onValueChange={onSearch} />; }); MultiSelectSearch.displayName = 'MultiSelectSearch'; const MultiSelectList = React.forwardRef< React.ElementRef<typeof CommandList>, ComponentPropsWithoutRef<typeof CommandList> >(({ className, ...props }, ref) => { return ( <CommandList ref={ref} className={cn('py-1 px-0 ', className)} {...props}> <ScrollArea viewPortClassName="max-h-[200px]"> {props.children} </ScrollArea> </CommandList> ); }); MultiSelectList.displayName = 'MultiSelectList'; type MultiSelectContentProps = ComponentPropsWithoutRef< typeof PopoverPrimitive.Content >; const MultiSelectContent = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Content>, MultiSelectContentProps >(({ className, children, ...props }, ref) => { const context = useMultiSelect(); const fragmentRef = React.useRef<DocumentFragment>(); if (!fragmentRef.current && typeof window !== 'undefined') { fragmentRef.current = document.createDocumentFragment(); } if (!context.open) { return fragmentRef.current ? createPortal(<Command>{children}</Command>, fragmentRef.current) : null; } return ( <PopoverPrimitive.Portal forceMount> <PopoverPrimitive.Content ref={ref} align="start" sideOffset={4} collisionPadding={10} className={cn( 'z-50 w-full rounded-md border bg-background p-0 text-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', )} style={ { '--radix-select-content-transform-origin': 'var(--radix-popper-transform-origin)', '--radix-select-content-available-width': 'var(--radix-popper-available-width)', '--radix-select-content-available-height': 'var(--radix-popper-available-height)', '--radix-select-trigger-width': 'var(--radix-popper-anchor-width)', '--radix-select-trigger-height': 'var(--radix-popper-anchor-height)', } as any } {...props} > <Command className={cn( 'px-1 max-h-96 w-full min-w-[var(--radix-select-trigger-width)]', className, )} shouldFilter={!context.onSearch} > {children} </Command> </PopoverPrimitive.Content> </PopoverPrimitive.Portal> ); }); MultiSelectContent.displayName = 'MultiSelectContent'; type MultiSelectItemProps = ComponentPropsWithoutRef<typeof CommandItem> & Partial<MultiSelectOptionItem> & { onSelect?: (value: string, item: MultiSelectOptionItem) => void; onDeselect?: (value: string, item: MultiSelectOptionItem) => void; }; const MultiSelectItem = React.forwardRef< React.ElementRef<typeof CommandItem>, MultiSelectItemProps >( ( { value, onSelect: onSelectProp, onDeselect: onDeselectProp, children, label, disabled: disabledProp, className, ...props }, forwardedRef, ) => { const { value: contextValue, maxCount, onSelect, onDeselect, itemCache, } = useMultiSelect(); const item = React.useMemo(() => { return value ? { value, label: label || (typeof children === 'string' ? children : undefined), } : undefined; }, [value, label, children]); const selected = Boolean(value && contextValue.includes(value)); React.useEffect(() => { if (value) { itemCache.set(value, item!); } }, [selected, value, item]); const disabled = Boolean( disabledProp || (!selected && maxCount && contextValue.length >= maxCount), ); const handleClick = () => { if (selected) { onDeselectProp?.(value!, item!); onDeselect(value!, item!); } else { itemCache.set(value!, item!); onSelectProp?.(value!, item!); onSelect(value!, item!); } }; return ( <CommandItem {...props} value={value} className={cn( 'cursor-pointer', disabled && 'text-muted-foreground cursor-not-allowed', className, )} disabled={disabled} onSelect={!disabled && value ? handleClick : undefined} ref={forwardedRef} > <span className="mr-2 whitespace-nowrap overflow-hidden text-ellipsis"> {children || label || value} </span> {selected ? <Check className="h-4 w-4 ml-auto shrink-0" /> : null} </CommandItem> ); }, ); MultiSelectItem.displayName = 'MultiSelectItem'; const MultiSelectGroup = React.forwardRef< React.ElementRef<typeof CommandGroup>, ComponentPropsWithoutRef<typeof CommandGroup> >((props, forwardRef) => { return <CommandGroup {...props} ref={forwardRef} />; }); MultiSelectGroup.displayName = 'MultiSelectGroup'; const MultiSelectSeparator = React.forwardRef< React.ElementRef<typeof CommandSeparator>, ComponentPropsWithoutRef<typeof CommandSeparator> >((props, forwardRef) => { return <CommandSeparator {...props} ref={forwardRef} />; }); MultiSelectSeparator.displayName = 'MultiSelectSeparator'; const MultiSelectEmpty = React.forwardRef< React.ElementRef<typeof CommandEmpty>, ComponentPropsWithoutRef<typeof CommandEmpty> >(({ children = 'No Content', ...props }, forwardRef) => { return ( <CommandEmpty {...props} ref={forwardRef}> {children} </CommandEmpty> ); }); MultiSelectEmpty.displayName = 'MultiSelectEmpty'; export interface MultiSelectOptionSeparator { type: 'separator'; } export interface MultiSelectOptionGroup { heading?: React.ReactNode; value?: string; children: MultiSelectOption[]; } export type MultiSelectOption = { value: unknown; label: string; }; export { MultiSelect, MultiSelectTrigger, MultiSelectValue, MultiSelectSearch, MultiSelectContent, MultiSelectList, MultiSelectItem, MultiSelectGroup, MultiSelectSeparator, MultiSelectEmpty, };

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

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