From 518474414b2d6622ac33c06c2cd33312acb051fe Mon Sep 17 00:00:00 2001 From: Ayush Jhanwar Date: Thu, 10 Sep 2026 15:59:23 +0530 Subject: [PATCH] feat(report): let users add not-yet-fired events in the event picker The event picker (ComboboxEvents) only offered events already present in the distinct-event-names source, so a not-yet-shipped event could not be added to a chart or funnel ahead of time. Add a synthetic "Create (not seen yet)" item, mirroring ComboboxAdvanced's free-text pattern, shown when the typed name matches no known event, letting you forward-declare an event. It routes through the existing selection handler as a plain name string; the event-name schema is already a free string and the query engine filters on name equality, so an unknown name returns 0 rows and the report auto-populates once the event first fires. Also corrects a latent VirtualList itemKey ("value" -> "name"; the items have no value field). --- .../src/components/ui/combobox-events.tsx | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/apps/start/src/components/ui/combobox-events.tsx b/apps/start/src/components/ui/combobox-events.tsx index 78e53c452..1baa17765 100644 --- a/apps/start/src/components/ui/combobox-events.tsx +++ b/apps/start/src/components/ui/combobox-events.tsx @@ -15,7 +15,12 @@ import { useNumber } from '@/hooks/use-numer-formatter'; import type { RouterOutputs } from '@/trpc/client'; import { cn } from '@/utils/cn'; import { PopoverPortal } from '@radix-ui/react-popover'; -import { CheckIcon, ChevronsUpDown, GanttChartIcon } from 'lucide-react'; +import { + CheckIcon, + ChevronsUpDown, + GanttChartIcon, + PlusIcon, +} from 'lucide-react'; import VirtualList from 'rc-virtual-list'; import * as React from 'react'; import { EventIcon } from '../events/event-icon'; @@ -99,6 +104,44 @@ export function ComboboxEvents< ? find(selectedValues[0]) : null; + const trimmedSearch = search.trim(); + + const filteredItems = React.useMemo(() => { + if (search === '') return items; + return items.filter((item) => + item.name.toLowerCase().includes(search.toLowerCase()), + ); + }, [items, search]); + + // Forward-declared event: when the typed name matches no known event, offer a + // synthetic "Create" item so a not-yet-fired event can still be added to a + // chart/funnel (mirrors ComboboxAdvanced). It flows through the same + // onChange, and the query filters `WHERE name = ` — returning 0 rows + // until the event first fires, at which point the report auto-populates. + const hasExactMatch = React.useMemo( + () => + items.some( + (item) => item.name.toLowerCase() === trimmedSearch.toLowerCase(), + ), + [items, trimmedSearch], + ); + + const showCreateItem = trimmedSearch !== '' && !hasExactMatch; + + type ListItem = (typeof items)[number] & { __create?: boolean }; + + const data = React.useMemo(() => { + const base = filteredItems as ListItem[]; + if (!showCreateItem) return base; + const createItem = { + name: trimmedSearch, + count: 0, + meta: undefined, + __create: true, + } as unknown as ListItem; + return [createItem, ...base]; + }, [filteredItems, showCreateItem, trimmedSearch]); + const handleSelection = (selectedValue: string) => { if (multiple) { const currentValues = selectedValues; @@ -179,15 +222,32 @@ export function ComboboxEvents< Nothing selected { - if (search === '') return true; - return item.name.toLowerCase().includes(search.toLowerCase()); - })} + data={data} itemHeight={32} - itemKey="value" + itemKey="name" className="w-[33em] max-sm:max-w-[100vw]" > {(item) => { + if (item.__create) { + return ( + { + handleSelection(item.name); + }} + > + + + Create "{item.name}" + + + not seen yet + + + ); + } return (