From 09e5196ea39094b1da7a0e08ffcc1c08a419b6e7 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Sat, 28 Mar 2026 10:28:59 -0700 Subject: [PATCH] Goal/Funnel mobile friendly changes. Polish event-data filters UI/UX --- .../(reports)/funnels/EventDataFilterRow.tsx | 252 ++++++++++++++ .../[websiteId]/(reports)/funnels/Funnel.tsx | 31 +- .../(reports)/funnels/FunnelAddButton.tsx | 25 +- .../(reports)/funnels/FunnelEditForm.tsx | 329 +++++------------- .../[websiteId]/(reports)/goals/Goal.tsx | 16 +- .../(reports)/goals/GoalAddButton.tsx | 29 +- .../(reports)/goals/GoalEditForm.tsx | 26 +- src/components/input/DialogButton.tsx | 33 +- src/components/input/FilterEditForm.tsx | 2 +- src/components/input/LookupField.tsx | 19 +- src/components/input/ReportEditButton.tsx | 43 ++- src/queries/sql/reports/getFunnel.ts | 10 +- 12 files changed, 472 insertions(+), 343 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx new file mode 100644 index 000000000..f970c62ff --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/EventDataFilterRow.tsx @@ -0,0 +1,252 @@ +import { + Button, + Column, + ComboBox, + FormField, + Grid, + Icon, + ListItem, + Loading, + Select, + useDebounce, +} from '@umami/react-zen'; +import { endOfDay, subMonths } from 'date-fns'; +import { useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { useApi, useMessages, useMobile } from '@/components/hooks'; +import { X } from '@/components/icons'; + +export function getEventDataDateRange() { + return { + startAt: +subMonths(endOfDay(new Date()), 6), + endAt: +endOfDay(new Date()), + }; +} + +function PropertySelect({ + websiteId, + eventName, + value, + onChange, + onPropertyChange, +}: { + websiteId: string; + eventName?: string; + value?: string; + onChange?: (value: string) => void; + onPropertyChange?: (value: string) => void; +}) { + const { get, useQuery } = useApi(); + const { t, messages } = useMessages(); + const [search, setSearch] = useState(value ?? ''); + const searchValue = useDebounce(search, 300); + const { startAt, endAt } = getEventDataDateRange(); + + const { data, isLoading } = useQuery< + Array<{ eventName: string; propertyName: string; total: number }> + >({ + queryKey: ['event-data:properties', { websiteId, eventName, searchValue, startAt, endAt }], + queryFn: () => + get(`/websites/${websiteId}/event-data/properties`, { + startAt, + endAt, + ...(eventName ? { event: eventName } : {}), + ...(searchValue ? { propertyName: searchValue } : {}), + }), + enabled: !!websiteId, + }); + + const properties = [...new Set(data?.map(d => d.propertyName) ?? [])]; + + return ( + { + setSearch(v); + onChange?.(v); + onPropertyChange?.(v); + }} + formValue="text" + allowsEmptyCollection + allowsCustomValue + renderEmptyState={() => + isLoading ? ( + + ) : ( + + ) + } + > + {properties.map(p => ( + + {p} + + ))} + + ); +} + +function ValueSelect({ + websiteId, + eventName, + propertyName, + value, + onChange, +}: { + websiteId: string; + eventName?: string; + propertyName?: string; + value?: string; + onChange?: (value: string) => void; +}) { + const { get, useQuery } = useApi(); + const { t, messages } = useMessages(); + const { startAt, endAt } = getEventDataDateRange(); + + const { data, isLoading } = useQuery>({ + queryKey: ['event-data:values', { websiteId, eventName, propertyName, startAt, endAt }], + queryFn: () => + get(`/websites/${websiteId}/event-data/values`, { + startAt, + endAt, + event: eventName, + propertyName, + }), + enabled: !!(websiteId && eventName && propertyName), + }); + + const values = data?.map(d => d.value) ?? []; + + return ( + { + onChange?.(v); + }} + formValue="text" + allowsEmptyCollection + allowsCustomValue + renderEmptyState={() => + isLoading ? ( + + ) : ( + + ) + } + > + {values.map(v => ( + + {v} + + ))} + + ); +} + +function OperatorSelect({ + value = 'eq', + onChange, +}: { + value?: string; + onChange?: (value: string) => void; +}) { + const { t, labels } = useMessages(); + return ( + + ); +} + +export interface EventDataFilterRowProps { + stepIndex: number; + filterIndex: number; + websiteId: string; + eventName: string; + initialProperty?: string; + onRemove: () => void; +} + +export function EventDataFilterRow({ + stepIndex, + filterIndex, + websiteId, + eventName, + initialProperty, + onRemove, +}: EventDataFilterRowProps) { + const { t, labels } = useMessages(); + const { isMobile } = useMobile(); + const [propertyName, setPropertyName] = useState(initialProperty ?? ''); + + if (isMobile) { + return ( + + + + setPropertyName(v)} + /> + + + + + + + + + + + ); + } + + return ( + + + + setPropertyName(v)} + /> + + + + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx index 589831cf5..4d599f70d 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx @@ -1,4 +1,3 @@ -import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useMessages, useNavigation, useOperatorLabels, useResultQuery } from '@/components/hooks'; import { File, User } from '@/components/icons'; @@ -7,6 +6,7 @@ import { ChangeLabel } from '@/components/metrics/ChangeLabel'; import { Lightning } from '@/components/svg'; import { formatLongNumber } from '@/lib/format'; import type { FunnelResult } from '@/queries/sql/reports/getFunnel'; +import { Box, Column, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen'; import { FunnelEditForm } from './FunnelEditForm'; interface FunnelProps { @@ -17,7 +17,7 @@ interface FunnelProps { websiteId: string; } -export function Funnel({ id, name, type, parameters, websiteId }) { +export function Funnel({ id, name, type, parameters, websiteId }: FunnelProps) { const { t, labels } = useMessages(); const { pathname } = useNavigation(); const isSharePage = pathname.includes('/share/'); @@ -41,14 +41,15 @@ export function Funnel({ id, name, type, parameters, websiteId }) { {!isSharePage && ( - - {({ close }) => { - return ( - - - - ); - }} + + {({ close }) => } )} @@ -100,15 +101,11 @@ export function Funnel({ id, name, type, parameters, websiteId }) { {filters?.map((f, i) => ( - - {f.property} - - + {f.property} + {operatorLabels[f.operator] ?? f.operator} - - {f.value} - + {f.value} ))} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx index 699ff83bf..89cdb789a 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelAddButton.tsx @@ -1,24 +1,21 @@ -import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { Plus } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; import { FunnelEditForm } from './FunnelEditForm'; export function FunnelAddButton({ websiteId }: { websiteId: string }) { const { t, labels } = useMessages(); return ( - - - - - {({ close }) => } - - - + } + label={t(labels.funnel)} + title={t(labels.funnel)} + width="700px" + height="600px" + > + {({ close }) => } + ); } diff --git a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx index 43c92585d..f8baa5c9e 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelEditForm.tsx @@ -1,7 +1,6 @@ import { Button, Column, - ComboBox, Form, FormButtons, FormField, @@ -9,174 +8,21 @@ import { FormSubmitButton, Grid, Icon, - ListItem, ListSeparator, Loading, Row, - Select, Text, TextField, - useDebounce, } from '@umami/react-zen'; -import { endOfDay, subMonths } from 'date-fns'; import { Fragment, useState } from 'react'; -import { Empty } from '@/components/common/Empty'; -import { useApi, useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks'; +import { useApi, useMessages, useMobile, useReportQuery, useUpdateQuery } from '@/components/hooks'; import { Plus, X } from '@/components/icons'; import { ActionSelect } from '@/components/input/ActionSelect'; import { LookupField } from '@/components/input/LookupField'; +import { EventDataFilterRow, getEventDataDateRange } from './EventDataFilterRow'; const FUNNEL_STEPS_MAX = 8; -function getEventDataDateRange() { - return { - startAt: +subMonths(endOfDay(new Date()), 6), - endAt: +endOfDay(new Date()), - }; -} - -function PropertySelect({ - websiteId, - eventName, - value, - onChange, - onPropertyChange, -}: { - websiteId: string; - eventName?: string; - value?: string; - onChange?: (value: string) => void; - onPropertyChange?: (value: string) => void; -}) { - const { get, useQuery } = useApi(); - const { t, messages } = useMessages(); - const [search, setSearch] = useState(value ?? ''); - const searchValue = useDebounce(search, 300); - const { startAt, endAt } = getEventDataDateRange(); - - const { data, isLoading } = useQuery< - Array<{ eventName: string; propertyName: string; total: number }> - >({ - queryKey: ['event-data:properties', { websiteId, eventName, searchValue, startAt, endAt }], - queryFn: () => - get(`/websites/${websiteId}/event-data/properties`, { - startAt, - endAt, - ...(eventName ? { event: eventName } : {}), - ...(searchValue ? { propertyName: searchValue } : {}), - }), - enabled: !!websiteId, - }); - - const properties = [...new Set(data?.map(d => d.propertyName) ?? [])]; - - return ( - { - setSearch(v); - onChange?.(v); - onPropertyChange?.(v); - }} - formValue="text" - allowsEmptyCollection - allowsCustomValue - renderEmptyState={() => - isLoading ? ( - - ) : ( - - ) - } - > - {properties.map(p => ( - - {p} - - ))} - - ); -} - -function ValueSelect({ - websiteId, - eventName, - propertyName, - value, - onChange, -}: { - websiteId: string; - eventName?: string; - propertyName?: string; - value?: string; - onChange?: (value: string) => void; -}) { - const { get, useQuery } = useApi(); - const { t, messages } = useMessages(); - const { startAt, endAt } = getEventDataDateRange(); - - const { data, isLoading } = useQuery>({ - queryKey: ['event-data:values', { websiteId, eventName, propertyName, startAt, endAt }], - queryFn: () => - get(`/websites/${websiteId}/event-data/values`, { - startAt, - endAt, - event: eventName, - propertyName, - }), - enabled: !!(websiteId && eventName && propertyName), - }); - - const values = data?.map(d => d.value) ?? []; - - return ( - { - onChange?.(v); - }} - formValue="text" - allowsEmptyCollection - allowsCustomValue - renderEmptyState={() => - isLoading ? ( - - ) : ( - - ) - } - > - {values.map(v => ( - - {v} - - ))} - - ); -} - -function OperatorSelect({ - value = 'eq', - onChange, -}: { - value?: string; - onChange?: (value: string) => void; -}) { - const { t, labels } = useMessages(); - return ( - - ); -} - function StepRow({ index, websiteId, @@ -189,39 +35,75 @@ function StepRow({ onRemove: () => void; }) { const { t, labels } = useMessages(); + const { isMobile } = useMobile(); + const { get, useQuery } = useApi(); const [eventName, setEventName] = useState(initialEventName ?? ''); + const { startAt, endAt } = getEventDataDateRange(); + + const { data: eventProperties } = useQuery>({ + queryKey: ['event-data:properties', { websiteId, eventName, searchValue: '', startAt, endAt }], + queryFn: () => + get(`/websites/${websiteId}/event-data/properties`, { + startAt, + endAt, + ...(eventName ? { event: eventName } : {}), + }), + enabled: !!websiteId && !!eventName, + }); + + const hasEventData = (eventProperties?.length ?? 0) > 0; + + const valueField = ( + + {({ field, context }) => { + const type = context.watch(`steps.${index}.type`); + return ( + { + setEventName(v); + }} + /> + ); + }} + + ); return ( - - - - - - - - - {({ field, context }) => { - const type = context.watch(`steps.${index}.type`); - return ( - { - setEventName(v); - }} - /> - ); - }} - - - - + {isMobile ? ( + + + + + + {valueField} + + + + ) : ( + + + + + + + + {valueField} + + + + )} {({ fields: filterFields, append: appendFilter, remove: removeFilter, watch }) => { const stepType = watch(`steps.${index}.type`); @@ -235,7 +117,7 @@ function StepRow({ { id: filterId, property: initialProperty }: { id: string; property?: string }, filterIndex: number, ) => ( - ), )} - - - + {hasEventData && ( + + + + )} ); }} @@ -265,51 +149,6 @@ function StepRow({ ); } -function FilterRow({ - stepIndex, - filterIndex, - websiteId, - eventName, - initialProperty, - onRemove, -}: { - stepIndex: number; - filterIndex: number; - websiteId: string; - eventName: string; - initialProperty?: string; - onRemove: () => void; -}) { - const { t, labels } = useMessages(); - const [propertyName, setPropertyName] = useState(initialProperty ?? ''); - - return ( - - - setPropertyName(v)} - /> - - - - - - - - - - ); -} - export function FunnelEditForm({ id, websiteId, @@ -322,7 +161,7 @@ export function FunnelEditForm({ onClose?: () => void; }) { const { t, labels } = useMessages(); - const { data } = useReportQuery(id); + const { data, isLoading } = useReportQuery(id); const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); const handleSubmit = async ({ @@ -345,7 +184,7 @@ export function FunnelEditForm({ ); }; - if (id && !data) { + if (id && isLoading) { return ; } @@ -405,7 +244,7 @@ export function FunnelEditForm({ ); }} - + diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx index d7553eea5..cebefb555 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/Goal.tsx @@ -1,4 +1,4 @@ -import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen'; +import { Column, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useMessages, useNavigation, useResultQuery } from '@/components/hooks'; import { File, User } from '@/components/icons'; @@ -49,18 +49,8 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate {!isSharePage && ( - - {({ close }) => { - return ( - - - - ); - }} + + {({ close }) => } )} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx index bf42b6560..3e1a42750 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalAddButton.tsx @@ -1,28 +1,21 @@ -import { Button, Dialog, DialogTrigger, Icon, Modal, Text } from '@umami/react-zen'; import { useMessages } from '@/components/hooks'; import { Plus } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; import { GoalEditForm } from './GoalEditForm'; export function GoalAddButton({ websiteId }: { websiteId: string }) { const { t, labels } = useMessages(); return ( - - - - - {({ close }) => } - - - + } + label={t(labels.goal)} + title={t(labels.goal)} + minWidth="400px" + minHeight="300px" + > + {({ close }) => } + ); } diff --git a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx index 159e43828..3c3b22e52 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/goals/GoalEditForm.tsx @@ -10,7 +10,7 @@ import { Loading, TextField, } from '@umami/react-zen'; -import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks'; +import { useMessages, useMobile, useReportQuery, useUpdateQuery } from '@/components/hooks'; import { ActionSelect } from '@/components/input/ActionSelect'; import { LookupField } from '@/components/input/LookupField'; @@ -26,6 +26,7 @@ export function GoalEditForm({ onClose?: () => void; }) { const { t, labels } = useMessages(); + const { isMobile } = useMobile(); const { data } = useReportQuery(id); const { mutateAsync, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`); @@ -64,20 +65,33 @@ export function GoalEditForm({ - - + {isMobile ? ( + - - {({ field }) => { return ; }} - + ) : ( + + + + + + + + + {({ field }) => { + return ; + }} + + + + )} diff --git a/src/components/input/DialogButton.tsx b/src/components/input/DialogButton.tsx index 1cba250f8..6038d733b 100644 --- a/src/components/input/DialogButton.tsx +++ b/src/components/input/DialogButton.tsx @@ -18,6 +18,8 @@ export interface DialogButtonProps extends Omit { height?: string; minWidth?: string; minHeight?: string; + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; children?: DialogProps['children']; } @@ -29,6 +31,8 @@ export function DialogButton({ height, minWidth, minHeight, + isOpen, + onOpenChange, children, ...props }: DialogButtonProps) { @@ -39,29 +43,44 @@ export function DialogButton({ minWidth, minHeight, maxHeight: 'calc(100dvh - 40px)', + overflowY: 'auto', padding: '32px', }; if (isMobile) { style.width = '100%'; style.height = '100%'; + style.minWidth = undefined; + style.minHeight = undefined; style.maxHeight = '100%'; style.overflowY = 'auto'; } + const dialog = ( + + {children} + + ); + + if (isOpen !== undefined) { + return ( + + {dialog} + + ); + } + return ( - - {children} - + {dialog} ); diff --git a/src/components/input/FilterEditForm.tsx b/src/components/input/FilterEditForm.tsx index 5e7e6020f..662be24a0 100644 --- a/src/components/input/FilterEditForm.tsx +++ b/src/components/input/FilterEditForm.tsx @@ -112,7 +112,7 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP - + diff --git a/src/components/input/LookupField.tsx b/src/components/input/LookupField.tsx index 65dc730a1..daa95aa0c 100644 --- a/src/components/input/LookupField.tsx +++ b/src/components/input/LookupField.tsx @@ -1,22 +1,25 @@ +import { Empty } from '@/components/common/Empty'; +import { useMessages, useWebsiteValuesQuery } from '@/components/hooks'; import { ComboBox, type ComboBoxProps, ListItem, Loading, useDebounce } from '@umami/react-zen'; import { endOfDay, subMonths } from 'date-fns'; import { type SetStateAction, useMemo, useState } from 'react'; -import { Empty } from '@/components/common/Empty'; -import { useMessages, useWebsiteValuesQuery } from '@/components/hooks'; -export interface LookupFieldProps extends ComboBoxProps { +export interface LookupFieldProps extends Omit { websiteId: string; type: string; value: string; onChange: (value: string) => void; - /** - * `FormField` injects `onChange` via `cloneElement`, overwriting any custom handler. - * Use `onValueChange` for side effects that must survive that override. - */ onValueChange?: (value: string) => void; } -export function LookupField({ websiteId, type, value, onChange, onValueChange, ...props }: LookupFieldProps) { +export function LookupField({ + websiteId, + type, + value, + onChange, + onValueChange, + ...props +}: LookupFieldProps) { const { t, messages } = useMessages(); const [search, setSearch] = useState(value); const searchValue = useDebounce(search, 300); diff --git a/src/components/input/ReportEditButton.tsx b/src/components/input/ReportEditButton.tsx index f61899277..777b75d5f 100644 --- a/src/components/input/ReportEditButton.tsx +++ b/src/components/input/ReportEditButton.tsx @@ -14,17 +14,28 @@ import { type ReactNode, useState } from 'react'; import { useMessages } from '@/components/hooks'; import { useDeleteQuery } from '@/components/hooks/queries/useDeleteQuery'; import { Edit, MoreHorizontal, Trash } from '@/components/icons'; +import { DialogButton } from './DialogButton'; export function ReportEditButton({ id, name, type, + title, + width, + height, + minWidth, + minHeight, children, onDelete, }: { id: string; name: string; type: string; + title?: ReactNode; + width?: string; + height?: string; + minWidth?: string; + minHeight?: string; onDelete?: () => void; children: ({ close }: { close: () => void }) => ReactNode; }) { @@ -81,18 +92,26 @@ export function ReportEditButton({ - - {showEdit && children({ close: handleClose })} - {showDelete && ( - - {t(messages.confirmDelete, { target: name })} - - )} + !open && handleClose()} + title={title} + width={width} + height={height} + minWidth={minWidth} + minHeight={minHeight} + > + {children} + + !open && handleClose()}> + + {t(messages.confirmDelete, { target: name })} + ); diff --git a/src/queries/sql/reports/getFunnel.ts b/src/queries/sql/reports/getFunnel.ts index fcf30a903..b1b55c2b3 100644 --- a/src/queries/sql/reports/getFunnel.ts +++ b/src/queries/sql/reports/getFunnel.ts @@ -84,6 +84,7 @@ async function relationalQuery( select 1 from event_data _ed${stepIndex}_${fi} where _ed${stepIndex}_${fi}.website_event_id = ${eventAlias}.event_id and _ed${stepIndex}_${fi}.website_id = {{websiteId::uuid}} + and _ed${stepIndex}_${fi}.created_at between {{startDate}} and {{endDate}} and _ed${stepIndex}_${fi}.data_key = {{${keyParam}}} and case when _ed${stepIndex}_${fi}.data_type = 2 then replace(_ed${stepIndex}_${fi}.string_value, '.0000', '') else _ed${stepIndex}_${fi}.string_value end ${op} {{${valParam}}} )`; @@ -210,6 +211,7 @@ async function clickhouseQuery( stepIndex: number, stepFilters: Array | undefined, params: Record, + eventAlias: string, ): string { if (!stepFilters?.length) return ''; @@ -231,9 +233,10 @@ async function clickhouseQuery( } params[valParam] = val; - return `and event_id in ( + return `and ${eventAlias}.event_id in ( select event_id from event_data where website_id = {websiteId:UUID} + and created_at between {startDate:DateTime64} and {endDate:DateTime64} and data_key = {${keyParam}:String} and multiIf(data_type = 2, replaceAll(string_value, '.0000', ''), string_value) ${op} {${valParam}:String} )`; @@ -269,8 +272,11 @@ async function clickhouseQuery( paramValue = cv.value.replace(/^\*|\*$/g, '%'); } + const eventAlias = levelNumber === 1 ? 'level0' : 'y'; const eventDataClause = - !isURL && cv.filters?.length ? buildEventDataFilters(i, cv.filters, extraParams) : ''; + !isURL && cv.filters?.length + ? buildEventDataFilters(i, cv.filters, extraParams, eventAlias) + : ''; if (levelNumber === 1) { pv.levelOneQuery = `\n