Skip to main content
Glama

Convex MCP server

Official
by get-convex
ContextMenu.tsx13.5 kB
import React, { ReactNode, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { useFloating, autoUpdate, flip, offset, shift, useRole, useDismiss, useInteractions, useListNavigation, useTypeahead, FloatingPortal, FloatingFocusManager, FloatingOverlay, FloatingList, useListItem, useFloatingTree, FloatingTree, FloatingNode, useFloatingNodeId, useFloatingParentNodeId, useHover, safePolygon, useClick, useMergeRefs, } from "@floating-ui/react"; import classNames from "classnames"; import { ChevronRightIcon } from "@radix-ui/react-icons"; import { UrlObject } from "url"; import { Button } from "@ui/Button"; import { TooltipSide } from "@ui/Tooltip"; import { Key, KeyboardShortcut } from "@ui/KeyboardShortcut"; const ContextMenuContext = React.createContext<{ getItemProps: ( userProps?: React.HTMLProps<HTMLElement>, ) => Record<string, unknown>; activeIndex: number | null; setActiveIndex: React.Dispatch<React.SetStateAction<number | null>>; isOpen: boolean; }>({ getItemProps: () => ({}), activeIndex: null, setActiveIndex: () => {}, isOpen: false, }); export type Target = { x: number; y: number }; type ContextMenuProps = React.PropsWithChildren<{ target: Target | null; onClose: () => void; }>; // Based on https://codesandbox.io/s/trusting-rui-2duieo // and https://codesandbox.io/s/admiring-lamport-5wt3yg export function ContextMenu(props: ContextMenuProps) { return ( <FloatingTree> <ContextMenuInner {...props} /> </FloatingTree> ); } function ContextMenuInner({ target, onClose, children }: ContextMenuProps) { const isOpen = target !== null; const onOpenChange = useCallback( (newIsOpen: boolean) => { if (!newIsOpen) { onClose(); } }, [onClose], ); const { refs, floatingStyles, context } = useFloating({ open: isOpen, onOpenChange, middleware: [ offset({ mainAxis: 5, alignmentAxis: 4 }), flip({ fallbackPlacements: ["left-start"], }), shift({ padding: 10 }), ], placement: "right-start", strategy: "fixed", whileElementsMounted: autoUpdate, }); // Interactions const role = useRole(context, { role: "menu" }); const dismiss = useDismiss(context); const [activeIndex, setActiveIndex] = useState<number | null>(null); const listItemsRef = useRef<Array<HTMLButtonElement | null>>([]); const listNavigation = useListNavigation(context, { listRef: listItemsRef, onNavigate: setActiveIndex, activeIndex, }); const listContentRef = useRef<Array<string | null>>([]); const typeahead = useTypeahead(context, { enabled: isOpen, listRef: listContentRef, onMatch: setActiveIndex, activeIndex, }); const { getFloatingProps, getItemProps } = useInteractions([ role, dismiss, listNavigation, typeahead, ]); // Position relative to the target useLayoutEffect(() => { if (!target) { refs.setPositionReference(null); return; } const { x, y } = target; refs.setPositionReference({ getBoundingClientRect() { return { width: 0, height: 0, x, y, top: y, right: x, bottom: y, left: x, }; }, }); }, [refs, target]); // Tree events const tree = useFloatingTree(); const nodeId = useFloatingNodeId(); useEffect(() => { if (!tree) return; function handleTreeClick() { onClose(); } tree.events.on("click", handleTreeClick); return () => tree.events.off("click", handleTreeClick); }, [tree, onClose]); const contextValue = useMemo( () => ({ activeIndex, setActiveIndex, getItemProps, isOpen, }), [activeIndex, setActiveIndex, getItemProps, isOpen], ); return ( <FloatingNode id={nodeId}> <ContextMenuContext.Provider value={contextValue}> <FloatingList elementsRef={listItemsRef} labelsRef={listContentRef}> <FloatingPortal> {isOpen && ( <FloatingOverlay className="z-50"> <FloatingFocusManager context={context} initialFocus={refs.floating} > {/* 20px = twice the padding in the `shift` middleware (https://floating-ui.com/docs/misc#handling-large-content) */} <div className="flex max-h-[calc(100vh-20px)] animate-fadeInFromLoading flex-col overflow-x-hidden overflow-y-auto rounded-lg border bg-background-secondary py-2 text-xs whitespace-nowrap shadow-xs outline-hidden dark:border" ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} > {children} </div> </FloatingFocusManager> </FloatingOverlay> )} </FloatingPortal> </FloatingList> </ContextMenuContext.Provider> </FloatingNode> ); } function ContextMenuItem({ icon, label, action, disabled, variant, shortcut, tip, tipSide, blankTarget = true, proBadge, }: { icon?: ReactNode; label: ReactNode; action: (() => void) | UrlObject | string; disabled?: boolean; variant?: "neutral" | "danger"; shortcut?: Key[]; tip?: ReactNode; tipSide?: TooltipSide; blankTarget?: boolean; proBadge?: boolean; }) { const menu = useContext(ContextMenuContext); const { itemRef: labelRef, itemText: labelText } = useTextContent(); const item = useListItem({ label: disabled ? null : labelText }); const tree = useFloatingTree(); const isActive = item.index === menu.activeIndex; return ( <Button variant="unstyled" className={classNames( "w-full flex max-w-xs gap-2 items-center px-3 py-1.5 text-left", "active:bg-background-tertiary disabled:active:bg-background-secondary focus:bg-background-tertiary disabled:focus:bg-background-secondary outline-hidden", disabled ? "cursor-not-allowed fill-content-tertiary text-content-tertiary" : variant === "danger" ? "text-content-errorSecondary" : ' "text-content-primary"', )} disabled={disabled} ref={item.ref} tabIndex={isActive ? 0 : -1} href={typeof action !== "function" ? action : undefined} target={ typeof action !== "function" ? blankTarget ? "_blank" : undefined : undefined } {...menu.getItemProps({ onClick: () => { typeof action === "function" && action(); setTimeout(() => { tree?.events.emit("click"); }, 0); }, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { typeof action === "function" && action(); typeof action !== "function" && e.currentTarget.click(); setTimeout(() => { tree?.events.emit("click"); }, 0); } }, })} onClickOfAnchorLink={(e) => { e.stopPropagation(); setTimeout(() => { tree?.events.emit("click"); }, 0); }} tip={tip} tipSide={tipSide} > {icon ?? null} <span className="flex-1 truncate overflow-hidden" ref={labelRef}> {label} </span> {shortcut && ( <KeyboardShortcut value={shortcut} className="ml-auto pl-6 text-content-tertiary" /> )} {proBadge && ( <span className="rounded-sm bg-util-accent px-1.5 py-0.5 text-xs font-semibold tracking-wider text-white uppercase" title="Only available on the Pro plan" > Pro </span> )} </Button> ); } ContextMenu.Item = ContextMenuItem; type ContextMenuSubmenuProps = React.PropsWithChildren<{ icon?: ReactNode; label: ReactNode; action?: () => void; disabled?: boolean; }>; function ContextMenuSubmenu({ icon, label, children, action, disabled = false, }: ContextMenuSubmenuProps) { // Item in the parent menu const parent = useContext(ContextMenuContext); const { itemRef: labelRef, itemText: labelText } = useTextContent(); const item = useListItem({ label: labelText }); const tree = useFloatingTree(); const nodeId = useFloatingNodeId(); const parentId = useFloatingParentNodeId(); // Submenu const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState<number | null>(null); const elementsRef = useRef<Array<HTMLButtonElement | null>>([]); const labelsRef = useRef<Array<string | null>>([]); const { floatingStyles, refs, context } = useFloating<HTMLButtonElement>({ nodeId, open: isOpen, onOpenChange: setIsOpen, placement: "right-start", middleware: [ offset({ mainAxis: 0, alignmentAxis: 0 }), flip(), shift({ padding: 10 }), ], whileElementsMounted: autoUpdate, }); // Interactions const hover = useHover(context, { enabled: true, delay: { open: 75 }, handleClose: safePolygon({ blockPointerEvents: true }), }); const click = useClick(context, { event: "mousedown", toggle: false, ignoreMouse: true, }); const role = useRole(context, { role: "menu" }); const dismiss = useDismiss(context, { bubbles: true }); const listNavigation = useListNavigation(context, { listRef: elementsRef, activeIndex, nested: true, onNavigate: setActiveIndex, }); const typeahead = useTypeahead(context, { listRef: labelsRef, onMatch: isOpen ? setActiveIndex : undefined, activeIndex, }); const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions( [hover, click, role, dismiss, listNavigation, typeahead], ); // Tree events useEffect(() => { if (!tree) return; function handleTreeClick() { setIsOpen(false); } function onSubMenuOpen(event: { nodeId: string; parentId: string }) { if (event.nodeId !== nodeId && event.parentId === parentId) { setIsOpen(false); } } tree.events.on("click", handleTreeClick); tree.events.on("menuopen", onSubMenuOpen); return () => { tree.events.off("click", handleTreeClick); tree.events.off("menuopen", onSubMenuOpen); }; }, [tree, nodeId, parentId]); useEffect(() => { if (isOpen && tree) { tree.events.emit("menuopen", { parentId, nodeId }); } }, [tree, isOpen, nodeId, parentId]); const contextValue = useMemo( () => ({ activeIndex, setActiveIndex, getItemProps, isOpen }), [activeIndex, setActiveIndex, getItemProps, isOpen], ); const isClickable = action !== undefined; return ( <FloatingNode id={nodeId}> <Button role="menuitem" ref={useMergeRefs([refs.setReference, item.ref])} variant="unstyled" className={classNames( "w-full flex max-w-xs gap-2 items-center px-3 py-1.5 text-left", "outline-hidden text-content-primary", "active:bg-background-tertiary focus:bg-background-tertiary", !isClickable && "cursor-default hover:bg-background-tertiary", "disabled:text-content-tertiary disabled:hover:bg-background-secondary disabled:cursor-not-allowed", )} disabled={disabled} tabIndex={item.index === parent.activeIndex ? 0 : -1} {...getReferenceProps(parent.getItemProps())} onClick={ isClickable ? () => { action?.(); tree?.events.emit("click"); } : undefined } > {icon ?? null} <span className="flex-1 truncate overflow-hidden" ref={labelRef}> {label} </span> {!disabled && ( <span className="ml-auto shrink-0 text-content-primary"> <ChevronRightIcon className="ml-2" /> </span> )} </Button> <ContextMenuContext.Provider value={contextValue}> <FloatingList elementsRef={elementsRef} labelsRef={labelsRef}> {!disabled && isOpen && ( <FloatingPortal> {/* 20px = twice the padding in the `shift` middleware (https://floating-ui.com/docs/misc#handling-large-content) */} <div className="z-50 overflow-hidden rounded-lg border bg-background-secondary shadow-md outline-hidden" ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()} > <div className="flex max-h-[calc(100vh-20px)] flex-col overflow-x-hidden overflow-y-auto py-2 text-xs whitespace-nowrap"> {children} </div> </div> </FloatingPortal> )} </FloatingList> </ContextMenuContext.Provider> </FloatingNode> ); } ContextMenuSubmenu.displayName = "ContextMenuSubmenu"; ContextMenu.Submenu = ContextMenuSubmenu; function useTextContent(): { itemRef: (element: HTMLElement) => void; itemText: string | undefined; } { const [itemText, setItemText] = useState<string | undefined>(undefined); const itemRef = useCallback( (element: HTMLElement) => setItemText(element ? element.innerText : undefined), [], ); return { itemRef, itemText, }; }

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