import { AbsoluteCenter, Avatar, Box, Combobox, HStack, Icon, Menu, Portal, Span, Spinner, StackSeparator, Text, InputGroup, type ListCollection, Flex } from "@chakra-ui/react" import type { MetadataResultBase, MetadataResultImage, MetadataResultServiceScore } from "../../../core/Api" import { getMusicServiceIconElement } from "../icons/ChakraIcons" import React, { useRef } from "react" import { MSErrorBoundary } from "../ErrorBoundary" import { EllipsisButtonMenu } from "../buttonMenus/ButtonMenu" export const LeftSideMetadataResultContent = (props: MetadataResultServiceScore & MetadataResultImage) => { if (props.image === undefined) { return } return ( } {...props} /> ) } export const MetadataServiceScore = (props: MetadataResultServiceScore) => ( }> {getMusicServiceIconElement(props.service)} {props.score !== undefined ? {props.score} : undefined} ) export const ArtworkAvatar = (props: MetadataResultImage & { overlay?: React.JSX.Element }) => { let overlayContent: React.JSX.Element | undefined = undefined; if (props.overlay !== undefined) { overlayContent = {props.overlay} } return ( {overlayContent} ) } export type MetadataPartials = Record T }>; export interface MetadataPickMenuProps { data: T partials: MetadataPartials onPick?: (val: T) => void } export const MetadataPickMenu = (props: MetadataPickMenuProps) => { const { data, partials, onPick } = props; if (onPick === undefined) { return undefined; } // menu content is portaled but still bubbles through the React tree // stop it here so Combobox does not select the whole item or treat Enter as freetext return ( e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}> ( {label} ))} menuCallback={(select) => onPick(partials[select.value].pick(data))} /> ); } export type MetadataSearchResult = MetadataResultBase & MetadataResultServiceScore; export interface MetadataSearchComboboxProps { placeholder: string collection: ListCollection isLoading: boolean isError: boolean inputGroupContent?: React.JSX.Element initialInput?: string onChange: (val: T) => void /** Called with the typed text when the user commits it without selecting a result */ onFreetext: (name: string) => void onQueryChange: (query: string) => void renderItem: (item: T, onPick: (val: T) => void) => React.JSX.Element } export const MetadataSearchCombobox = (props: MetadataSearchComboboxProps) => { const { placeholder, collection, isLoading, isError, onChange, onFreetext, onQueryChange, renderItem, initialInput = '' } = props; // a ref, not state: combobox captures onInteractOutside when the popup opens so state read there is stale const rawInput = useRef(initialInput === '' ? undefined : initialInput); const commitFreetext = () => { if (rawInput.current !== undefined && rawInput.current !== initialInput) { onFreetext(rawInput.current); } }; const inputElm = ; let input: React.JSX.Element; if(props.inputGroupContent !== undefined) { input = ( {inputElm} ) } else { input = inputElm; } return ( { // combobox prevents default when Enter selects a highlighted item if (e.key === 'Enter' && !e.defaultPrevented) { commitFreetext(); } }} collection={collection} onInteractOutside={commitFreetext} onValueChange={(val) => { console.log(val, 'value change'); onChange(val.items[0]); rawInput.current = undefined; }} onSelect={(val) => { console.log(val, 'select') }} onInputValueChange={(e) => { onQueryChange(e.inputValue); // selecting an item rewrites the input, this is not freetext if (e.reason !== 'item-select') { rawInput.current = e.inputValue; } }} > {input} {isLoading ? ( Loading... ) : isError ? ( Error fetching ) : ( {(combobox) => collection.items?.map((item) => ( {renderItem(item, (val) => { onChange(val); rawInput.current = undefined; combobox.setInputValue(collection.stringifyItem(val) ?? '', 'item-select'); combobox.setOpen(false); })} ))} )} No items found ); }