From 1fa61b1ae99e8b0eef3b91f51a8595cbd042addf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Mon, 24 Nov 2025 15:50:28 +0100 Subject: [PATCH] ts --- apps/api/src/controllers/export.controller.ts | 6 +- .../report-chart/common/report-table.tsx | 10 +- .../report-chart/conversion/chart.tsx | 11 +- .../src/components/report-chart/shortcut.tsx | 6 +- .../src/components/report/reportSlice.ts | 47 ++-- .../sidebar/EventPropertiesCombobox.tsx | 4 +- .../report/sidebar/ReportEvents.tsx | 254 +++++++++--------- .../report/sidebar/ReportSeries.tsx | 15 +- .../report/sidebar/ReportSidebar.tsx | 1 - .../report/sidebar/filters/FilterItem.tsx | 6 +- .../src/modals/add-notification-rule.tsx | 3 +- apps/start/src/modals/event-details.tsx | 3 +- ...zationId.$projectId.events._tabs.stats.tsx | 20 +- .../7-migrate-events-to-series.ts | 104 +++++++ packages/db/src/engine/plan.ts | 17 +- .../db/src/services/conversion.service.ts | 13 +- packages/db/src/services/funnel.service.ts | 9 +- packages/db/src/services/reports.service.ts | 75 ++---- packages/trpc/src/routers/chart.ts | 8 +- packages/validation/src/types.validation.ts | 4 + 20 files changed, 321 insertions(+), 295 deletions(-) create mode 100644 packages/db/code-migrations/7-migrate-events-to-series.ts diff --git a/apps/api/src/controllers/export.controller.ts b/apps/api/src/controllers/export.controller.ts index 850fd768..ac622908 100644 --- a/apps/api/src/controllers/export.controller.ts +++ b/apps/api/src/controllers/export.controller.ts @@ -13,11 +13,7 @@ import { getSettingsForProject, } from '@openpanel/db'; import { ChartEngine } from '@openpanel/db'; -import { - zChartEvent, - zChartInput, - zChartInputBase, -} from '@openpanel/validation'; +import { zChartEvent, zChartInputBase } from '@openpanel/validation'; import { omit } from 'ramda'; async function getProjectId( diff --git a/apps/start/src/components/report-chart/common/report-table.tsx b/apps/start/src/components/report-chart/common/report-table.tsx index fb3c538d..2080d7bc 100644 --- a/apps/start/src/components/report-chart/common/report-table.tsx +++ b/apps/start/src/components/report-chart/common/report-table.tsx @@ -1,12 +1,4 @@ import { Checkbox } from '@/components/ui/checkbox'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow as UITableRow, -} from '@/components/ui/table'; import { useFormatDateInterval } from '@/hooks/use-format-date-interval'; import { useNumber } from '@/hooks/use-numer-formatter'; import { useSelector } from '@/redux'; @@ -28,8 +20,8 @@ import { } from '@tanstack/react-virtual'; import throttle from 'lodash.throttle'; import { ChevronDown, ChevronRight } from 'lucide-react'; -import { memo, useEffect, useMemo, useRef, useState } from 'react'; import type * as React from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { ReportTableToolbar } from './report-table-toolbar'; import { diff --git a/apps/start/src/components/report-chart/conversion/chart.tsx b/apps/start/src/components/report-chart/conversion/chart.tsx index 4237a351..5553d243 100644 --- a/apps/start/src/components/report-chart/conversion/chart.tsx +++ b/apps/start/src/components/report-chart/conversion/chart.tsx @@ -30,16 +30,7 @@ interface Props { export function Chart({ data }: Props) { const { - report: { - previous, - interval, - projectId, - startDate, - endDate, - range, - lineType, - events, - }, + report: { interval, projectId, startDate, endDate, range, lineType }, isEditMode, options: { hideXAxis, hideYAxis, maxDomain }, } = useReportChartContext(); diff --git a/apps/start/src/components/report-chart/shortcut.tsx b/apps/start/src/components/report-chart/shortcut.tsx index bcf3cfd2..c42e6aa4 100644 --- a/apps/start/src/components/report-chart/shortcut.tsx +++ b/apps/start/src/components/report-chart/shortcut.tsx @@ -7,7 +7,7 @@ type ChartRootShortcutProps = Omit & { previous?: ReportChartProps['report']['previous']; chartType?: ReportChartProps['report']['chartType']; interval?: ReportChartProps['report']['interval']; - events: ReportChartProps['report']['events']; + series: ReportChartProps['report']['series']; breakdowns?: ReportChartProps['report']['breakdowns']; lineType?: ReportChartProps['report']['lineType']; }; @@ -18,7 +18,7 @@ export const ReportChartShortcut = ({ previous = false, chartType = 'linear', interval = 'day', - events, + series, breakdowns, lineType = 'monotone', options, @@ -33,7 +33,7 @@ export const ReportChartShortcut = ({ previous, chartType, interval, - events, + series, lineType, metric: 'sum', }} diff --git a/apps/start/src/components/report/reportSlice.ts b/apps/start/src/components/report/reportSlice.ts index b216e549..9ec30e32 100644 --- a/apps/start/src/components/report/reportSlice.ts +++ b/apps/start/src/components/report/reportSlice.ts @@ -11,7 +11,6 @@ import { } from '@openpanel/constants'; import type { IChartBreakdown, - IChartEvent, IChartEventItem, IChartFormula, IChartLineType, @@ -19,6 +18,7 @@ import type { IChartRange, IChartType, IInterval, + UnionOmit, zCriteria, } from '@openpanel/validation'; import type { z } from 'zod'; @@ -89,37 +89,26 @@ export const reportSlice = createSlice({ state.name = action.payload; }, // Series (Events and Formulas) - addEvent: (state, action: PayloadAction>) => { - state.dirty = true; - state.series.push({ - id: shortId(), - type: 'event', - ...action.payload, - } as IChartEventItem); - }, - addFormula: ( + addSerie: ( state, - action: PayloadAction>, + action: PayloadAction>, ) => { state.dirty = true; state.series.push({ id: shortId(), ...action.payload, - } as IChartEventItem); + }); }, - duplicateEvent: ( - state, - action: PayloadAction, - ) => { + duplicateEvent: (state, action: PayloadAction) => { state.dirty = true; if (action.payload.type === 'event') { state.series.push({ - ...action.payload, - filters: action.payload.filters.map((filter) => ({ - ...filter, + ...action.payload, + filters: action.payload.filters.map((filter) => ({ + ...filter, + id: shortId(), + })), id: shortId(), - })), - id: shortId(), } as IChartEventItem); } else { state.series.push({ @@ -135,19 +124,14 @@ export const reportSlice = createSlice({ }>, ) => { state.dirty = true; - state.series = state.series.filter( - (event) => { - // Handle both old format (no type) and new format - const eventId = 'type' in event ? event.id : (event as IChartEvent).id; - return eventId !== action.payload.id; - }, - ); + state.series = state.series.filter((event) => { + return event.id !== action.payload.id; + }); }, changeEvent: (state, action: PayloadAction) => { state.dirty = true; state.series = state.series.map((event) => { - const eventId = 'type' in event ? event.id : (event as IChartEvent).id; - if (eventId === action.payload.id) { + if (event.id === action.payload.id) { return action.payload; } return event; @@ -307,8 +291,7 @@ export const { ready, setReport, setName, - addEvent, - addFormula, + addSerie, removeEvent, duplicateEvent, changeEvent, diff --git a/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx b/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx index fe702087..059c47c7 100644 --- a/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx +++ b/apps/start/src/components/report/sidebar/EventPropertiesCombobox.tsx @@ -1,8 +1,7 @@ import { Combobox } from '@/components/ui/combobox'; import { useAppParams } from '@/hooks/use-app-params'; import { useEventProperties } from '@/hooks/use-event-properties'; -import { useDispatch, useSelector } from '@/redux'; -import { api } from '@/trpc/client'; +import { useDispatch } from '@/redux'; import { cn } from '@/utils/cn'; import { DatabaseIcon } from 'lucide-react'; @@ -43,6 +42,7 @@ export function EventPropertiesCombobox({ changeEvent({ ...event, property: value, + type: 'event', }), ); }} diff --git a/apps/start/src/components/report/sidebar/ReportEvents.tsx b/apps/start/src/components/report/sidebar/ReportEvents.tsx index 20004f7e..6cb03682 100644 --- a/apps/start/src/components/report/sidebar/ReportEvents.tsx +++ b/apps/start/src/components/report/sidebar/ReportEvents.tsx @@ -1,6 +1,8 @@ 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'; @@ -23,19 +25,16 @@ import { 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 type { IChartEventItem, IChartFormula } from '@openpanel/validation'; import { FilterIcon, HandIcon, PlusIcon } from 'lucide-react'; import { ReportSegment } from '../ReportSegment'; import { - addEvent, - addFormula, + addSerie, changeEvent, duplicateEvent, removeEvent, reorderEvents, } from '../reportSlice'; -import { InputEnter } from '@/components/ui/input-enter'; -import { Button } from '@/components/ui/button'; import { EventPropertiesCombobox } from './EventPropertiesCombobox'; import { PropertiesCombobox } from './PropertiesCombobox'; import type { ReportEventMoreProps } from './ReportEventMore'; @@ -50,28 +49,22 @@ function SortableEvent({ isSelectManyEvents, ...props }: { - event: IChartEventItem | IChartEvent; + event: IChartEventItem; 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 ?? '' }); + useSortable({ id: event.id ?? '' }); 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' }); + const isEvent = event.type === 'event'; return (
@@ -88,15 +81,15 @@ function SortableEvent({
{/* Segment and Filter buttons - only for events */} - {chartEvent && (showSegment || showAddFilter) && ( + {isEvent && (showSegment || showAddFilter) && (
{showSegment && ( { dispatch( changeEvent({ - ...chartEvent, + ...event, segment, }), ); @@ -105,13 +98,13 @@ function SortableEvent({ )} {showAddFilter && ( { dispatch( changeEvent({ - ...chartEvent, + ...event, filters: [ - ...chartEvent.filters, + ...event.filters, { id: shortId(), name: action.value, @@ -135,20 +128,20 @@ function SortableEvent({ )} - {showSegment && chartEvent.segment.startsWith('property_') && ( - + {showSegment && event.segment.startsWith('property_') && ( + )}
)} {/* Filters - only for events */} - {chartEvent && !isSelectManyEvents && } + {isEvent && !isSelectManyEvents && } ); } export function ReportEvents() { - const selectedEvents = useSelector((state) => state.report.events); + const selectedEvents = useSelector((state) => state.report.series); const chartType = useSelector((state) => state.report.chartType); const dispatch = useDispatch(); const { projectId } = useAppParams(); @@ -162,7 +155,7 @@ export function ReportEvents() { const isAddEventDisabled = (chartType === 'retention' || chartType === 'conversion') && selectedEvents.length >= 2; - const dispatchChangeEvent = useDebounceFn((event: IChartEvent) => { + const dispatchChangeEvent = useDebounceFn((event: IChartEventItem) => { dispatch(changeEvent(event)); }); const isSelectManyEvents = chartType === 'retention'; @@ -185,15 +178,18 @@ export function ReportEvents() { } }; - const handleMore = (event: IChartEventItem | IChartEvent) => { + const handleMore = (event: IChartEventItem) => { const callback: ReportEventMoreProps['onClick'] = (action) => { switch (action) { case 'remove': { - return dispatch(removeEvent({ id: 'type' in event ? event.id : (event as IChartEvent).id })); + return dispatch( + removeEvent({ + id: event.id, + }), + ); } case 'duplicate': { - const normalized = 'type' in event ? event : { ...event, type: 'event' as const }; - return dispatch(duplicateEvent(normalized)); + return dispatch(duplicateEvent(event)); } } }; @@ -205,7 +201,10 @@ export function ReportEvents() { dispatch(changeEvent(formula)); }); - const showFormula = chartType !== 'conversion' && chartType !== 'funnel' && chartType !== 'retention'; + const showFormula = + chartType !== 'conversion' && + chartType !== 'funnel' && + chartType !== 'retention'; return (
@@ -216,20 +215,17 @@ export function ReportEvents() { onDragEnd={handleDragEnd} > ({ id: ('type' in e ? e.id : (e as IChartEvent).id) ?? '' }))} + items={selectedEvents.map((e) => ({ id: e.id! }))} strategy={verticalListSortingStrategy} >
{selectedEvents.map((event, index) => { - // Normalize event to have type field - const normalized: IChartEventItem = - 'type' in event ? event : { ...event, type: 'event' as const }; - const isFormula = normalized.type === 'formula'; + const isFormula = event.type === 'formula'; return ( { dispatchChangeFormula({ - ...normalized, + ...event, formula: value, }); }} @@ -252,75 +248,75 @@ export function ReportEvents() { {showDisplayNameInput && ( { dispatchChangeFormula({ - ...normalized, + ...event, displayName: e.target.value, }); }} /> )}
- + ) : ( <> - { - dispatch( - changeEvent( - Array.isArray(value) - ? { - id: normalized.id, + { + dispatch( + changeEvent( + Array.isArray(value) + ? { + id: event.id, type: 'event', - segment: 'user', - filters: [ - { - name: 'name', - operator: 'is', - value: value, + segment: 'user', + filters: [ + { + name: 'name', + operator: 'is', + value: value, + }, + ], + name: '*', + } + : { + ...event, + type: 'event', + name: value, + filters: [], }, - ], - name: '*', - } - : { - ...normalized, - type: 'event', - name: value, - filters: [], - }, - ), - ); - }} - items={eventNames} - placeholder="Select event" - /> - {showDisplayNameInput && ( - { - dispatchChangeEvent({ - ...(normalized as IChartEventItem & { type: 'event' }), - displayName: e.target.value, - }); - }} - /> - )} - + ), + ); + }} + items={eventNames} + placeholder="Select event" + /> + {showDisplayNameInput && ( + { + dispatchChangeEvent({ + ...event, + displayName: e.target.value, + }); + }} + /> + )} + )} @@ -328,38 +324,40 @@ export function ReportEvents() { })}
- { - 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} - /> + { + if (isSelectManyEvents) { + dispatch( + addSerie({ + type: 'event', + segment: 'user', + name: value, + filters: [ + { + name: 'name', + operator: 'is', + value: [value], + }, + ], + }), + ); + } else { + dispatch( + addSerie({ + type: 'event', + name: value, + segment: 'event', + filters: [], + }), + ); + } + }} + placeholder="Select event" + items={eventNames} + /> {showFormula && (