diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
index 96a7d1a74..d201f2c34 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx
@@ -1,42 +1,23 @@
'use client';
import { Column, Grid, ListItem, Select } from '@umami/react-zen';
-import { useMemo, useState } from 'react';
+import { useState } from 'react';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Empty } from '@/components/common/Empty';
-import { useDateRange, useMessages, useWebsiteValuesQuery } from '@/components/hooks';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { WebsiteValueComboBox } from '@/components/input/WebsiteValueComboBox';
import { Attribution } from './Attribution';
-const stepPlaceholder = 'Select an item';
-
export function AttributionPage({ websiteId }: { websiteId: string }) {
const [model, setModel] = useState('first-click');
const [type, setType] = useState('path');
const [step, setStep] = useState('');
- const [search, setSearch] = useState('');
const { t, labels } = useMessages();
const {
dateRange: { startDate, endDate },
} = useDateRange();
- const { data, isLoading } = useWebsiteValuesQuery({
- websiteId,
- type,
- search,
- startDate,
- endDate,
- });
- const items = data?.filter(({ value }) => value) || [];
- const stepItems = useMemo(() => {
- if (!step || items.some(({ value }) => value === step)) {
- return items;
- }
-
- return [{ value: step }, ...items];
- }, [items, step]);
-
const handleTypeChange = (value: any) => {
setType(value as string);
setStep('');
- setSearch('');
};
return (
@@ -66,24 +47,15 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
-
+ onChange={setStep}
+ />
{step ? (
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
index 06939e068..452b776be 100644
--- a/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
+++ b/src/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage.tsx
@@ -1,10 +1,11 @@
'use client';
-import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen';
+import { Column, Grid, ListItem, Row, Select } from '@umami/react-zen';
import { useState } from 'react';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Panel } from '@/components/common/Panel';
import { useDateRange, useMessages } from '@/components/hooks';
import { FilterButtons } from '@/components/input/FilterButtons';
+import { WebsiteValueComboBox } from '@/components/input/WebsiteValueComboBox';
import { Journey } from './Journey';
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
@@ -19,6 +20,8 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
const [steps, setSteps] = useState(DEFAULT_STEP);
const [startStep, setStartStep] = useState('');
const [endStep, setEndStep] = useState('');
+ const stepType = view === 'events' ? 'event' : 'path';
+ const additionalStepType = view === 'all' ? 'event' : undefined;
const buttons = [
{
@@ -52,19 +55,27 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
))}
-
-
diff --git a/src/components/input/WebsiteValueComboBox.test.tsx b/src/components/input/WebsiteValueComboBox.test.tsx
new file mode 100644
index 000000000..0dd6cf51d
--- /dev/null
+++ b/src/components/input/WebsiteValueComboBox.test.tsx
@@ -0,0 +1,84 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import type { ReactNode } from 'react';
+import { expect, test, vi } from 'vitest';
+import { WebsiteValueComboBox } from './WebsiteValueComboBox';
+
+vi.mock('@umami/react-zen', () => ({
+ cn: (...values: string[]) => values.filter(Boolean).join(' '),
+ Label: ({ children }: { children: ReactNode }) => ,
+ ListItem: ({ children }: { children: ReactNode }) =>
{children}
,
+ Loading: () => Loading
,
+ useDebounce: (value: string) => value,
+}));
+
+vi.mock('@/components/common/ComboBox', () => ({
+ ComboBox: ({
+ 'aria-label': ariaLabel,
+ inputValue,
+ onInputValueChange,
+ children,
+ }: {
+ 'aria-label'?: string;
+ inputValue: string;
+ onInputValueChange: (value: string) => void;
+ children: ReactNode;
+ }) => (
+
+ onInputValueChange(event.currentTarget.value)}
+ />
+ {children}
+
+ ),
+}));
+
+vi.mock('@/components/hooks', () => ({
+ useMessages: () => ({
+ t: (value: string) => value,
+ messages: { noResultsFound: 'No results found' },
+ }),
+ useWebsiteValuesQuery: ({ type }: { type: string }) => ({
+ data: type === 'path' ? [{ value: '/home' }] : type === 'event' ? [{ value: 'signup' }] : [],
+ isLoading: false,
+ }),
+}));
+
+test('clears the selected website value when the input is cleared', () => {
+ const onChange = vi.fn();
+
+ render(
+ ,
+ );
+
+ fireEvent.change(screen.getByLabelText('Start step'), { target: { value: '' } });
+
+ expect(onChange).toHaveBeenCalledWith('');
+});
+
+test('combines values from both Journey step types', () => {
+ render(
+ {}}
+ />,
+ );
+
+ expect(screen.getByText('/home')).toBeInTheDocument();
+ expect(screen.getByText('signup')).toBeInTheDocument();
+});
diff --git a/src/components/input/WebsiteValueComboBox.tsx b/src/components/input/WebsiteValueComboBox.tsx
new file mode 100644
index 000000000..fc9b5120a
--- /dev/null
+++ b/src/components/input/WebsiteValueComboBox.tsx
@@ -0,0 +1,95 @@
+import { type ComboBoxProps, cn, Label, ListItem, Loading, useDebounce } from '@umami/react-zen';
+import { useEffect, useMemo, useState } from 'react';
+import { ComboBox } from '@/components/common/ComboBox';
+import { Empty } from '@/components/common/Empty';
+import { useMessages, useWebsiteValuesQuery } from '@/components/hooks';
+
+const DEFAULT_PLACEHOLDER = 'Select an item';
+
+export interface WebsiteValueComboBoxProps
+ extends Omit<
+ ComboBoxProps,
+ 'children' | 'inputValue' | 'items' | 'onChange' | 'onInputValueChange'
+ > {
+ websiteId: string;
+ type: string;
+ additionalType?: string;
+ startDate: Date;
+ endDate: Date;
+ value: string;
+ onChange: (value: string) => void;
+}
+
+export function WebsiteValueComboBox({
+ websiteId,
+ type,
+ additionalType = '',
+ startDate,
+ endDate,
+ value,
+ onChange,
+ label,
+ className,
+ placeholder = DEFAULT_PLACEHOLDER,
+ ...props
+}: WebsiteValueComboBoxProps) {
+ const { t, messages } = useMessages();
+ const [search, setSearch] = useState(value);
+ const searchValue = useDebounce(search, 300);
+ const primaryQuery = useWebsiteValuesQuery({
+ websiteId,
+ type,
+ search: searchValue,
+ startDate,
+ endDate,
+ });
+ const additionalQuery = useWebsiteValuesQuery({
+ websiteId,
+ type: additionalType,
+ search: searchValue,
+ startDate,
+ endDate,
+ });
+
+ useEffect(() => {
+ setSearch('');
+ }, [type, additionalType]);
+
+ const items = useMemo(() => {
+ const values = [...(primaryQuery.data || []), ...(additionalQuery.data || [])]
+ .map(({ value }) => value)
+ .filter(Boolean);
+
+ return [...new Set(values)];
+ }, [primaryQuery.data, additionalQuery.data]);
+
+ return (
+
+ {label && }
+ {
+ setSearch(value);
+ onChange(value);
+ }}
+ renderEmptyState={() =>
+ primaryQuery.isLoading || additionalQuery.isLoading ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {items.map(item => (
+
+ {item}
+
+ ))}
+
+
+ );
+}