diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx
index 41568558b..62fe1d39a 100644
--- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx
+++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterBar.tsx
@@ -38,6 +38,10 @@ export function EventDataFilterBar({
return t(labels.greaterThanEquals);
case OPERATORS.lessThanEquals:
return t(labels.lessThanEquals);
+ case OPERATORS.before:
+ return t(labels.before);
+ case OPERATORS.after:
+ return t(labels.after);
default:
return op;
}
diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx
index 5d8c1f960..ab394b0d0 100644
--- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx
+++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterEditForm.tsx
@@ -1,10 +1,11 @@
'use client';
import { Button, Column, Grid, Icon, List, ListItem, Menu, MenuItem, MenuTrigger, Popover, Row } from '@umami/react-zen';
+import { format } from 'date-fns';
import { useState } from 'react';
import { Empty } from '@/components/common/Empty';
import { useEventDataFieldsQuery, useMessages, useMobile } from '@/components/hooks';
import { Plus } from '@/components/icons';
-import { OPERATORS } from '@/lib/constants';
+import { DATA_TYPE, OPERATORS } from '@/lib/constants';
import type { EventPropertyFilter } from '@/lib/types';
import { EventDataFilterRecord } from './EventDataFilterRecord';
@@ -32,7 +33,22 @@ export function EventDataFilterEditForm({
const dataType: number = field?.dataType ?? 1;
setFilters(prev => [
...prev,
- { propertyName, dataType, operator: OPERATORS.equals, value: '' },
+ {
+ propertyName,
+ dataType,
+ operator:
+ dataType === DATA_TYPE.date
+ ? OPERATORS.before
+ : dataType === DATA_TYPE.array
+ ? OPERATORS.contains
+ : OPERATORS.equals,
+ value:
+ dataType === DATA_TYPE.boolean
+ ? 'true'
+ : dataType === DATA_TYPE.date
+ ? format(new Date(), 'yyyy-MM-dd')
+ : '',
+ },
]);
};
diff --git a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx
index 16097db1d..e6d58027a 100644
--- a/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx
+++ b/src/app/(main)/websites/[websiteId]/event-data/EventDataFilterRecord.tsx
@@ -1,11 +1,14 @@
'use client';
-import { Button, Column, Grid, Icon, Label, ListItem, Loading, Select, TextField } from '@umami/react-zen';
+import { Button, Calendar, Column, ComboBox, Dialog, DialogTrigger, Grid, Icon, Label, ListItem, Loading, Popover, Row, Select, TextField } from '@umami/react-zen';
+import { format, parseISO } from 'date-fns';
import { useState } from 'react';
+import { DateDisplay } from '@/components/common/DateDisplay';
import { Empty } from '@/components/common/Empty';
import { MultiSelect } from '@/components/common/MultiSelect';
import { useEventDataValuesQuery, useMessages } from '@/components/hooks';
import { X } from '@/components/icons';
-import { OPERATORS } from '@/lib/constants';
+import { DATA_TYPE, OPERATORS } from '@/lib/constants';
+import { getMaxSelectableDate } from '@/lib/date';
import type { EventPropertyFilter, Operator } from '@/lib/types';
const STRING_OPERATORS: Operator[] = [
@@ -24,6 +27,9 @@ const NUMERIC_OPERATORS: Operator[] = [
OPERATORS.greaterThanEquals,
OPERATORS.lessThanEquals,
];
+const BOOLEAN_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals];
+const DATE_OPERATORS: Operator[] = [OPERATORS.before, OPERATORS.after];
+const ARRAY_OPERATORS: Operator[] = [OPERATORS.contains, OPERATORS.doesNotContain];
const MULTI_OPERATORS: Operator[] = [OPERATORS.equals, OPERATORS.notEquals];
const FREE_TEXT_OPERATORS: Operator[] = [
OPERATORS.contains,
@@ -48,15 +54,27 @@ export function EventDataFilterRecord({
const { t, labels, messages } = useMessages();
const [search, setSearch] = useState('');
- const isNumeric = filter.dataType === 2;
- const operators = isNumeric ? NUMERIC_OPERATORS : STRING_OPERATORS;
- const isFreeText = FREE_TEXT_OPERATORS.includes(filter.operator);
+ const isNumeric = filter.dataType === DATA_TYPE.number;
+ const isBoolean = filter.dataType === DATA_TYPE.boolean;
+ const isDate = filter.dataType === DATA_TYPE.date;
+ const isArray = filter.dataType === DATA_TYPE.array;
+ const operators = isNumeric
+ ? NUMERIC_OPERATORS
+ : isBoolean
+ ? BOOLEAN_OPERATORS
+ : isDate
+ ? DATE_OPERATORS
+ : isArray
+ ? ARRAY_OPERATORS
+ : STRING_OPERATORS;
+ const isFreeText = !isArray && FREE_TEXT_OPERATORS.includes(filter.operator);
const { data, isLoading } = useEventDataValuesQuery(
websiteId,
eventName,
filter.propertyName,
- { enabled: !isNumeric && !isFreeText },
+ filter.dataType,
+ { enabled: !isNumeric && !isBoolean && !isDate && !isFreeText },
);
const values = (data as Array<{ value: string }> | undefined)?.map(d => d.value) ?? [];
@@ -77,6 +95,8 @@ export function EventDataFilterRecord({
case OPERATORS.lessThan: return t(labels.lessThan);
case OPERATORS.greaterThanEquals: return t(labels.greaterThanEquals);
case OPERATORS.lessThanEquals: return t(labels.lessThanEquals);
+ case OPERATORS.before: return t(labels.before);
+ case OPERATORS.after: return t(labels.after);
default: return op;
}
};
@@ -104,7 +124,47 @@ export function EventDataFilterRecord({
))}
- {isNumeric || isFreeText ? (
+ {isBoolean ? (
+
+ ) : isDate ? (
+ onChange({ ...filter, value })}
+ />
+ ) : isArray ? (
+ {
+ setSearch(v);
+ onChange({ ...filter, value: v });
+ }}
+ formValue="text"
+ allowsEmptyCollection
+ allowsCustomValue
+ renderEmptyState={() =>
+ isLoading ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {filteredValues.map(v => (
+
+ {v}
+
+ ))}
+
+ ) : isNumeric || isFreeText ? (
onChange({ ...filter, value: v })}
@@ -145,3 +205,62 @@ export function EventDataFilterRecord({
);
}
+
+function DateValuePicker({
+ value,
+ onChange,
+}: {
+ value: string;
+ onChange: (value: string) => void;
+}) {
+ const { t, labels } = useMessages();
+ const [showPicker, setShowPicker] = useState(false);
+ const selectedDate = getSelectedDate(value);
+ const [draftDate, setDraftDate] = useState(selectedDate);
+
+ const handleOpen = () => {
+ setDraftDate(getSelectedDate(value));
+ setShowPicker(true);
+ };
+
+ const handleApply = () => {
+ onChange(format(draftDate, 'yyyy-MM-dd'));
+ setShowPicker(false);
+ };
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+function getSelectedDate(value?: string) {
+ if (!value) {
+ return new Date();
+ }
+
+ const date = parseISO(value);
+
+ return Number.isNaN(date.getTime()) ? new Date() : date;
+}
diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts
index 12e8f2dc0..4f1d8fc62 100644
--- a/src/app/api/websites/[websiteId]/event-data/values/route.ts
+++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts
@@ -14,6 +14,7 @@ export async function GET(
endAt: z.coerce.number().int(),
event: z.string(),
propertyName: z.string(),
+ dataType: z.coerce.number().int().optional(),
...filterParams,
});
@@ -29,12 +30,13 @@ export async function GET(
return unauthorized();
}
- const { propertyName } = query;
+ const { propertyName, dataType } = query;
const filters = await getQueryFilters(query, websiteId);
const data = await getEventDataValues(websiteId, {
...filters,
propertyName,
+ dataType,
});
return json(data);
diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts
index db2a2d7e0..3141e41d1 100644
--- a/src/components/hooks/queries/useEventDataValuesQuery.ts
+++ b/src/components/hooks/queries/useEventDataValuesQuery.ts
@@ -7,6 +7,7 @@ export function useEventDataValuesQuery(
websiteId: string,
event: string,
propertyName: string,
+ dataType?: number,
options?: ReactQueryOptions,
) {
const { get, useQuery } = useApi();
@@ -16,7 +17,7 @@ export function useEventDataValuesQuery(
return useQuery({
queryKey: [
'websites:event-data:values',
- { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName },
+ { websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName, dataType },
],
queryFn: () =>
get(`/websites/${websiteId}/event-data/values`, {
@@ -27,6 +28,7 @@ export function useEventDataValuesQuery(
...filters,
event,
propertyName,
+ dataType,
}),
enabled: !!(websiteId && propertyName),
...options,
diff --git a/src/components/input/DateFilter.tsx b/src/components/input/DateFilter.tsx
index ca55bbefa..4bff007ad 100644
--- a/src/components/input/DateFilter.tsx
+++ b/src/components/input/DateFilter.tsx
@@ -1,10 +1,9 @@
import { Dialog, ListItem, ListSeparator, Modal, Select, type SelectProps } from '@umami/react-zen';
-import { endOfYear } from 'date-fns';
import { Fragment, type Key, useState } from 'react';
import { DateDisplay } from '@/components/common/DateDisplay';
import { useMessages, useMobile } from '@/components/hooks';
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
-import { parseDateRange } from '@/lib/date';
+import { getMaxSelectableDate, parseDateRange } from '@/lib/date';
export interface DateFilterProps extends SelectProps {
value?: string;
@@ -129,7 +128,7 @@ export function DateFilter({
startDate={startDate}
endDate={endDate}
minDate={new Date(2000, 0, 1)}
- maxDate={endOfYear(new Date())}
+ maxDate={getMaxSelectableDate()}
onChange={handlePickerChange}
onClose={() => setShowPicker(false)}
/>
diff --git a/src/components/input/FilterBar.tsx b/src/components/input/FilterBar.tsx
index c5d6e3c10..d8c121222 100644
--- a/src/components/input/FilterBar.tsx
+++ b/src/components/input/FilterBar.tsx
@@ -14,6 +14,7 @@ import {
useFilters,
useFormat,
useMessages,
+ useMobile,
useNavigation,
useWebsiteSegmentQuery,
} from '@/components/hooks';
@@ -22,6 +23,7 @@ import { isSearchOperator } from '@/lib/params';
export function FilterBar({ websiteId }: { websiteId?: string }) {
const { t, labels } = useMessages();
+ const { isMobile } = useMobile();
const { formatValue } = useFormat();
const {
router,
@@ -114,8 +116,19 @@ export function FilterBar({ websiteId }: { websiteId?: string }) {
)}
-
-