import { ColorSquare } from '@/components/color-square'; import { Button } from '@/components/ui/button'; import { ComboboxEvents } from '@/components/ui/combobox-events'; import { Input } from '@/components/ui/input'; import { InputEnter } from '@/components/ui/input-enter'; import { useAppParams } from '@/hooks/use-app-params'; import { useDebounceFn } from '@/hooks/use-debounce-fn'; import { useEventNames } from '@/hooks/use-event-names'; import { useDispatch, useSelector } from '@/redux'; import { DndContext, type DragEndEvent, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core'; import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { shortId } from '@openpanel/common'; import { alphabetIds } from '@openpanel/constants'; import type { IChartEvent, IChartEventItem, IChartFormula, } from '@openpanel/validation'; import { FilterIcon, HandIcon, PiIcon, PlusIcon } from 'lucide-react'; import { ReportSegment } from '../ReportSegment'; import { addEvent, addFormula, changeEvent, duplicateEvent, removeEvent, reorderEvents, } from '../reportSlice'; import { EventPropertiesCombobox } from './EventPropertiesCombobox'; import { PropertiesCombobox } from './PropertiesCombobox'; import type { ReportEventMoreProps } from './ReportEventMore'; import { ReportEventMore } from './ReportEventMore'; import { FiltersList } from './filters/FiltersList'; function SortableSeries({ event, index, showSegment, showAddFilter, isSelectManyEvents, ...props }: { event: IChartEventItem | IChartEvent; index: number; showSegment: boolean; showAddFilter: boolean; isSelectManyEvents: boolean; } & React.HTMLAttributes) { const dispatch = useDispatch(); const eventId = 'type' in event ? event.id : (event as IChartEvent).id; const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: eventId ?? '' }); const style = { transform: CSS.Transform.toString(transform), transition, }; // Normalize event to have type field const normalizedEvent: IChartEventItem = 'type' in event ? event : { ...event, type: 'event' as const }; const isFormula = normalizedEvent.type === 'formula'; const chartEvent = isFormula ? null : (normalizedEvent as IChartEventItem & { type: 'event' }); return (
{props.children}
{/* Segment and Filter buttons - only for events */} {chartEvent && (showSegment || showAddFilter) && (
{showSegment && ( { dispatch( changeEvent({ ...chartEvent, segment, }), ); }} /> )} {showAddFilter && ( { dispatch( changeEvent({ ...chartEvent, filters: [ ...chartEvent.filters, { id: shortId(), name: action.value, operator: 'is', value: [], }, ], }), ); }} > {(setOpen) => ( )} )} {showSegment && chartEvent.segment.startsWith('property_') && ( )}
)} {/* Filters - only for events */} {chartEvent && !isSelectManyEvents && }
); } export function ReportSeries() { const selectedSeries = useSelector((state) => state.report.series); const chartType = useSelector((state) => state.report.chartType); const dispatch = useDispatch(); const { projectId } = useAppParams(); const eventNames = useEventNames({ projectId, }); const showSegment = !['retention', 'funnel'].includes(chartType); const showAddFilter = !['retention'].includes(chartType); const showDisplayNameInput = !['retention'].includes(chartType); const isAddEventDisabled = (chartType === 'retention' || chartType === 'conversion') && selectedSeries.length >= 2; const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => { dispatch(changeEvent(event)); }); const isSelectManyEvents = chartType === 'retention'; const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = selectedSeries.findIndex((e) => e.id === active.id); const newIndex = selectedSeries.findIndex((e) => e.id === over.id); dispatch(reorderEvents({ fromIndex: oldIndex, toIndex: newIndex })); } }; const handleMore = (event: IChartEventItem | IChartEvent) => { const callback: ReportEventMoreProps['onClick'] = (action) => { switch (action) { case 'remove': { return dispatch( removeEvent({ id: 'type' in event ? event.id : (event as IChartEvent).id, }), ); } case 'duplicate': { const normalized = 'type' in event ? event : { ...event, type: 'event' as const }; return dispatch(duplicateEvent(normalized)); } } }; return callback; }; const dispatchChangeFormula = useDebounceFn((formula: IChartFormula) => { dispatch(changeEvent(formula)); }); const showFormula = chartType !== 'conversion' && chartType !== 'funnel' && chartType !== 'retention'; return (

Metrics

({ id: ('type' in e ? e.id : (e as IChartEvent).id) ?? '', }))} strategy={verticalListSortingStrategy} >
{selectedSeries.map((event, index) => { const isFormula = event.type === 'formula'; return ( {isFormula ? ( <>
{ dispatchChangeFormula({ ...event, formula: value, }); }} /> {showDisplayNameInput && ( { dispatchChangeFormula({ ...event, displayName: e.target.value, }); }} /> )}
) : ( <> { dispatch( changeEvent( Array.isArray(value) ? { id: event.id, type: 'event', segment: 'user', filters: [ { name: 'name', operator: 'is', value: value, }, ], name: '*', } : { ...event, type: 'event', name: value, filters: [], }, ), ); }} items={eventNames} placeholder="Select event" /> {showDisplayNameInput && ( { dispatchChangeEvent({ ...(event as IChartEventItem & { type: 'event'; }), displayName: e.target.value, }); }} /> )} )}
); })}
{ if (isSelectManyEvents) { dispatch( addEvent({ segment: 'user', name: value, filters: [ { name: 'name', operator: 'is', value: [value], }, ], }), ); } else { dispatch( addEvent({ name: value, segment: 'event', filters: [], }), ); } }} placeholder="Select event" items={eventNames} /> {showFormula && ( )}
); }