Cohort selection.

This commit is contained in:
Mike Cao 2025-08-28 23:29:42 -07:00
parent 05f9a67727
commit bab4f8ebcc
32 changed files with 841 additions and 655 deletions

View file

@ -7,7 +7,7 @@ export function ActionButton({
title,
children,
}: {
onSave?: () => void;
onClick?: () => void;
icon?: ReactNode;
title?: string;
children?: ReactNode;

View file

@ -0,0 +1,18 @@
import { Select, ListItem } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
export interface ActionSelectProps {
value?: string;
onChange?: (value: string) => void;
}
export function ActionSelect({ value = 'path', onChange }: ActionSelectProps) {
const { formatMessage, labels } = useMessages();
return (
<Select value={value} onChange={onChange}>
<ListItem id="path">{formatMessage(labels.viewedPage)}</ListItem>
<ListItem id="event">{formatMessage(labels.triggeredEvent)}</ListItem>
</Select>
);
}

View file

@ -14,7 +14,7 @@ export function CurrencySelect({ value, onChange }) {
value={value}
defaultValue={value}
onChange={onChange}
listProps={{ style: { maxHeight: '300px' } }}
listProps={{ style: { maxHeight: 300 } }}
onSearch={setSearch}
allowSearch
>

View file

@ -30,8 +30,8 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
query: { segment, cohort },
} = useNavigation();
const { filters, operatorLabels } = useFilters();
const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment);
const canSave = filters.length > 0 && !segment && !cohort;
const { data, isLoading } = useWebsiteSegmentQuery(websiteId, segment || cohort);
const canSaveSegment = filters.length > 0 && !segment && !cohort;
const handleCloseFilter = (param: string) => {
router.push(updateParams({ [param]: undefined }));
@ -41,11 +41,11 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
router.push(replaceParams());
};
const handleSegmentRemove = () => {
router.push(updateParams({ segment: undefined }));
const handleSegmentRemove = (type: string) => {
router.push(updateParams({ [type]: undefined }));
};
if (!filters.length && !segment) {
if (!filters.length && !segment && !cohort) {
return null;
}
@ -58,7 +58,16 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
label={formatMessage(labels.segment)}
value={data?.name || segment}
operator={operatorLabels.eq}
onRemove={handleSegmentRemove}
onRemove={() => handleSegmentRemove('segment')}
/>
)}
{cohort && !isLoading && (
<FilterItem
name="cohort"
label={formatMessage(labels.cohort)}
value={data?.name || cohort}
operator={operatorLabels.eq}
onRemove={() => handleSegmentRemove('cohort')}
/>
)}
{filters.map(filter => {
@ -79,7 +88,7 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
</Row>
<Row alignItems="center">
<DialogTrigger>
{canSave && (
{canSaveSegment && (
<TooltipTrigger delay={0}>
<Button variant="zero">
<Icon>

View file

@ -1,40 +1,43 @@
import { useState } from 'react';
import { Column, Tabs, TabList, Tab, TabPanel, Row, Button } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { useFilters, useMessages, useNavigation } from '@/components/hooks';
import { FieldFilters } from '@/components/input/FieldFilters';
import { SegmentFilters } from '@/components/input/SegmentFilters';
export interface FilterEditFormProps {
websiteId?: string;
filters: any[];
segmentId?: string;
onChange?: (params: { filters: any[]; segment: any }) => void;
onChange?: (params: { filters: any[]; segment?: string; cohort?: string }) => void;
onClose?: () => void;
}
export function FilterEditForm({
websiteId,
filters = [],
segmentId,
onChange,
onClose,
}: FilterEditFormProps) {
export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormProps) {
const {
query: { segment, cohort },
} = useNavigation();
const { filters } = useFilters();
const { formatMessage, labels } = useMessages();
const [currentFilters, setCurrentFilters] = useState(filters);
const [currentSegment, setCurrentSegment] = useState(segmentId);
const [currentSegment, setCurrentSegment] = useState(segment);
const [currentCohort, setCurrentCohort] = useState(cohort);
const handleReset = () => {
setCurrentFilters([]);
setCurrentSegment(null);
setCurrentSegment(undefined);
setCurrentCohort(undefined);
};
const handleSave = () => {
onChange?.({ filters: currentFilters.filter(f => f.value), segment: currentSegment });
onChange?.({
filters: currentFilters.filter(f => f.value),
segment: currentSegment,
cohort: currentCohort,
});
onClose?.();
};
const handleSegmentChange = (id: string) => {
setCurrentSegment(id);
const handleSegmentChange = (id: string, type: string) => {
setCurrentSegment(type === 'segment' ? id : undefined);
setCurrentCohort(type === 'cohort' ? id : undefined);
};
return (
@ -43,6 +46,7 @@ export function FilterEditForm({
<TabList>
<Tab id="fields">{formatMessage(labels.fields)}</Tab>
<Tab id="segments">{formatMessage(labels.segments)}</Tab>
<Tab id="cohorts">{formatMessage(labels.cohorts)}</Tab>
</TabList>
<TabPanel id="fields">
<FieldFilters websiteId={websiteId} value={currentFilters} onChange={setCurrentFilters} />
@ -51,7 +55,15 @@ export function FilterEditForm({
<SegmentFilters
websiteId={websiteId}
segmentId={currentSegment}
onSave={handleSegmentChange}
onChange={handleSegmentChange}
/>
</TabPanel>
<TabPanel id="cohorts" style={{ height: 400 }}>
<SegmentFilters
type="cohort"
websiteId={websiteId}
segmentId={currentCohort}
onChange={handleSegmentChange}
/>
</TabPanel>
</Tabs>

View file

@ -0,0 +1,65 @@
import { SetStateAction, useMemo, useState } from 'react';
import { endOfDay, subMonths } from 'date-fns';
import { ComboBox, ListItem, Loading, useDebounce, ComboBoxProps } from '@umami/react-zen';
import { Empty } from '@/components/common/Empty';
import { useMessages, useWebsiteValuesQuery } from '@/components/hooks';
export interface LookupFieldProps extends ComboBoxProps {
websiteId: string;
type: string;
value: string;
onChange: (value: string) => void;
}
export function LookupField({ websiteId, type, value, onChange, ...props }: LookupFieldProps) {
const { formatMessage, messages } = useMessages();
const [search, setSearch] = useState(value);
const searchValue = useDebounce(search, 300);
const startDate = subMonths(endOfDay(new Date()), 6);
const endDate = endOfDay(new Date());
const { data, isLoading } = useWebsiteValuesQuery({
websiteId,
type,
search: searchValue,
startDate,
endDate,
});
const items: string[] = useMemo(() => {
return data?.map(({ value }) => value) || [];
}, [data]);
const handleSearch = (value: SetStateAction<string>) => {
setSearch(value);
};
return (
<ComboBox
aria-label="LookupField"
{...props}
items={items}
inputValue={value}
onInputChange={value => {
handleSearch(value);
onChange?.(value);
}}
formValue="text"
allowsEmptyCollection
allowsCustomValue
renderEmptyState={() =>
isLoading ? (
<Loading position="center" icon="dots" />
) : (
<Empty message={formatMessage(messages.noResultsFound)} />
)
}
>
{items.map(item => (
<ListItem key={item} id={item}>
{item}
</ListItem>
))}
</ComboBox>
);
}

View file

@ -5,14 +5,20 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
export interface SegmentFiltersProps {
websiteId: string;
segmentId: string;
onSave?: (data: any) => void;
type?: string;
onChange?: (id: string, type: string) => void;
}
export function SegmentFilters({ websiteId, segmentId, onSave }: SegmentFiltersProps) {
const { data, isLoading, isFetching } = useWebsiteSegmentsQuery(websiteId, { type: 'segment' });
export function SegmentFilters({
websiteId,
segmentId,
type = 'segment',
onChange,
}: SegmentFiltersProps) {
const { data, isLoading, isFetching } = useWebsiteSegmentsQuery(websiteId, { type });
const handleChange = (id: string) => {
onSave?.(id);
onChange?.(id, type);
};
return (