import type { IChartEvent } from '@openpanel/validation'; import { useQuery } from '@tanstack/react-query'; import { AnimatePresence, motion } from 'framer-motion'; import { ArrowLeftIcon, Building2Icon, DatabaseIcon, UserIcon, } from 'lucide-react'; import VirtualList from 'rc-virtual-list'; import { type Dispatch, type SetStateAction, useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { useAppParams } from '@/hooks/use-app-params'; import { useEventProperties } from '@/hooks/use-event-properties'; import { useTRPC } from '@/integrations/trpc/react'; interface PropertiesComboboxProps { event?: IChartEvent; children: (setOpen: Dispatch>) => React.ReactNode; onSelect: (action: { value: string; label: string; description: string; }) => void; exclude?: string[]; mode?: 'events' | 'profile'; } function SearchHeader({ onBack, onSearch, value, }: { onBack?: () => void; onSearch: (value: string) => void; value: string; }) { return (
{!!onBack && ( )} onSearch(e.target.value)} placeholder="Search" value={value} />
); } export function PropertiesCombobox({ event, children, onSelect, mode, exclude = [], }: PropertiesComboboxProps) { const { projectId } = useAppParams(); const trpc = useTRPC(); const [open, setOpen] = useState(false); const properties = useEventProperties({ event: event?.name, projectId, }); const groupPropertiesQuery = useQuery( trpc.group.properties.queryOptions({ projectId }) ); const [state, setState] = useState<'index' | 'event' | 'profile' | 'group'>( 'index' ); const [search, setSearch] = useState(''); const [direction, setDirection] = useState<'forward' | 'backward'>('forward'); useEffect(() => { if (!open) { setState(mode ? (mode === 'events' ? 'event' : 'profile') : 'index'); } }, [open, mode]); const shouldShowProperty = (property: string) => { return !exclude.find((ex) => { if (ex.endsWith('*')) { return property.startsWith(ex.slice(0, -1)); } return property === ex; }); }; // Fixed group properties: name, type, plus dynamic property keys const groupActions = [ { value: 'group.name', label: 'name', description: 'group' }, { value: 'group.type', label: 'type', description: 'group' }, ...(groupPropertiesQuery.data ?? []).map((key) => ({ value: `group.properties.${key}`, label: key, description: 'group.properties', })), ].filter((a) => shouldShowProperty(a.value)); const profileActions = properties .filter( (property) => property.startsWith('profile') && shouldShowProperty(property) ) .map((property) => ({ value: property, label: property.split('.').pop() ?? property, description: property.split('.').slice(0, -1).join('.'), })); const eventActions = properties .filter( (property) => !property.startsWith('profile') && shouldShowProperty(property) ) .map((property) => ({ value: property, label: property.split('.').pop() ?? property, description: property.split('.').slice(0, -1).join('.'), })); const handleStateChange = ( newState: 'index' | 'event' | 'profile' | 'group' ) => { setDirection(newState === 'index' ? 'backward' : 'forward'); setState(newState); }; const handleSelect = (action: { value: string; label: string; description: string; }) => { setOpen(false); onSelect(action); }; const renderIndex = () => { return ( {/* {}} value={search} /> */} {/* */} { e.preventDefault(); handleStateChange('event'); }} > Event properties { e.preventDefault(); handleStateChange('profile'); }} > Profile properties { e.preventDefault(); handleStateChange('group'); }} > Group properties ); }; const renderEvent = () => { const filteredActions = eventActions.filter( (action) => action.label.toLowerCase().includes(search.toLowerCase()) || action.description.toLowerCase().includes(search.toLowerCase()) ); return (
handleStateChange('index') : undefined } onSearch={setSearch} value={search} /> {(action) => ( handleSelect(action)} >
{action.label}
{action.description}
)}
); }; const renderProfile = () => { const filteredActions = profileActions.filter( (action) => action.label.toLowerCase().includes(search.toLowerCase()) || action.description.toLowerCase().includes(search.toLowerCase()) ); return (
handleStateChange('index')} onSearch={setSearch} value={search} /> {(action) => ( handleSelect(action)} >
{action.label}
{action.description}
)}
); }; const renderGroup = () => { const filteredActions = groupActions.filter( (action) => action.label.toLowerCase().includes(search.toLowerCase()) || action.description.toLowerCase().includes(search.toLowerCase()) ); return (
handleStateChange('index')} onSearch={setSearch} value={search} /> {(action) => ( handleSelect(action)} >
{action.label}
{action.description}
)}
); }; return ( { setOpen(open); }} open={open} > {children(setOpen)} {state === 'index' && ( {renderIndex()} )} {state === 'event' && ( {renderEvent()} )} {state === 'profile' && ( {renderProfile()} )} {state === 'group' && ( {renderGroup()} )} ); }