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": "مدير", "manager": "مدير",
"max": "الحد الأقصى", "max": "الحد الأقصى",
"maximize": "توسيع", "maximize": "توسيع",
"match": "تطابق",
"match-all": "الكل (AND)",
"match-any": "أي (OR)",
"medium": "وسيط", "medium": "وسيط",
"member": "عضو", "member": "عضو",
"members": "الأعضاء", "members": "الأعضاء",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -106,30 +106,47 @@ function mapFilter(
} }
function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string { function getFilterQuery(filters: Record<string, any>, options: QueryOptions = {}): string {
const query = filtersObjectToArray(filters, options).reduce( const { isCohort, cohortMatch, cohortActionName } = options;
(arr, { name, column, operator, prefix = '', paramName }) => { const isOr = isCohort ? cohortMatch === 'any' : filters.match === 'any';
const isCohort = options?.isCohort; const orClauses: string[] = [];
const andClauses: string[] = [];
filtersObjectToArray(filters, options).forEach(
({ name, column, operator, prefix = '', paramName }) => {
if (isCohort) { if (isCohort) {
column = FILTER_COLUMNS[name.slice('cohort_'.length)]; column = FILTER_COLUMNS[name.slice('cohort_'.length)];
} }
if (column) { 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') { 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)`, `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 = {}) { function getCohortQuery(filters: QueryFilters = {}) {
@ -137,7 +154,10 @@ function getCohortQuery(filters: QueryFilters = {}) {
return ''; 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 return `join
(select distinct website_event.session_id (select distinct website_event.session_id

View file

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

View file

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

View file

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