implement OR logic to filters, segments, cohorts
Some checks are pending
Node.js CI / build (push) Waiting to run

This commit is contained in:
Francis Cao 2026-02-21 20:00:43 -08:00
parent 79c06787cd
commit cc1e4438d0
64 changed files with 343 additions and 54 deletions

View file

@ -171,6 +171,9 @@
"manager": "مدير",
"max": "الحد الأقصى",
"maximize": "توسيع",
"match": "تطابق",
"match-all": "الكل (AND)",
"match-any": "أي (OR)",
"medium": "وسيط",
"member": "عضو",
"members": "الأعضاء",

View file

@ -171,6 +171,9 @@
"manager": "Кіраўнік",
"max": "Максімум",
"maximize": "Разгарнуць",
"match": "Адпаведнасць",
"match-all": "Усе (AND)",
"match-any": "Любы (OR)",
"medium": "Сярэдні",
"member": "Удзельнік",
"members": "Удзельнікі",

View file

@ -171,6 +171,9 @@
"manager": "Мениджър",
"max": "Максимум",
"maximize": "Разшири",
"match": "Съвпадение",
"match-all": "Всички (AND)",
"match-any": "Някои (OR)",
"medium": "Среден",
"member": "Член",
"members": "Членове",

View file

@ -171,6 +171,9 @@
"manager": "পরিচালক",
"max": "সর্বাধিক",
"maximize": "বিস্তৃত করুন",
"match": "মিলান",
"match-all": "সব (AND)",
"match-any": "যেকোনো (OR)",
"medium": "মাঝারি",
"member": "সদস্য",
"members": "সদস্যগণ",

View file

@ -171,6 +171,9 @@
"manager": "Menadžer",
"max": "Maks",
"maximize": "Proširi",
"match": "Podudaranje",
"match-all": "Sve (AND)",
"match-any": "Bilo koje (OR)",
"medium": "Srednje",
"member": "Član",
"members": "Članovi",

View file

@ -171,6 +171,9 @@
"manager": "Responsable",
"max": "Màx",
"maximize": "Expandeix",
"match": "Coincidència",
"match-all": "Tots (AND)",
"match-any": "Qualsevol (OR)",
"medium": "Mitjà",
"member": "Membre",
"members": "Membres",

View file

@ -171,6 +171,9 @@
"manager": "Správce",
"max": "Max",
"maximize": "Rozbalit",
"match": "Shoda",
"match-all": "Vše (AND)",
"match-any": "Jakýkoli (OR)",
"medium": "Střední",
"member": "Člen",
"members": "Členové",

View file

@ -171,6 +171,9 @@
"manager": "Leder",
"max": "Maks",
"maximize": "Udvid",
"match": "Match",
"match-all": "Alle (AND)",
"match-any": "Enhver (OR)",
"medium": "Medie",
"member": "Medlem",
"members": "Medlemmer",

View file

@ -171,6 +171,9 @@
"manager": "Verwalter",
"max": "Max",
"maximize": "Uusklappe",
"match": "Übereinstimmung",
"match-all": "Alle (AND)",
"match-any": "Beliebige (OR)",
"medium": "Medium",
"member": "Mitglied",
"members": "Mitglieder",

View file

@ -171,6 +171,9 @@
"manager": "Verwaltung",
"max": "Max",
"maximize": "Erweitern",
"match": "Übereinstimmung",
"match-all": "Alle (AND)",
"match-any": "Beliebige (OR)",
"medium": "Medium",
"member": "Mitglied",
"members": "Mitglieder",

View file

@ -171,6 +171,9 @@
"manager": "Διαχειριστής",
"max": "Μέγ",
"maximize": "Expand",
"match": "Αντιστοίχιση",
"match-all": "Όλα (AND)",
"match-any": "Οποιοδήποτε (OR)",
"medium": "Μέσο",
"member": "Μέλος",
"members": "Μέλη",

View file

@ -171,6 +171,9 @@
"manager": "Manager",
"max": "Max",
"maximize": "Expand",
"match": "Match",
"match-all": "All (AND)",
"match-any": "Any (OR)",
"medium": "Medium",
"member": "Member",
"members": "Members",

View file

@ -170,6 +170,9 @@
"manage": "Manage",
"manager": "Manager",
"max": "Max",
"match": "Match",
"match-all": "All (AND)",
"match-any": "Any (OR)",
"maximize": "Maximize",
"medium": "Medium",
"member": "Member",

View file

@ -171,6 +171,9 @@
"manager": "Gerente",
"max": "Máximo",
"maximize": "Expandir",
"match": "Coincidencia",
"match-all": "Todo (AND)",
"match-any": "Cualquiera (OR)",
"medium": "Medio",
"member": "Miembro",
"members": "Miembros",

View file

@ -171,6 +171,9 @@
"manager": "مدیر",
"max": "حداکثر",
"maximize": "گسترش",
"match": "تطابق",
"match-all": "همه (AND)",
"match-any": "هر (OR)",
"medium": "متوسط",
"member": "عضو",
"members": "اعضا",

View file

@ -171,6 +171,9 @@
"manager": "Päällikkö",
"max": "Maksimi",
"maximize": "Laajenna",
"match": "Vastaavuus",
"match-all": "Kaikki (AND)",
"match-any": "Mikä tahansa (OR)",
"medium": "Keskitaso",
"member": "Jäsen",
"members": "Jäsenet",

View file

@ -171,6 +171,9 @@
"manager": "Stjóri",
"max": "Mest",
"maximize": "Víðka",
"match": "Samsvørun",
"match-all": "Allt (AND)",
"match-any": "Nakað (OR)",
"medium": "Miðal",
"member": "Limur",
"members": "Limir",

View file

@ -171,6 +171,9 @@
"manager": "Gestionnaire",
"max": "Max",
"maximize": "Développer",
"match": "Correspondance",
"match-all": "Tous (AND)",
"match-any": "N'importe lequel (OR)",
"medium": "Moyen",
"member": "Membre",
"members": "Membres",

View file

@ -171,6 +171,9 @@
"manager": "Xestor",
"max": "Máx",
"maximize": "Expandir",
"match": "Coincidencia",
"match-all": "Todo (AND)",
"match-any": "Calquera (OR)",
"medium": "Medio",
"member": "Membro",
"members": "Membros",

View file

@ -171,6 +171,9 @@
"manager": "מנהל",
"max": "מקסימום",
"maximize": "הרחב",
"match": "התאמה",
"match-all": "הכל (AND)",
"match-any": "כלשהו (OR)",
"medium": "בינוני",
"member": "חבר",
"members": "חברים",

View file

@ -171,6 +171,9 @@
"manager": "प्रबंधक",
"max": "अधिकतम",
"maximize": "विस्तार करें",
"match": "मेल",
"match-all": "सभी (AND)",
"match-any": "कोई भी (OR)",
"medium": "मध्यम",
"member": "सदस्य",
"members": "सदस्यगण",

View file

@ -171,6 +171,9 @@
"manager": "Upravitelj",
"max": "Maksimum",
"maximize": "Proširi",
"match": "Podudaranje",
"match-all": "Sve (AND)",
"match-any": "Bilo koje (OR)",
"medium": "Srednje",
"member": "Član",
"members": "Članovi",

View file

@ -171,6 +171,9 @@
"manager": "Menedzser",
"max": "Maximum",
"maximize": "Kibontás",
"match": "Egyezés",
"match-all": "Összes (AND)",
"match-any": "Bármelyik (OR)",
"medium": "Közepes",
"member": "Tag",
"members": "Tagok",

View file

@ -171,6 +171,9 @@
"manager": "Manajer",
"max": "Maksimum",
"maximize": "Perluas",
"match": "Cocok",
"match-all": "Semua (AND)",
"match-any": "Salah satu (OR)",
"medium": "Sedang",
"member": "Anggota",
"members": "Anggota",

View file

@ -171,6 +171,9 @@
"manager": "Gestore",
"max": "Massimo",
"maximize": "Espandi",
"match": "Corrispondenza",
"match-all": "Tutti (AND)",
"match-any": "Qualsiasi (OR)",
"medium": "Medio",
"member": "Membro",
"members": "Membri",

View file

@ -171,6 +171,9 @@
"manager": "管理者",
"max": "最大",
"maximize": "展開",
"match": "一致",
"match-all": "すべて (AND)",
"match-any": "いずれか (OR)",
"medium": "メディア",
"member": "メンバー",
"members": "メンバー",

View file

@ -171,6 +171,9 @@
"manager": "អ្នកគ្រប់គ្រង",
"max": "អតិបរមា",
"maximize": "ពង្រីក",
"match": "ត្រូវគ្នា",
"match-all": "ទាំងអស់ (AND)",
"match-any": "ណាមួយ (OR)",
"medium": "មធ្យម",
"member": "សមាជិក",
"members": "សមាជិក",

View file

@ -171,6 +171,9 @@
"manager": "관리자",
"max": "최대",
"maximize": "확장",
"match": "일치",
"match-all": "전체 (AND)",
"match-any": "일부 (OR)",
"medium": "미디엄",
"member": "멤버",
"members": "멤버",

View file

@ -171,6 +171,9 @@
"manager": "Vadovas",
"max": "Maksimumas",
"maximize": "Išplėsti",
"match": "Atitikimas",
"match-all": "Visi (AND)",
"match-any": "Bet kuris (OR)",
"medium": "Vidutinis",
"member": "Narys",
"members": "Nariai",

View file

@ -171,6 +171,9 @@
"manager": "Удирдагч",
"max": "Дээд",
"maximize": "Өргөтгөх",
"match": "Тохирох",
"match-all": "Бүгд (AND)",
"match-any": "Ямар ч (OR)",
"medium": "Дунд",
"member": "Гишүүн",
"members": "Гишүүд",

View file

@ -171,6 +171,9 @@
"manager": "Pengurus",
"max": "Maks",
"maximize": "Expand",
"match": "Padanan",
"match-all": "Semua (AND)",
"match-any": "Mana-mana (OR)",
"medium": "Medium",
"member": "Ahli",
"members": "Ahli",

View file

@ -171,6 +171,9 @@
"manager": "မန်နေဂျာ",
"max": "အများဆုံး",
"maximize": "Expand",
"match": "ကိုက်ညီ",
"match-all": "အားလုံး (AND)",
"match-any": "တစ်ခုခု (OR)",
"medium": "မီဒီယမ်",
"member": "အဖွဲ့ဝင်",
"members": "အဖွဲ့ဝင်များ",

View file

@ -171,6 +171,9 @@
"manager": "Administrator",
"max": "Maks",
"maximize": "Utvid",
"match": "Samsvar",
"match-all": "Alle (AND)",
"match-any": "Enhver (OR)",
"medium": "Medium",
"member": "Bruker",
"members": "Brukere",

View file

@ -171,6 +171,9 @@
"manager": "Beheerder",
"max": "Max",
"maximize": "Uitvouwen",
"match": "Overeenkomst",
"match-all": "Alle (AND)",
"match-any": "Elk (OR)",
"medium": "Medium",
"member": "Gebruiker",
"members": "Gebruikers",

View file

@ -171,6 +171,9 @@
"manager": "Menedżer",
"max": "Maks",
"maximize": "Rozwiń",
"match": "Dopasowanie",
"match-all": "Wszystkie (AND)",
"match-any": "Dowolny (OR)",
"medium": "Medium",
"member": "Członek",
"members": "Członkowie",

View file

@ -171,6 +171,9 @@
"manager": "Gerente",
"max": "Máximo",
"maximize": "Expandir",
"match": "Correspondência",
"match-all": "Todos (AND)",
"match-any": "Qualquer (OR)",
"medium": "Médio",
"member": "Membro",
"members": "Membros",

View file

@ -171,6 +171,9 @@
"manager": "Gestor",
"max": "Máximo",
"maximize": "Expandir",
"match": "Correspondência",
"match-all": "Todos (AND)",
"match-any": "Qualquer (OR)",
"medium": "Médio",
"member": "Membro",
"members": "Membros",

View file

@ -171,6 +171,9 @@
"manager": "Manager",
"max": "Max",
"maximize": "Extinde",
"match": "Potrivire",
"match-all": "Toate (AND)",
"match-any": "Oricare (OR)",
"medium": "Mediu",
"member": "Membru",
"members": "Membri",

View file

@ -171,6 +171,9 @@
"manager": "Менеджер",
"max": "Максимум",
"maximize": "Развернуть",
"match": "Соответствие",
"match-all": "Все (AND)",
"match-any": "Любой (OR)",
"medium": "Средний",
"member": "Участник",
"members": "Участники",

View file

@ -171,6 +171,9 @@
"manager": "කළමනාකරු",
"max": "උපරිම",
"maximize": "Expand",
"match": "ගැළපීම",
"match-all": "සියල්ල (AND)",
"match-any": "ඕනෑම (OR)",
"medium": "මාධ්‍යය",
"member": "සාමාජිකයා",
"members": "සාමාජිකයින්",

View file

@ -171,6 +171,9 @@
"manager": "Manažér",
"max": "Maximum",
"maximize": "Rozbaliť",
"match": "Zhoda",
"match-all": "Všetky (AND)",
"match-any": "Akýkoľvek (OR)",
"medium": "Stredný",
"member": "Člen",
"members": "Členovia",

View file

@ -171,6 +171,9 @@
"manager": "Upravitelj",
"max": "Največ",
"maximize": "Povečaj",
"match": "Ujemanje",
"match-all": "Vse (AND)",
"match-any": "Katero koli (OR)",
"medium": "Medij",
"member": "Član",
"members": "Člani",

View file

@ -171,6 +171,9 @@
"manager": "Ansvarig",
"max": "Max",
"maximize": "Expandera",
"match": "Matchning",
"match-all": "Alla (AND)",
"match-any": "Vilken som helst (OR)",
"medium": "Medium",
"member": "Medlem",
"members": "Medlemmar",

View file

@ -171,6 +171,9 @@
"manager": "மேலாளர்",
"max": "அதிகபட்சம்",
"maximize": "Expand",
"match": "பொருத்தம்",
"match-all": "அனைத்தும் (AND)",
"match-any": "எதுவும் (OR)",
"medium": "ஊடகம்",
"member": "உறுப்பினர்",
"members": "உறுப்பினர்கள்",

View file

@ -171,6 +171,9 @@
"manager": "ผู้จัดการ",
"max": "สูงสุด",
"maximize": "Expand",
"match": "จับคู่",
"match-all": "ทั้งหมด (AND)",
"match-any": "ใดๆ (OR)",
"medium": "สื่อ",
"member": "สมาชิก",
"members": "สมาชิก",

View file

@ -171,6 +171,9 @@
"manager": "Yönetici",
"max": "Maks",
"maximize": "Genişlet",
"match": "Eşleşme",
"match-all": "Tümü (AND)",
"match-any": "Herhangi (OR)",
"medium": "Orta",
"member": "Üye",
"members": "Üyeler",

View file

@ -171,6 +171,9 @@
"manager": "Менеджер",
"max": "Макс.",
"maximize": "Розгорнути",
"match": "Відповідність",
"match-all": "Усі (AND)",
"match-any": "Будь-який (OR)",
"medium": "Середній",
"member": "Учасник",
"members": "Учасники",

View file

@ -171,6 +171,9 @@
"manager": "منتظم",
"max": "زیادہ سے زیادہ",
"maximize": "Expand",
"match": "مطابقت",
"match-all": "سب (AND)",
"match-any": "کوئی بھی (OR)",
"medium": "میڈیم",
"member": "رکن",
"members": "اراکین",

View file

@ -171,6 +171,9 @@
"manager": "Menejer",
"max": "Maksimal",
"maximize": "Kattalashtirish",
"match": "Moslik",
"match-all": "Barchasi (AND)",
"match-any": "Istalgan (OR)",
"medium": "Vosita",
"member": "A'zo",
"members": "A'zolar",

View file

@ -171,6 +171,9 @@
"manager": "Quản lý",
"max": "Tối đa",
"maximize": "Phóng to",
"match": "Khớp",
"match-all": "Tất cả (AND)",
"match-any": "Bất kỳ (OR)",
"medium": "Phương tiện",
"member": "Thành viên",
"members": "Các thành viên",

View file

@ -171,6 +171,9 @@
"manager": "管理者",
"max": "最大",
"maximize": "展开",
"match": "匹配",
"match-all": "全部 (AND)",
"match-any": "任意 (OR)",
"medium": "中等",
"member": "成员",
"members": "成员",

View file

@ -171,6 +171,9 @@
"manager": "管理者",
"max": "最大值",
"maximize": "Expand",
"match": "符合",
"match-all": "全部 (AND)",
"match-any": "任意 (OR)",
"medium": "媒介",
"member": "成員",
"members": "成員",

View file

@ -10,6 +10,7 @@ import {
Loading,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useMessages, useUpdateQuery, useWebsiteCohortQuery } from '@/components/hooks';
import { ActionSelect } from '@/components/input/ActionSelect';
import { DateFilter } from '@/components/input/DateFilter';
@ -32,6 +33,11 @@ export function CohortEditForm({
}) {
const { data } = useWebsiteCohortQuery(websiteId, cohortId);
const { t, labels, messages, getErrorMessage } = useMessages();
const [currentMatch, setCurrentMatch] = useState<string>('all');
useEffect(() => {
setCurrentMatch((data?.parameters as any)?.match || 'all');
}, [data]);
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
`/websites/${websiteId}/segments${cohortId ? `/${cohortId}` : ''}`,
@ -41,14 +47,23 @@ export function CohortEditForm({
);
const handleSubmit = async (formData: any) => {
await mutateAsync(formData, {
onSuccess: async () => {
toast(t(messages.saved));
touch('cohorts');
onSave?.();
onClose?.();
await mutateAsync(
{
...formData,
parameters: {
...formData.parameters,
match: currentMatch !== 'all' ? currentMatch : undefined,
},
},
});
{
onSuccess: async () => {
toast(t(messages.saved));
touch('cohorts');
onSave?.();
onClose?.();
},
},
);
};
if (cohortId && !data) {
@ -105,7 +120,12 @@ export function CohortEditForm({
<Column>
<Label>{t(labels.filters)}</Label>
<FormField name="parameters.filters">
<FieldFilters websiteId={websiteId} exclude={['path', 'event']} />
<FieldFilters
websiteId={websiteId}
exclude={['path', 'event']}
match={currentMatch}
onMatchChange={setCurrentMatch}
/>
</FormField>
</Column>

View file

@ -8,6 +8,7 @@ import {
Loading,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useMessages, useUpdateQuery, useWebsiteSegmentQuery } from '@/components/hooks';
import { FieldFilters } from '@/components/input/FieldFilters';
@ -28,6 +29,11 @@ export function SegmentEditForm({
}) {
const { data } = useWebsiteSegmentQuery(websiteId, segmentId);
const { t, labels, messages, getErrorMessage } = useMessages();
const [currentMatch, setCurrentMatch] = useState<string>('all');
useEffect(() => {
setCurrentMatch((data?.parameters as any)?.match || 'all');
}, [data]);
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
`/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`,
@ -37,14 +43,23 @@ export function SegmentEditForm({
);
const handleSubmit = async (formData: any) => {
await mutateAsync(formData, {
onSuccess: async () => {
toast(t(messages.saved));
touch('segments');
onSave?.();
onClose?.();
await mutateAsync(
{
...formData,
parameters: {
...formData.parameters,
match: currentMatch !== 'all' ? currentMatch : undefined,
},
},
});
{
onSuccess: async () => {
toast(t(messages.saved));
touch('segments');
onSave?.();
onClose?.();
},
},
);
};
if (segmentId && !data) {
@ -64,7 +79,11 @@ export function SegmentEditForm({
<>
<Label>{t(labels.filters)}</Label>
<FormField name="parameters.filters" rules={{ required: t(labels.required) }}>
<FieldFilters websiteId={websiteId} />
<FieldFilters
websiteId={websiteId}
match={currentMatch}
onMatchChange={setCurrentMatch}
/>
</FormField>
</>
)}

View file

@ -21,6 +21,7 @@ export function useFilterParameters() {
segment: query.segment,
cohort: query.cohort,
excludeBounce: query.excludeBounce,
match: query.match,
page: query.page,
pageSize: query.pageSize,
};

View file

@ -3,6 +3,7 @@ import {
Column,
Grid,
Icon,
Label,
List,
ListItem,
ListSection,
@ -12,6 +13,7 @@ import {
MenuTrigger,
Popover,
Row,
Select,
} from '@umami/react-zen';
import { endOfDay, subMonths } from 'date-fns';
import type { Key } from 'react';
@ -24,11 +26,20 @@ export interface FieldFiltersProps {
websiteId: string;
value?: { name: string; operator: string; value: string }[];
exclude?: string[];
match?: string;
onChange?: (data: any) => void;
onMatchChange?: (match: string) => void;
}
export function FieldFilters({ websiteId, value, exclude = [], onChange }: FieldFiltersProps) {
const { t, messages } = useMessages();
export function FieldFilters({
websiteId,
value,
exclude = [],
match = 'all',
onChange,
onMatchChange,
}: FieldFiltersProps) {
const { t, labels, messages } = useMessages();
const { fields, groupLabels } = useFields();
const startDate = subMonths(endOfDay(new Date()), 6);
const endDate = endOfDay(new Date());
@ -126,6 +137,17 @@ export function FieldFilters({ websiteId, value, exclude = [], onChange }: Field
</List>
</Column>
<Column overflow="auto" gapY="4" style={{ contain: 'layout' }}>
{onMatchChange && (
<Row alignItems="center" gap>
<Column gap="1">
<Label>{t(labels.match)}</Label>
<Select value={match} onChange={onMatchChange} style={{ width: 150 }}>
<ListItem id="all">{t(labels.matchAll)}</ListItem>
<ListItem id="any">{t(labels.matchAny)}</ListItem>
</Select>
</Column>
</Row>
)}
{value.map((filter, index) => {
return (
<FilterRecord

View file

@ -6,13 +6,18 @@ import { SegmentFilters } from '@/components/input/SegmentFilters';
export interface FilterEditFormProps {
websiteId?: string;
onChange?: (params: { filters: any[]; segment?: string; cohort?: string }) => void;
onChange?: (params: {
filters: any[];
segment?: string;
cohort?: string;
match?: string;
}) => void;
onClose?: () => void;
}
export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormProps) {
const {
query: { segment, cohort },
query: { segment, cohort, match },
pathname,
} = useNavigation();
const { filters } = useFilters();
@ -20,6 +25,7 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
const [currentFilters, setCurrentFilters] = useState(filters);
const [currentSegment, setCurrentSegment] = useState(segment);
const [currentCohort, setCurrentCohort] = useState(cohort);
const [currentMatch, setCurrentMatch] = useState<string>(match || 'all');
const { isMobile } = useMobile();
const excludeFilters = pathname.includes('/pixels') || pathname.includes('/links');
const excludeEvent = !pathname.endsWith('/events');
@ -28,6 +34,7 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
setCurrentFilters([]);
setCurrentSegment(undefined);
setCurrentCohort(undefined);
setCurrentMatch('all');
};
const handleSave = () => {
@ -35,6 +42,7 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
filters: currentFilters.filter(f => f.value),
segment: currentSegment,
cohort: currentCohort,
match: currentMatch !== 'all' ? currentMatch : undefined,
});
onClose?.();
};
@ -61,7 +69,9 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
<FieldFilters
websiteId={websiteId}
value={currentFilters}
match={currentMatch}
onChange={setCurrentFilters}
onMatchChange={setCurrentMatch}
exclude={
excludeFilters
? ['path', 'title', 'hostname', 'distinctId', 'tag', 'event']
@ -92,7 +102,13 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
<Button onPress={handleReset}>{t(labels.reset)}</Button>
<Row alignItems="center" justifyContent="flex-end" gridColumn="span 2" gap>
<Button onPress={onClose}>{t(labels.cancel)}</Button>
<Button variant="primary" onPress={handleSave}>
<Button
variant="primary"
onPress={handleSave}
isDisabled={
currentFilters.filter(f => f.value).length === 0 && !currentSegment && !currentCohort
}
>
{t(labels.apply)}
</Button>
</Row>

View file

@ -19,13 +19,14 @@ export function WebsiteFilterButton({
const isOverview =
/^\/teams\/[^/]+\/websites\/[^/]+$/.test(pathname) || /^\/share\/[^/]+$/.test(pathname);
const handleChange = ({ filters, segment, cohort }: any) => {
const handleChange = ({ filters, segment, cohort, match }: any) => {
const params = filtersArrayToObject(filters);
const url = updateParams({
...params,
segment,
cohort,
match,
excludeBounce: excludeBounce ? 'true' : undefined,
});

View file

@ -305,6 +305,9 @@ export const labels: Record<string, string> = {
other: 'label.other',
boards: 'label.boards',
apply: 'label.apply',
match: 'label.match',
matchAll: 'label.match-all',
matchAny: 'label.match-any',
link: 'label.link',
links: 'label.links',
pixel: 'label.pixel',

View file

@ -99,32 +99,44 @@ function mapFilter(
}
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}) {
const query = filtersObjectToArray(filters, options).reduce(
(arr, { name, column, operator, paramName }) => {
const isCohort = options?.isCohort;
const { isCohort, cohortMatch, cohortActionName } = options;
const isOr = isCohort ? cohortMatch === 'any' : filters.match === 'any';
const orClauses: string[] = [];
const andClauses: string[] = [];
if (isCohort) {
column = FILTER_COLUMNS[name.slice('cohort_'.length)];
filtersObjectToArray(filters, options).forEach(({ name, column, operator, paramName }) => {
if (isCohort) {
column = FILTER_COLUMNS[name.slice('cohort_'.length)];
}
if (column) {
const isAlwaysAnd = name === 'eventType' || (isCohort && name === cohortActionName);
if (isAlwaysAnd) {
andClauses.push(
`and ${mapFilter(column, operator, name, name === 'eventType' ? 'UInt32' : 'String', paramName)}`,
);
} else if (isOr) {
orClauses.push(mapFilter(column, operator, name, 'String', paramName));
} else {
andClauses.push(`and ${mapFilter(column, operator, name, 'String', paramName)}`);
}
if (column) {
if (name === 'eventType') {
arr.push(`and ${mapFilter(column, operator, name, 'UInt32', paramName)}`);
} else {
arr.push(`and ${mapFilter(column, operator, name, 'String', paramName)}`);
}
if (name === 'referrer') {
arr.push(`and referrer_domain != hostname`);
}
if (name === 'referrer') {
andClauses.push(`and referrer_domain != hostname`);
}
}
});
return arr;
},
[],
);
const parts: string[] = [];
return query.join('\n');
if (orClauses.length > 0) {
parts.push(`and (\n ${orClauses.join('\n or ')}\n)`);
}
parts.push(...andClauses);
return parts.join('\n');
}
function getCohortQuery(filters: Record<string, any>) {
@ -132,7 +144,10 @@ function getCohortQuery(filters: Record<string, any>) {
return '';
}
const filterQuery = getFilterQuery(filters, { isCohort: true });
const cohortMatch = filters.cohort_match;
const cohortActionName = filters.cohort_actionName;
const filterQuery = getFilterQuery(filters, { isCohort: true, cohortMatch, cohortActionName });
return `join (
select distinct session_id
@ -267,7 +282,7 @@ async function rawQuery<T = unknown>(
output_format_json_quote_64bit_integers: 0,
},
});
console.log(query, params);
return (await resultSet.json()) as T;
}

View file

@ -106,30 +106,47 @@ function mapFilter(
}
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
const query = filtersObjectToArray(filters, options).reduce(
(arr, { name, column, operator, prefix = '', paramName }) => {
const isCohort = options?.isCohort;
const { isCohort, cohortMatch, cohortActionName } = options;
const isOr = isCohort ? cohortMatch === 'any' : filters.match === 'any';
const orClauses: string[] = [];
const andClauses: string[] = [];
filtersObjectToArray(filters, options).forEach(
({ name, column, operator, prefix = '', paramName }) => {
if (isCohort) {
column = FILTER_COLUMNS[name.slice('cohort_'.length)];
}
if (column) {
arr.push(`and ${mapFilter(`${prefix}${column}`, operator, name, '', paramName)}`);
const clause = mapFilter(`${prefix}${column}`, operator, name, '', paramName);
const isAlwaysAnd = name === 'eventType' || (isCohort && name === cohortActionName);
if (isAlwaysAnd) {
andClauses.push(`and ${clause}`);
} else if (isOr) {
orClauses.push(clause);
} else {
andClauses.push(`and ${clause}`);
}
if (name === 'referrer') {
arr.push(
andClauses.push(
`and (website_event.referrer_domain != regexp_replace(website_event.hostname, '^www.', '') or website_event.referrer_domain is null)`,
);
}
}
return arr;
},
[],
);
return query.join('\n');
const parts: string[] = [];
if (orClauses.length > 0) {
parts.push(`and (\n ${orClauses.join('\n or ')}\n)`);
}
parts.push(...andClauses);
return parts.join('\n');
}
function getCohortQuery(filters: QueryFilters = {}) {
@ -137,7 +154,10 @@ function getCohortQuery(filters: QueryFilters = {}) {
return '';
}
const filterQuery = getFilterQuery(filters, { isCohort: true });
const cohortMatch = (filters as any).cohort_match;
const cohortActionName = (filters as any).cohort_actionName;
const filterQuery = getFilterQuery(filters, { isCohort: true, cohortMatch, cohortActionName });
return `join
(select distinct website_event.session_id

View file

@ -115,6 +115,8 @@ export async function getQueryFilters(
const dateRange = getRequestDateRange(params);
const filters = getRequestFilters(params);
let match = params?.match;
if (websiteId) {
await setWebsiteDate(websiteId, dateRange);
@ -123,6 +125,10 @@ export async function getQueryFilters(
?.parameters as Record<string, any>;
Object.assign(filters, filtersArrayToObject(segmentParams.filters));
if (segmentParams.match) {
match = segmentParams.match;
}
}
if (params.cohort) {
@ -146,6 +152,10 @@ export async function getQueryFilters(
...filtersArrayToObject(cohortFilters),
cohort_startDate: startDate,
cohort_endDate: endDate,
...(cohortParams.match && {
cohort_match: cohortParams.match,
cohort_actionName: `cohort_${cohortParams.action.type}`,
}),
});
}
@ -157,6 +167,7 @@ export async function getQueryFilters(
return {
...dateRange,
...filters,
match,
page: params?.page,
pageSize: params?.pageSize ? params?.pageSize || DEFAULT_PAGE_SIZE : undefined,
orderBy: params?.orderBy,

View file

@ -67,6 +67,7 @@ export const filterParams = {
cohort: z.uuid().optional(),
eventType: z.coerce.number().int().positive().optional(),
excludeBounce: z.string().optional(),
match: z.enum(['all', 'any']).optional(),
};
export const searchParams = {
@ -295,6 +296,7 @@ export const segmentParamSchema = z.object({
}),
)
.optional(),
match: z.enum(['all', 'any']).optional(),
dateRange: z.string().optional(),
action: z
.object({

View file

@ -53,6 +53,8 @@ export interface QueryOptions {
limit?: number;
prefix?: string;
isCohort?: boolean;
cohortMatch?: string;
cohortActionName?: string;
}
export interface QueryFilters
@ -93,6 +95,7 @@ export interface FilterParams {
cohort?: string;
compare?: string;
excludeBounce?: boolean;
match?: 'all' | 'any';
}
export interface SortParams {