Merge branch 'dev' of https://github.com/umami-software/umami into analytics

This commit is contained in:
Francis Cao
2026-05-07 13:31:01 -07:00
75 changed files with 450 additions and 172 deletions
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "مُجمّعة",
"filter-raw": "خام",
"filters": "التصفيات",
"filters-enabled": "الفلاتر مفعّلة",
"first-click": "النقرة الأولى",
"first-seen": "أول ظهور",
"funnel": "قمع",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Камбініраваны",
"filter-raw": "Сырыя",
"filters": "Фільтры",
"filters-enabled": "Фільтры ўключаны",
"first-click": "Першы клік",
"first-seen": "Першы раз убачана",
"funnel": "Варонка",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Комбиниран",
"filter-raw": "Суров",
"filters": "Филтри",
"filters-enabled": "Филтрите са активирани",
"first-click": "Първо кликване",
"first-seen": "Първо видяно",
"funnel": "Фуния",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "সম্মিলিত",
"filter-raw": "অপরিশোধিত",
"filters": "ফিল্টারসমূহ",
"filters-enabled": "ফিল্টার সক্রিয়",
"first-click": "প্রথম ক্লিক",
"first-seen": "প্রথম দেখা",
"funnel": "ফানেল",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinovano",
"filter-raw": "Sirovo",
"filters": "Filtri",
"filters-enabled": "Filteri omogućeni",
"first-click": "Prvi klik",
"first-seen": "Prvi put viđeno",
"funnel": "Lijevak",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combinat",
"filter-raw": "En cru",
"filters": "Filtres",
"filters-enabled": "Filtres activats",
"first-click": "Primer clic",
"first-seen": "Vist per primer cop",
"funnel": "Embut",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinace",
"filter-raw": "Nezpracované",
"filters": "Filtry",
"filters-enabled": "Filtry povoleny",
"first-click": "První kliknutí",
"first-seen": "Poprvé viděno",
"funnel": "Trychtýř",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombineret",
"filter-raw": "Rå",
"filters": "Filtre",
"filters-enabled": "Filtre aktiveret",
"first-click": "Første klik",
"first-seen": "Først set",
"funnel": "Tragt",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombiniert",
"filter-raw": "Rohdate",
"filters": "Filter",
"filters-enabled": "Filter aktiviert",
"first-click": "Erste Klick",
"first-seen": "Erstmal gse",
"funnel": "Tunnel",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombiniert",
"filter-raw": "Rohdaten",
"filters": "Filter",
"filters-enabled": "Filter aktiviert",
"first-click": "Erster Klick",
"first-seen": "Erstmalig gesehen",
"funnel": "Trichter",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Σε συνδυασμό",
"filter-raw": "Ακατέργαστο",
"filters": "Φίλτρα",
"filters-enabled": "Φίλτρα ενεργοποιημένα",
"first-click": "Πρώτο κλικ",
"first-seen": "Πρώτη εμφάνιση",
"funnel": "Χωνί",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combined",
"filter-raw": "Raw",
"filters": "Filters",
"filters-enabled": "Filters enabled",
"first-click": "First click",
"first-seen": "First seen",
"funnel": "Funnel",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combined",
"filter-raw": "Raw",
"filters": "Filters",
"filters-enabled": "Filters enabled",
"property-filter": "Property Filter",
"activity-by-property": "Activity by property",
"first-click": "First click",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combinado",
"filter-raw": "En crudo",
"filters": "Filtros",
"filters-enabled": "Filtros activados",
"first-click": "Primer clic",
"first-seen": "Primera vez visto",
"funnel": "Embudo",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "ترکیب شده",
"filter-raw": "خام",
"filters": "فیلترها",
"filters-enabled": "فیلترها فعال",
"first-click": "اولین کلیک",
"first-seen": "اولین بار دیده شده",
"funnel": "فانل",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Yhdistetty",
"filter-raw": "Käsittelemätön",
"filters": "Suodattimet",
"filters-enabled": "Suodattimet käytössä",
"first-click": "Ensimmäinen klikkaus",
"first-seen": "Ensimmäinen havainto",
"funnel": "Suppilo",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Samansett",
"filter-raw": "Óviðgjørt",
"filters": "Síur",
"filters-enabled": "Síur virknaðar",
"first-click": "Fyrsta trýst",
"first-seen": "Fyrst sæddur",
"funnel": "Traktari",
+1
View File
@@ -126,6 +126,7 @@
"filter-combined": "Combiné",
"filter-raw": "Brut",
"filters": "Filtres",
"filters-enabled": "Filtres activés",
"first-click": "Premier clic",
"first-seen": "Vu pour la première fois",
"funnel": "Entonnoir",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combinado",
"filter-raw": "Crú",
"filters": "Filtros",
"filters-enabled": "Filtros activados",
"first-click": "Primeiro clic",
"first-seen": "Primeira visita",
"funnel": "Funil",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "משותף",
"filter-raw": "גולמי",
"filters": "מסננים",
"filters-enabled": "מסננים מופעלים",
"first-click": "קליק ראשון",
"first-seen": "נראה לראשונה",
"funnel": "משפך",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "संयुक्त",
"filter-raw": "रॉ",
"filters": "फ़िल्टर",
"filters-enabled": "फ़िल्टर सक्षम",
"first-click": "पहला क्लिक",
"first-seen": "पहली बार देखा गया",
"funnel": "फनल",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinirano",
"filter-raw": "Neobrađeno",
"filters": "Filteri",
"filters-enabled": "Filtri omogućeni",
"first-click": "Prvi klik",
"first-seen": "Prvi put viđeno",
"funnel": "Lijevak",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Összevont",
"filter-raw": "Nyers",
"filters": "Szűrők",
"filters-enabled": "Szűrők engedélyezve",
"first-click": "Első kattintás",
"first-seen": "Első megtekintés",
"funnel": "Tölcsér",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Gabungan",
"filter-raw": "Mentah",
"filters": "Filter",
"filters-enabled": "Filter diaktifkan",
"first-click": "Klik pertama",
"first-seen": "Pertama kali dilihat",
"funnel": "Corong",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Aggregati",
"filter-raw": "Grezzo",
"filters": "Filtri",
"filters-enabled": "Filtri abilitati",
"first-click": "Primo clic",
"first-seen": "Prima visualizzazione",
"funnel": "Funnel",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "結合",
"filter-raw": "RAW",
"filters": "フィルター",
"filters-enabled": "フィルター有効",
"first-click": "最初のクリック",
"first-seen": "初回ログイン",
"funnel": "ファネル",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "រួមបញ្ចូលគ្នា",
"filter-raw": "ដើម",
"filters": "ចម្រោះ",
"filters-enabled": "ការច្រោះបានបើក",
"first-click": "ចុចដំបូង",
"first-seen": "ឃើញដំបូង",
"funnel": "ផ្លូវបង្ហាញ",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "합쳐 보기",
"filter-raw": "전체 보기",
"filters": "필터",
"filters-enabled": "필터 활성화됨",
"first-click": "첫 클릭",
"first-seen": "첫 접속",
"funnel": "퍼널",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinuoti",
"filter-raw": "Neapdoroti",
"filters": "Filtrai",
"filters-enabled": "Filtrai įjungti",
"first-click": "Pirmas paspaudimas",
"first-seen": "Pirmą kartą matyta",
"funnel": "Piltuvas",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Нэгтгэсэн",
"filter-raw": "Түүхий",
"filters": "Шүүлтүүр",
"filters-enabled": "Шүүлтүүр идэвхжүүлэгдсэн",
"first-click": "Эхний даралт",
"first-seen": "Анх харсан",
"funnel": "Цутгал",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Digabungkan",
"filter-raw": "Mentah",
"filters": "Tapis",
"filters-enabled": "Penapis diaktifkan",
"first-click": "Klik pertama",
"first-seen": "Pertama dilihat",
"funnel": "Corong",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "ပေါင်းစပ်ပြီး",
"filter-raw": "အရှိအတိုင်း",
"filters": "Filter များ",
"filters-enabled": "စစ်ထုတ်မှုများ ဖွင့်ထားသည်",
"first-click": "ပထမဆုံးနှိပ်ချက်",
"first-seen": "ပထမဆုံးတွေ့ရှိချိန်",
"funnel": "ဖန်နယ်",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinert",
"filter-raw": "Rå",
"filters": "Filter",
"filters-enabled": "Filtre aktivert",
"first-click": "Første klikk",
"first-seen": "Først sett",
"funnel": "Trakt",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Gecombineerd",
"filter-raw": "Ruw",
"filters": "Filters",
"filters-enabled": "Filters ingeschakeld",
"first-click": "Eerste klik",
"first-seen": "Eerst gezien",
"funnel": "Trechter",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Połączone",
"filter-raw": "Surowe dane",
"filters": "Filtry",
"filters-enabled": "Filtry włączone",
"first-click": "Pierwsze kliknięcie",
"first-seen": "Pierwsza wizyta",
"funnel": "Lejek",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combinado",
"filter-raw": "Bruto",
"filters": "Filtros",
"filters-enabled": "Filtros ativados",
"first-click": "Primeiro clique",
"first-seen": "Visto pela primeira vez",
"funnel": "Funil",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combinado",
"filter-raw": "Dados brutos",
"filters": "Filtros",
"filters-enabled": "Filtros ativados",
"first-click": "Primeiro clique",
"first-seen": "Primeira visualização",
"funnel": "Funil",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Combinat",
"filter-raw": "Brut",
"filters": "Filtre",
"filters-enabled": "Filtre activate",
"first-click": "Primul click",
"first-seen": "Văzut pentru prima dată",
"funnel": "Parcursul utilizatorului",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Объединенные",
"filter-raw": "Сырые данные",
"filters": "Фильтры",
"filters-enabled": "Фильтры включены",
"first-click": "Первый клик",
"first-seen": "Первый вход",
"funnel": "Воронка",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "ඒකාබද්ධ",
"filter-raw": "අමු",
"filters": "පෙරහන්",
"filters-enabled": "පෙරහන් සක්‍රීය කර ඇත",
"first-click": "පළමු ක්ලික්",
"first-seen": "මුලින් දුටු",
"funnel": "පුනීලය",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinácie",
"filter-raw": "Nezpracované",
"filters": "Filtre",
"filters-enabled": "Filtre povolené",
"first-click": "Prvé kliknutie",
"first-seen": "Prvýkrát videné",
"funnel": "Lievik",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Skupaj",
"filter-raw": "Neobdelano",
"filters": "Filtri",
"filters-enabled": "Filtri omogočeni",
"first-click": "Prvi klik",
"first-seen": "Prvič viden",
"funnel": "Prodajni lijak",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Kombinerade",
"filter-raw": "Rådata",
"filters": "Filter",
"filters-enabled": "Filter aktiverade",
"first-click": "Första klicket",
"first-seen": "Först sedd",
"funnel": "Tratt",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "ஒருங்கிணைந்த",
"filter-raw": "மூல",
"filters": "வடிகட்டிகள்",
"filters-enabled": "வடிகட்டிகள் இயக்கப்பட்டன",
"first-click": "முதல் கிளிக்",
"first-seen": "முதலில் பார்த்தது",
"funnel": "புனல்",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "ข้อมูลรวม",
"filter-raw": "ข้อมูลดิบ",
"filters": "ตัวกรอง",
"filters-enabled": "เปิดใช้งานตัวกรอง",
"first-click": "คลิกแรก",
"first-seen": "เห็นครั้งแรก",
"funnel": "ช่องทางขาย",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Birleşik filtre",
"filter-raw": "Ham filtre",
"filters": "Filtreler",
"filters-enabled": "Filtreler etkin",
"first-click": "İlk tıklama",
"first-seen": "İlk görülme",
"funnel": "Huni",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Об'єднані",
"filter-raw": "Сирі дані",
"filters": "Фільтри",
"filters-enabled": "Фільтри увімкнено",
"first-click": "Перший клік",
"first-seen": "Перший візит",
"funnel": "Воронка",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "مشترکہ",
"filter-raw": "خام",
"filters": "فلٹرز",
"filters-enabled": "فلٹرز فعال",
"first-click": "پہلا کلک",
"first-seen": "پہلی بار دیکھا گیا",
"funnel": "فنل",
+1
View File
@@ -125,6 +125,7 @@
"filter-combined": "Birlashtirilgan",
"filter-raw": "Xom",
"filters": "Filtrlar",
"filters-enabled": "Filtrlar yoqilgan",
"first-click": "Birinchi bosish",
"first-seen": "Birinchi koʻrilgan",
"funnel": "Voronka",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "Kết hợp lọc",
"filter-raw": "Lọc thô",
"filters": "Bộ lọc",
"filters-enabled": "Bộ lọc đã bật",
"first-click": "Nhấp đầu tiên",
"first-seen": "Lần đầu tiên nhìn thấy",
"funnel": "Phễu",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "合并",
"filter-raw": "原始",
"filters": "筛选",
"filters-enabled": "过滤器已启用",
"first-click": "首次点击",
"first-seen": "首次出现",
"funnel": "分析",
+1
View File
@@ -124,6 +124,7 @@
"filter-combined": "組合",
"filter-raw": "原始",
"filters": "篩選條件",
"filters-enabled": "篩選器已啟用",
"first-click": "首次點擊",
"first-seen": "首次造訪",
"funnel": "漏斗分析",
@@ -1,6 +1,16 @@
import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
import {
Button,
Checkbox,
Column,
Form,
FormField,
FormSubmitButton,
Row,
TextField,
} from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function BoardShareCreateForm({
boardId,
@@ -15,15 +25,19 @@ export function BoardShareCreateForm({
const { touch } = useModified();
const { t, labels, getErrorMessage } = useMessages();
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>(null);
const [error, setError] = useState<any>(null);
const handleSubmit = async (data: { name: string }) => {
const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => {
setIsPending(true);
setError(null);
try {
await post(`/boards/${boardId}/shares`, {
name: data.name,
parameters: {
allowFilter: data.allowFilter ?? true,
theme: data.theme === 'system' ? undefined : data.theme,
},
});
touch('shares');
@@ -36,22 +50,38 @@ export function BoardShareCreateForm({
};
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ name: '' }}>
<Column gap="4">
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row justifyContent="flex-end" gap="3">
{onCancel && (
<Button isDisabled={isPending} onPress={onCancel}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.add)}
</FormSubmitButton>
</Row>
</Column>
<Form
onSubmit={handleSubmit}
error={getErrorMessage(error)}
defaultValues={{ name: '', allowFilter: true, theme: 'system' }}
>
{({ watch, setValue }) => (
<Column gap="4">
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<FormField name="allowFilter">
<Checkbox>{t(labels.filters)}</Checkbox>
</FormField>
<FormField label={t(labels.theme)} name="theme">
<ThemeModeSelector
value={watch('theme')}
includeSystem
onChange={value => setValue('theme', value, { shouldDirty: true })}
/>
</FormField>
<Row justifyContent="flex-end" gap="3">
{onCancel && (
<Button isDisabled={isPending} onPress={onCancel}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.add)}
</FormSubmitButton>
</Row>
</Column>
)}
</Form>
);
}
@@ -3,6 +3,7 @@ import { CopyButton } from '@/components/common/CopyButton';
import { ExternalLink } from '@/components/common/ExternalLink';
import { useConfig, useMessages, useMobile } from '@/components/hooks';
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
import { SimpleShareEditButton } from '@/components/share/SimpleShareEditButton';
export function BoardSharesTable(props: DataTableProps) {
const { t, labels } = useMessages();
@@ -36,9 +37,10 @@ export function BoardSharesTable(props: DataTableProps) {
);
}}
</DataColumn>
<DataColumn id="action" align="end" width="60px">
<DataColumn id="action" align="end" width="100px">
{({ id, slug }: any) => (
<Row>
<SimpleShareEditButton shareId={id} />
<ShareDeleteButton shareId={id} slug={slug} />
</Row>
)}
@@ -1,9 +1,11 @@
import { Column, Row } from '@umami/react-zen';
import { useShare } from '@/components/hooks';
import { ExportButton } from '@/components/input/ExportButton';
import { FilterBar } from '@/components/input/FilterBar';
import { MonthFilter } from '@/components/input/MonthFilter';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
import { allowShareFilter } from '@/lib/share';
export function LinkControls({
linkId: websiteId,
@@ -18,15 +20,18 @@ export function LinkControls({
allowMonthFilter?: boolean;
allowDownload?: boolean;
}) {
const share = useShare();
const showFilter = allowFilter && allowShareFilter(share?.parameters);
return (
<Column gap>
<Row alignItems="center" justifyContent="space-between" gap="3">
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{showFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
{allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <MonthFilter />}
</Row>
{allowFilter && <FilterBar websiteId={websiteId} />}
{showFilter && <FilterBar websiteId={websiteId} />}
</Column>
);
}
@@ -1,9 +1,11 @@
import { Column, Row } from '@umami/react-zen';
import { useShare } from '@/components/hooks';
import { ExportButton } from '@/components/input/ExportButton';
import { FilterBar } from '@/components/input/FilterBar';
import { MonthFilter } from '@/components/input/MonthFilter';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
import { allowShareFilter } from '@/lib/share';
export function PixelControls({
pixelId: websiteId,
@@ -18,15 +20,18 @@ export function PixelControls({
allowMonthFilter?: boolean;
allowDownload?: boolean;
}) {
const share = useShare();
const showFilter = allowFilter && allowShareFilter(share?.parameters);
return (
<Column gap>
<Row alignItems="center" justifyContent="space-between" gap="3">
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{showFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} showAllTime={false} />}
{allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <MonthFilter />}
</Row>
{allowFilter && <FilterBar websiteId={websiteId} />}
{showFilter && <FilterBar websiteId={websiteId} />}
</Column>
);
}
@@ -1,21 +1,8 @@
import { Button, Icon, Row, useTheme } from '@umami/react-zen';
import { Moon, Sun } from '@/components/icons';
import { useTheme } from '@umami/react-zen';
import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function ThemeSetting() {
const { theme, setTheme } = useTheme();
return (
<Row gap>
<Button variant={theme === 'light' ? 'primary' : undefined} onPress={() => setTheme('light')}>
<Icon>
<Sun />
</Icon>
</Button>
<Button variant={theme === 'dark' ? 'primary' : undefined} onPress={() => setTheme('dark')}>
<Icon>
<Moon />
</Icon>
</Button>
</Row>
);
return <ThemeModeSelector value={theme} onChange={value => setTheme(value as 'light' | 'dark')} />;
}
@@ -1,5 +1,6 @@
import { DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen';
import type { ReactNode } from 'react';
import { DateDistance } from '@/components/common/DateDistance';
import { LinkButton } from '@/components/common/LinkButton';
import { SortableLabel } from '@/components/common/SortableLabel';
import { useMessages, useNavigation } from '@/components/hooks';
@@ -22,6 +23,13 @@ export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTab
{renderLink}
</DataColumn>
<DataColumn id="domain" label={<SortableLabel label={t(labels.domain)} sortKey="domain" />} />
<DataColumn
id="created"
label={<SortableLabel label={t(labels.created)} sortKey="createdAt" defaultDirection="desc" />}
width="200px"
>
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
</DataColumn>
{showActions && (
<DataColumn id="action" label=" " align="end">
{(row: any) => {
@@ -1,9 +1,11 @@
import { Column, Grid, Row } from '@umami/react-zen';
import { useShare } from '@/components/hooks';
import { ExportButton } from '@/components/input/ExportButton';
import { FilterBar } from '@/components/input/FilterBar';
import { MonthFilter } from '@/components/input/MonthFilter';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { WebsiteFilterButton } from '@/components/input/WebsiteFilterButton';
import { allowShareFilter } from '@/lib/share';
export function WebsiteControls({
websiteId,
@@ -22,11 +24,16 @@ export function WebsiteControls({
allowDownload?: boolean;
allowCompare?: boolean;
}) {
const share = useShare();
const showFilter = allowFilter && allowShareFilter(share?.parameters);
return (
<Column gap>
<Grid columns={{ base: '1fr', md: 'auto 1fr' }} gap>
<Row alignItems="center" justifyContent="flex-start" gap="4">
{allowFilter && <WebsiteFilterButton websiteId={websiteId} allowBounceFilter={allowBounceFilter} />}
{showFilter && (
<WebsiteFilterButton websiteId={websiteId} allowBounceFilter={allowBounceFilter} />
)}
</Row>
<Row alignItems="center" justifyContent={{ base: 'flex-start', md: 'flex-end' }}>
{allowDateFilter && (
@@ -36,7 +43,7 @@ export function WebsiteControls({
{allowMonthFilter && <MonthFilter />}
</Row>
</Grid>
{allowFilter && <FilterBar websiteId={websiteId} />}
{showFilter && <FilterBar websiteId={websiteId} />}
</Column>
);
}
@@ -7,13 +7,16 @@ import {
FormSubmitButton,
Grid,
Label,
ListSeparator,
Loading,
Row,
Switch,
Text,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
import { SHARE_NAV_ITEMS } from './constants';
export function ShareEditForm({
@@ -59,12 +62,14 @@ export function ShareEditForm({
}, [shareId, modified]);
const handleSubmit = async (data: any) => {
const parameters: Record<string, boolean> = {};
const parameters: Record<string, boolean | string | undefined> = {};
SHARE_NAV_ITEMS.forEach(section => {
section.items.forEach(item => {
parameters[item.id] = data[item.id] ?? false;
});
});
parameters.allowFilter = data.allowFilter ?? true;
parameters.theme = data.theme === 'system' ? undefined : data.theme;
setIsPending(true);
setError(null);
@@ -101,6 +106,8 @@ export function ShareEditForm({
// Build default values from share parameters
const defaultValues: Record<string, any> = {
name: share?.name || '',
allowFilter: share?.parameters?.allowFilter ?? true,
theme: share?.parameters?.theme || 'system',
};
SHARE_NAV_ITEMS.forEach(section => {
section.items.forEach(item => {
@@ -114,7 +121,7 @@ export function ShareEditForm({
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
{({ watch }) => {
{({ watch, setValue }) => {
const values = watch();
const hasSelection = allItemIds.some(id => values[id]);
@@ -129,7 +136,25 @@ export function ShareEditForm({
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus={!isEditing} />
</FormField>
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
<Grid columns={{ base: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }} gap="3">
<FormField label={t(labels.filters)} name="allowFilter">
<Switch
isSelected={watch('allowFilter')}
onChange={value => setValue('allowFilter', value, { shouldDirty: true })}
>
{t(labels.filtersEnabled)}
</Switch>
</FormField>
<FormField label={t(labels.theme)} name="theme">
<ThemeModeSelector
value={watch('theme')}
includeSystem
onChange={value => setValue('theme', value, { shouldDirty: true })}
/>
</FormField>
</Grid>
<ListSeparator />
<Grid columns={{ base: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }} gap="3">
{SHARE_NAV_ITEMS.map(section => (
<Column key={section.section} gap="3">
<Text weight="bold">{t((labels as any)[section.section])}</Text>
+5 -3
View File
@@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto';
import { getRandomChars } from '@/lib/generate';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, pagingParams } from '@/lib/schema';
import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
import { canUpdateBoard, canViewBoard } from '@/permissions';
import { createShare, getSharesByEntityId } from '@/queries/prisma';
@@ -45,6 +45,7 @@ export async function POST(
) {
const schema = z.object({
name: z.string().max(200),
parameters: anyObjectParam.optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -54,7 +55,8 @@ export async function POST(
}
const { boardId } = await params;
const { name } = body;
const { name, parameters } = body;
const shareParameters = parameters ?? {};
if (!(await canUpdateBoard(auth, boardId))) {
return unauthorized();
@@ -66,7 +68,7 @@ export async function POST(
shareType: ENTITY_TYPE.board,
name,
slug: getRandomChars(16),
parameters: {},
parameters: shareParameters,
});
return json(share);
+5 -3
View File
@@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto';
import { getRandomChars } from '@/lib/generate';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, pagingParams } from '@/lib/schema';
import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
import { canUpdateLink, canViewLink } from '@/permissions';
import { createShare, getSharesByEntityId } from '@/queries/prisma';
@@ -45,6 +45,7 @@ export async function POST(
) {
const schema = z.object({
name: z.string().max(200),
parameters: anyObjectParam.optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -54,7 +55,8 @@ export async function POST(
}
const { linkId } = await params;
const { name } = body;
const { name, parameters } = body;
const shareParameters = parameters ?? {};
if (!(await canUpdateLink(auth, linkId))) {
return unauthorized();
@@ -66,7 +68,7 @@ export async function POST(
shareType: ENTITY_TYPE.link,
name,
slug: getRandomChars(16),
parameters: {},
parameters: shareParameters,
});
return json(share);
+5 -3
View File
@@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto';
import { getRandomChars } from '@/lib/generate';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, pagingParams } from '@/lib/schema';
import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
import { canUpdatePixel, canViewPixel } from '@/permissions';
import { createShare, getSharesByEntityId } from '@/queries/prisma';
@@ -45,6 +45,7 @@ export async function POST(
) {
const schema = z.object({
name: z.string().max(200),
parameters: anyObjectParam.optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
@@ -54,7 +55,8 @@ export async function POST(
}
const { pixelId } = await params;
const { name } = body;
const { name, parameters } = body;
const shareParameters = parameters ?? {};
if (!(await canUpdatePixel(auth, pixelId))) {
return unauthorized();
@@ -66,7 +68,7 @@ export async function POST(
shareType: ENTITY_TYPE.pixel,
name,
slug: getRandomChars(16),
parameters: {},
parameters: shareParameters,
});
return json(share);
+2 -2
View File
@@ -1,7 +1,7 @@
'use client';
import { useShareTokenQuery } from '@/components/hooks';
import { ENTITY_TYPE } from '@/lib/constants';
import type { WhiteLabel } from '@/lib/types';
import type { ShareParameters, WhiteLabel } from '@/lib/types';
import { Loading } from '@umami/react-zen';
import { usePathname, useRouter } from 'next/navigation';
import { createContext, type ReactNode, useEffect } from 'react';
@@ -15,7 +15,7 @@ export interface ShareData {
boardId?: string;
pixelId?: string;
linkId?: string;
parameters: any;
parameters: ShareParameters;
token: string;
whiteLabel?: WhiteLabel;
}
+11 -3
View File
@@ -3,6 +3,7 @@ import { useMessages, useNavigation, useShare } from '@/components/hooks';
import { AlignEndHorizontal, Clock, Eye, PanelLeft, Sheet, Tag, User } from '@/components/icons';
import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { allowShareFilter, excludeShareFilterParam, getShareTheme } from '@/lib/share';
import { Funnel, Gauge, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
import { buildPath } from '@/lib/url';
import {
@@ -32,12 +33,19 @@ export function ShareNav({
const { t, labels } = useMessages();
const { pathname, query } = useNavigation();
const { slug, parameters } = share;
const allowFilter = allowShareFilter(parameters);
const shareTheme = getShareTheme(parameters);
const renderPath = (path: string) =>
buildPath(`/share/${slug}${path}`, {
...query,
...Object.fromEntries(
Object.entries(query).filter(([key]) => {
return allowFilter || !excludeShareFilterParam(key);
}),
),
event: undefined,
compare: undefined,
theme: undefined,
view: undefined,
unit: undefined,
excludeBounce: undefined,
@@ -202,13 +210,13 @@ export function ShareNav({
>
{collapsed ? (
<Column gap="2" alignItems="center">
<ThemeButton />
{!shareTheme && <ThemeButton />}
<LanguageButton />
<PreferencesButton />
</Column>
) : (
<Row>
<ThemeButton />
{!shareTheme && <ThemeButton />}
<LanguageButton />
<PreferencesButton />
</Row>
@@ -22,6 +22,7 @@ import { PageBody } from '@/components/common/PageBody';
import { useShare } from '@/components/hooks';
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
import { ENTITY_TYPE } from '@/lib/constants';
import { getShareTheme } from '@/lib/share';
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
@@ -68,20 +69,20 @@ export function SharePage() {
setNavCollapsed(value);
};
const share = useShare();
const { setTheme } = useTheme();
const { initTheme } = useTheme();
const router = useRouter();
const pathname = usePathname();
const path = getSharePath(pathname);
const { slug, websiteId, boardId, pixelId, linkId, parameters = {}, shareType } = share;
const shareTheme = getShareTheme(parameters);
useEffect(() => {
const url = new URL(window?.location?.href);
const theme = url.searchParams.get('theme');
initTheme(shareTheme, 'system');
if (theme === 'light' || theme === 'dark') {
setTheme(theme);
}
}, [setTheme]);
return () => {
initTheme(undefined, 'system');
};
}, [shareTheme, initTheme]);
// Check if the requested path is allowed
const pageKey = path || '';
+36 -36
View File
@@ -62,47 +62,47 @@ export function DataGrid({
const child = data ? (typeof children === 'function' ? children(data) : children) : null;
return (
<Column gap="4" minHeight="300px">
{allowSearch && (
<Row alignItems="center" justifyContent="space-between" wrap="wrap" gap>
<SearchField
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus}
placeholder={t(labels.search)}
/>
{renderActions?.()}
</Row>
)}
<LoadingPanel
data={data?.data}
isLoading={isLoading}
isFetching={isFetching}
error={error}
renderEmpty={renderEmpty}
>
{data && (
<>
<Column gap="4" minHeight="300px" justifyContent="space-between">
<Column gap="4">
{allowSearch && (
<Row alignItems="center" justifyContent="space-between" wrap="wrap" gap>
<SearchField
value={search}
onSearch={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={autoFocus}
placeholder={t(labels.search)}
/>
{renderActions?.()}
</Row>
)}
<LoadingPanel
data={data?.data}
isLoading={isLoading}
isFetching={isFetching}
error={error}
renderEmpty={renderEmpty}
>
{data && (
<Column>
{isValidElement(child)
? cloneElement(child as ReactElement<any>, { displayMode })
: child}
</Column>
{showPager && (
<Row marginTop="6">
<Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
isCapped={data.isCapped}
onPageChange={handlePageChange}
/>
</Row>
)}
</>
)}
</LoadingPanel>
)}
</LoadingPanel>
</Column>
{showPager && (
<Row marginTop="4">
<Pager
page={data.page}
pageSize={data.pageSize}
count={data.count}
isCapped={data.isCapped}
onPageChange={handlePageChange}
/>
</Row>
)}
</Column>
);
}
+14 -9
View File
@@ -1,27 +1,32 @@
import { useMemo } from 'react';
import { FILTER_COLUMNS } from '@/lib/constants';
import { useShare } from './context/useShare';
import { useNavigation } from './useNavigation';
export function useFilterParameters({ includePagination = true }: { includePagination?: boolean } = {}) {
const { query } = useNavigation();
const share = useShare();
const allowFilter = share?.parameters?.allowFilter !== false;
return useMemo(() => {
const filterParams: Record<string, any> = {};
for (const key of Object.keys(query)) {
const baseName = key.replace(/\d+$/, '');
if (FILTER_COLUMNS[baseName]) {
filterParams[key] = query[key];
if (allowFilter) {
for (const key of Object.keys(query)) {
const baseName = key.replace(/\d+$/, '');
if (FILTER_COLUMNS[baseName]) {
filterParams[key] = query[key];
}
}
}
const params = {
...filterParams,
search: query.search,
segment: query.segment,
cohort: query.cohort,
excludeBounce: query.excludeBounce,
match: query.match,
segment: allowFilter ? query.segment : undefined,
cohort: allowFilter ? query.cohort : undefined,
excludeBounce: allowFilter ? query.excludeBounce : undefined,
match: allowFilter ? query.match : undefined,
} as Record<string, any>;
if (includePagination) {
@@ -30,5 +35,5 @@ export function useFilterParameters({ includePagination = true }: { includePagin
}
return params;
}, [query, includePagination]);
}, [allowFilter, includePagination, query]);
}
+26 -21
View File
@@ -1,5 +1,6 @@
import { FILTER_COLUMNS, OPERATORS } from '@/lib/constants';
import { safeDecodeURIComponent } from '@/lib/url';
import { useShare } from './context/useShare';
import { useFields } from './useFields';
import { useMessages } from './useMessages';
import { useNavigation } from './useNavigation';
@@ -10,6 +11,8 @@ export function useFilters() {
const { query } = useNavigation();
const { fields } = useFields();
const operatorLabels = useOperatorLabels();
const share = useShare();
const allowFilter = share?.parameters?.allowFilter !== false;
const operators = [
{ name: 'eq', type: 'string', label: t(labels.is) },
@@ -56,30 +59,32 @@ export function useFilters() {
uuid: [OPERATORS.equals],
};
const filters = Object.keys(query).reduce((arr, key) => {
const baseName = key.replace(/\d+$/, '');
if (FILTER_COLUMNS[baseName]) {
let operator = 'eq';
let value = safeDecodeURIComponent(query[key]);
const label = fields.find(({ name }) => name === baseName)?.label;
const filters = allowFilter
? Object.keys(query).reduce((arr, key) => {
const baseName = key.replace(/\d+$/, '');
if (FILTER_COLUMNS[baseName]) {
let operator = 'eq';
let value = safeDecodeURIComponent(query[key]);
const label = fields.find(({ name }) => name === baseName)?.label;
const match = value.match(/^([a-z]+)\.(.*)/);
const match = value.match(/^([a-z]+)\.(.*)/);
if (match) {
operator = match[1];
value = match[2];
}
if (match) {
operator = match[1];
value = match[2];
}
return arr.concat({
name: key,
type: baseName,
operator,
value,
label,
});
}
return arr;
}, []);
return arr.concat({
name: key,
type: baseName,
operator,
value,
label,
});
}
return arr;
}, [])
: [];
const getFilters = (type: string) => {
return (
@@ -0,0 +1,36 @@
import type { ReactNode } from 'react';
import { Button, Icon, Row } from '@umami/react-zen';
import { Monitor, Moon, Sun } from '@/components/icons';
export type ThemeMode = 'light' | 'dark' | 'system';
export function ThemeModeSelector({
value,
onChange,
includeSystem = false,
}: {
value?: ThemeMode;
onChange: (value: ThemeMode) => void;
includeSystem?: boolean;
}) {
const options: { id: ThemeMode; icon: ReactNode; label: string }[] = [
{ id: 'light', icon: <Sun />, label: 'Light' },
...(includeSystem ? [{ id: 'system' as const, icon: <Monitor />, label: 'System' }] : []),
{ id: 'dark', icon: <Moon />, label: 'Dark' },
];
return (
<Row gap>
{options.map(({ id, icon, label }) => (
<Button
key={id}
variant={value === id ? 'primary' : undefined}
onPress={() => onChange(id)}
aria-label={label}
>
<Icon>{icon}</Icon>
</Button>
))}
</Row>
);
}
+1
View File
@@ -190,6 +190,7 @@ export const labels: Record<string, string> = {
type: 'label.type',
filter: 'label.filter',
filters: 'label.filters',
filtersEnabled: 'label.filters-enabled',
propertyFilter: 'label.property-filter',
activityByProperty: 'label.activity-by-property',
breakdown: 'label.breakdown',
+56 -19
View File
@@ -1,6 +1,16 @@
import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
import {
Button,
Column,
Form,
FormField,
FormSubmitButton,
Row,
Switch,
TextField,
} from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function SimpleShareCreateForm({
createPath,
@@ -15,15 +25,19 @@ export function SimpleShareCreateForm({
const { touch } = useModified();
const { t, labels, getErrorMessage } = useMessages();
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>(null);
const [error, setError] = useState<any>(null);
const handleSubmit = async (data: { name: string }) => {
const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => {
setIsPending(true);
setError(null);
try {
await post(createPath, {
name: data.name,
parameters: {
allowFilter: data.allowFilter ?? true,
theme: data.theme === 'system' ? undefined : data.theme,
},
});
touch('shares');
@@ -36,22 +50,45 @@ export function SimpleShareCreateForm({
};
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ name: '' }}>
<Column gap="4">
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row justifyContent="flex-end" gap="3">
{onCancel && (
<Button isDisabled={isPending} onPress={onCancel}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.add)}
</FormSubmitButton>
</Row>
</Column>
<Form
onSubmit={handleSubmit}
error={getErrorMessage(error)}
defaultValues={{ name: '', allowFilter: true, theme: 'system' }}
>
{({ watch, setValue }) => (
<Column gap="4">
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row gap="6">
<FormField label={t(labels.filters)} name="allowFilter">
<Switch
isSelected={watch('allowFilter')}
onChange={value => setValue('allowFilter', value, { shouldDirty: true })}
>
{t(labels.filtersEnabled)}
</Switch>
</FormField>
<FormField label={t(labels.theme)} name="theme">
<ThemeModeSelector
value={watch('theme')}
includeSystem
onChange={value => setValue('theme', value, { shouldDirty: true })}
/>
</FormField>
</Row>
<Row justifyContent="flex-end" gap="3">
{onCancel && (
<Button isDisabled={isPending} onPress={onCancel}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.add)}
</FormSubmitButton>
</Row>
</Column>
)}
</Form>
);
}
+50 -21
View File
@@ -7,10 +7,12 @@ import {
Label,
Loading,
Row,
Switch,
TextField,
} from '@umami/react-zen';
import { useEffect, useState } from 'react';
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
import { ThemeModeSelector } from '@/components/input/ThemeModeSelector';
export function SimpleShareEditForm({
shareId,
@@ -49,7 +51,7 @@ export function SimpleShareEditForm({
loadShare();
}, [get, modified, shareId]);
const handleSubmit = async (data: { name: string }) => {
const handleSubmit = async (data: { name: string; allowFilter?: boolean; theme?: string }) => {
setIsPending(true);
setError(null);
@@ -57,7 +59,11 @@ export function SimpleShareEditForm({
await post(`/share/id/${shareId}`, {
name: data.name,
slug: share.slug,
parameters: share.parameters || {},
parameters: {
...(share.parameters || {}),
allowFilter: data.allowFilter ?? true,
theme: data.theme === 'system' ? undefined : data.theme,
},
});
touch('shares');
@@ -78,27 +84,50 @@ export function SimpleShareEditForm({
<Form
onSubmit={handleSubmit}
error={getErrorMessage(error)}
defaultValues={{ name: share?.name || '' }}
defaultValues={{
name: share?.name || '',
allowFilter: share?.parameters?.allowFilter ?? true,
theme: share?.parameters?.theme || 'system',
}}
>
<Column gap="6">
<Column>
<Label>{t(labels.shareUrl)}</Label>
<TextField value={getUrl(share?.slug || '')} isReadOnly allowCopy />
{({ watch, setValue }) => (
<Column gap="6">
<Column>
<Label>{t(labels.shareUrl)}</Label>
<TextField value={getUrl(share?.slug || '')} isReadOnly allowCopy />
</Column>
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row gap="6">
<FormField label={t(labels.filters)} name="allowFilter">
<Switch
isSelected={watch('allowFilter')}
onChange={value => setValue('allowFilter', value, { shouldDirty: true })}
>
{t(labels.filtersEnabled)}
</Switch>
</FormField>
<FormField label={t(labels.theme)} name="theme">
<ThemeModeSelector
value={watch('theme')}
includeSystem
onChange={value => setValue('theme', value, { shouldDirty: true })}
/>
</FormField>
</Row>
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.save)}
</FormSubmitButton>
</Row>
</Column>
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row justifyContent="flex-end" paddingTop="3" gap="3">
{onClose && (
<Button isDisabled={isPending} onPress={onClose}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.save)}
</FormSubmitButton>
</Row>
</Column>
)}
</Form>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { FILTER_COLUMNS } from './constants';
import type { ShareParameters, ShareTheme } from './types';
const FILTER_QUERY_PARAMS = new Set(['cohort', 'excludeBounce', 'match', 'segment']);
export function allowShareFilter(parameters?: ShareParameters | null) {
return parameters?.allowFilter !== false;
}
export function getShareTheme(parameters?: ShareParameters | null): ShareTheme | undefined {
return parameters?.theme === 'light' || parameters?.theme === 'dark'
? parameters.theme
: undefined;
}
export function excludeShareFilterParam(key: string) {
const baseName = key.replace(/\d+$/, '');
return baseName in FILTER_COLUMNS || FILTER_QUERY_PARAMS.has(key);
}
+9
View File
@@ -27,9 +27,18 @@ export interface Auth {
pixelIds?: string[];
linkId?: string;
linkIds?: string[];
parameters?: ShareParameters;
};
}
export type ShareTheme = 'light' | 'dark';
export interface ShareParameters {
allowFilter?: boolean;
theme?: ShareTheme;
[key: string]: boolean | ShareTheme | undefined;
}
export interface PropertyFilter {
propertyName: string;
dataType: number;