import { Badge, Button, clx, DropdownMenu, IconButton, Kbd, Text, } from "@medusajs/ui" import { Command } from "cmdk" import { Dialog as RadixDialog } from "radix-ui" import { Children, ComponentPropsWithoutRef, ElementRef, forwardRef, Fragment, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react" import { useTranslation } from "react-i18next" import { useLocation, useNavigate } from "react-router-dom" import { ArrowUturnLeft, MagnifyingGlass, Plus, Spinner, TriangleDownMini, } from "@medusajs/icons" import { matchSorter } from "match-sorter" import { useSearch } from "../../providers/search-provider" import { Skeleton } from "../common/skeleton" import { Thumbnail } from "../common/thumbnail" import { DEFAULT_SEARCH_LIMIT, SEARCH_AREAS, SEARCH_LIMIT_INCREMENT, } from "./constants" import { SearchArea } from "./types" import { useSearchResults } from "./use-search-results" import { useDocumentDirection } from "../../hooks/use-document-direction" export const Search = () => { const [area, setArea] = useState("all") const [search, setSearch] = useState("") const [limit, setLimit] = useState(DEFAULT_SEARCH_LIMIT) const { open, onOpenChange } = useSearch() const location = useLocation() const { t } = useTranslation() const navigate = useNavigate() const inputRef = useRef(null) const listRef = useRef(null) const { staticResults, dynamicResults, isFetching } = useSearchResults({ area, limit, q: search, }) const handleReset = useCallback(() => { setArea("all") setSearch("") setLimit(DEFAULT_SEARCH_LIMIT) }, [setLimit]) const handleBack = () => { handleReset() inputRef.current?.focus() } const handleOpenChange = useCallback( (open: boolean) => { if (!open) { handleReset() } onOpenChange(open) }, [onOpenChange, handleReset] ) useEffect(() => { handleOpenChange(false) }, [location.pathname, handleOpenChange]) const handleSelect = (item: { to?: string; callback?: () => void }) => { handleOpenChange(false) if (item.to) { navigate(item.to) return } if (item.callback) { item.callback() return } } const handleShowMore = (area: SearchArea) => { if (area === "all") { setLimit(DEFAULT_SEARCH_LIMIT) } else { setLimit(SEARCH_LIMIT_INCREMENT) } setArea(area) inputRef.current?.focus() } const handleLoadMore = () => { setLimit((l) => l + SEARCH_LIMIT_INCREMENT) } const filteredStaticResults = useMemo(() => { const filteredResults: typeof staticResults = [] staticResults.forEach((group) => { const filteredItems = matchSorter(group.items, search, { keys: ["label"], }) if (filteredItems.length === 0) { return } filteredResults.push({ ...group, items: filteredItems, }) }) return filteredResults }, [staticResults, search]) const handleSearch = (q: string) => { setSearch(q) listRef.current?.scrollTo({ top: 0 }) } const showLoading = useMemo(() => { return isFetching && !dynamicResults.length && !filteredStaticResults.length }, [isFetching, dynamicResults, filteredStaticResults]) return ( {showLoading && } {dynamicResults.map((group) => { return ( {group.items.map((item) => { return ( handleSelect(item)} value={item.value} className="flex items-center justify-between" >
{item.thumbnail && ( )} {item.title} {item.subtitle && ( {item.subtitle} )}
) })} {group.hasMore && area === "all" && ( handleShowMore(group.area)} hidden={true} value={`${group.title}:show:more`} // Prevent the "Show more" buttons across groups from sharing the same value/state >
{t("app.search.showMore")}
)} {group.hasMore && area === group.area && ( )}
) })} {filteredStaticResults.map((group) => { return ( {group.items.map((item) => { return ( handleSelect(item)} className="flex items-center justify-between" > {item.label}
{item.keys.Mac?.map((key, index) => { return (
{key} {index < (item.keys.Mac?.length || 0) - 1 && ( {t("app.keyboardShortcuts.then")} )}
) })}
) })}
) })} {!showLoading && }
) } const CommandPalette = forwardRef< ElementRef, ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandPalette.displayName = Command.displayName interface CommandDialogProps extends RadixDialog.DialogProps { isLoading?: boolean } const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const { t } = useTranslation() const preserveHeight = useMemo(() => { return props.isLoading && Children.count(children) === 0 }, [props.isLoading, children]) return ( {t("app.search.title")} {t("app.search.description")} {children}
{t("app.search.navigation")}
{t("app.search.openResult")}
) } const CommandInput = forwardRef< ElementRef, ComponentPropsWithoutRef & { area: SearchArea setArea: (area: SearchArea) => void isFetching: boolean onBack?: () => void } >( ( { className, value, onValueChange, area, setArea, isFetching, onBack, ...props }, ref ) => { const { t } = useTranslation() const innerRef = useRef(null) const direction = useDocumentDirection() useImperativeHandle( ref, () => innerRef.current ) return (
{t(`app.search.groups.${area}`)} { e.preventDefault() innerRef.current?.focus() }} > setArea(v as SearchArea)} > {SEARCH_AREAS.map((area) => ( {area === "command" && } {t(`app.search.groups.${area}`)} {area === "all" && } ))}
{onBack && ( )}
{isFetching && ( )} {value && ( )}
) } ) CommandInput.displayName = Command.Input.displayName const CommandList = forwardRef< ElementRef, ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandList.displayName = Command.List.displayName const CommandEmpty = forwardRef< ElementRef, Omit, "children"> & { q?: string } >((props, ref) => { const { t } = useTranslation() return (
{props.q ? t("app.search.noResultsTitle") : t("app.search.emptySearchTitle")} {props.q ? t("app.search.noResultsMessage") : t("app.search.emptySearchMessage")}
) }) CommandEmpty.displayName = Command.Empty.displayName const CommandLoading = forwardRef< ElementRef, ComponentPropsWithoutRef >((props, ref) => { return (
{Array.from({ length: 7 }).map((_, index) => (
))}
) }) CommandLoading.displayName = Command.Loading.displayName const CommandGroup = forwardRef< ElementRef, ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandGroup.displayName = Command.Group.displayName const CommandSeparator = forwardRef< ElementRef, ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) CommandSeparator.displayName = Command.Separator.displayName const CommandItem = forwardRef< ElementRef, ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( svg]:text-ui-fg-subtle relative flex cursor-pointer select-none items-center gap-x-3 rounded-md p-2 outline-none data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50", className )} {...props} /> )) CommandItem.displayName = Command.Item.displayName