mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 20:57:17 +01:00
Cohort selection.
This commit is contained in:
parent
05f9a67727
commit
bab4f8ebcc
32 changed files with 841 additions and 655 deletions
|
|
@ -7,7 +7,7 @@ export function ActionButton({
|
|||
title,
|
||||
children,
|
||||
}: {
|
||||
onSave?: () => void;
|
||||
onClick?: () => void;
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
children?: ReactNode;
|
||||
|
|
|
|||
18
src/components/input/ActionSelect.tsx
Normal file
18
src/components/input/ActionSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
65
src/components/input/LookupField.tsx
Normal file
65
src/components/input/LookupField.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue