Merge branch 'dev' into fix/delete-cascade
This commit is contained in:
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "مُجمّعة",
|
||||
"filter-raw": "خام",
|
||||
"filters": "التصفيات",
|
||||
"filters-enabled": "الفلاتر مفعّلة",
|
||||
"first-click": "النقرة الأولى",
|
||||
"first-seen": "أول ظهور",
|
||||
"funnel": "قمع",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "Камбініраваны",
|
||||
"filter-raw": "Сырыя",
|
||||
"filters": "Фільтры",
|
||||
"filters-enabled": "Фільтры ўключаны",
|
||||
"first-click": "Першы клік",
|
||||
"first-seen": "Першы раз убачана",
|
||||
"funnel": "Варонка",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "Комбиниран",
|
||||
"filter-raw": "Суров",
|
||||
"filters": "Филтри",
|
||||
"filters-enabled": "Филтрите са активирани",
|
||||
"first-click": "Първо кликване",
|
||||
"first-seen": "Първо видяно",
|
||||
"funnel": "Фуния",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "সম্মিলিত",
|
||||
"filter-raw": "অপরিশোধিত",
|
||||
"filters": "ফিল্টারসমূহ",
|
||||
"filters-enabled": "ফিল্টার সক্রিয়",
|
||||
"first-click": "প্রথম ক্লিক",
|
||||
"first-seen": "প্রথম দেখা",
|
||||
"funnel": "ফানেল",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ýř",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "Σε συνδυασμό",
|
||||
"filter-raw": "Ακατέργαστο",
|
||||
"filters": "Φίλτρα",
|
||||
"filters-enabled": "Φίλτρα ενεργοποιημένα",
|
||||
"first-click": "Πρώτο κλικ",
|
||||
"first-seen": "Πρώτη εμφάνιση",
|
||||
"funnel": "Χωνί",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "ترکیب شده",
|
||||
"filter-raw": "خام",
|
||||
"filters": "فیلترها",
|
||||
"filters-enabled": "فیلترها فعال",
|
||||
"first-click": "اولین کلیک",
|
||||
"first-seen": "اولین بار دیده شده",
|
||||
"funnel": "فانل",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "משותף",
|
||||
"filter-raw": "גולמי",
|
||||
"filters": "מסננים",
|
||||
"filters-enabled": "מסננים מופעלים",
|
||||
"first-click": "קליק ראשון",
|
||||
"first-seen": "נראה לראשונה",
|
||||
"funnel": "משפך",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "संयुक्त",
|
||||
"filter-raw": "रॉ",
|
||||
"filters": "फ़िल्टर",
|
||||
"filters-enabled": "फ़िल्टर सक्षम",
|
||||
"first-click": "पहला क्लिक",
|
||||
"first-seen": "पहली बार देखा गया",
|
||||
"funnel": "फनल",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "結合",
|
||||
"filter-raw": "RAW",
|
||||
"filters": "フィルター",
|
||||
"filters-enabled": "フィルター有効",
|
||||
"first-click": "最初のクリック",
|
||||
"first-seen": "初回ログイン",
|
||||
"funnel": "ファネル",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "រួមបញ្ចូលគ្នា",
|
||||
"filter-raw": "ដើម",
|
||||
"filters": "ចម្រោះ",
|
||||
"filters-enabled": "ការច្រោះបានបើក",
|
||||
"first-click": "ចុចដំបូង",
|
||||
"first-seen": "ឃើញដំបូង",
|
||||
"funnel": "ផ្លូវបង្ហាញ",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "합쳐 보기",
|
||||
"filter-raw": "전체 보기",
|
||||
"filters": "필터",
|
||||
"filters-enabled": "필터 활성화됨",
|
||||
"first-click": "첫 클릭",
|
||||
"first-seen": "첫 접속",
|
||||
"funnel": "퍼널",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "Нэгтгэсэн",
|
||||
"filter-raw": "Түүхий",
|
||||
"filters": "Шүүлтүүр",
|
||||
"filters-enabled": "Шүүлтүүр идэвхжүүлэгдсэн",
|
||||
"first-click": "Эхний даралт",
|
||||
"first-seen": "Анх харсан",
|
||||
"funnel": "Цутгал",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "ပေါင်းစပ်ပြီး",
|
||||
"filter-raw": "အရှိအတိုင်း",
|
||||
"filters": "Filter များ",
|
||||
"filters-enabled": "စစ်ထုတ်မှုများ ဖွင့်ထားသည်",
|
||||
"first-click": "ပထမဆုံးနှိပ်ချက်",
|
||||
"first-seen": "ပထမဆုံးတွေ့ရှိချိန်",
|
||||
"funnel": "ဖန်နယ်",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "Объединенные",
|
||||
"filter-raw": "Сырые данные",
|
||||
"filters": "Фильтры",
|
||||
"filters-enabled": "Фильтры включены",
|
||||
"first-click": "Первый клик",
|
||||
"first-seen": "Первый вход",
|
||||
"funnel": "Воронка",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "ඒකාබද්ධ",
|
||||
"filter-raw": "අමු",
|
||||
"filters": "පෙරහන්",
|
||||
"filters-enabled": "පෙරහන් සක්රීය කර ඇත",
|
||||
"first-click": "පළමු ක්ලික්",
|
||||
"first-seen": "මුලින් දුටු",
|
||||
"funnel": "පුනීලය",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "ஒருங்கிணைந்த",
|
||||
"filter-raw": "மூல",
|
||||
"filters": "வடிகட்டிகள்",
|
||||
"filters-enabled": "வடிகட்டிகள் இயக்கப்பட்டன",
|
||||
"first-click": "முதல் கிளிக்",
|
||||
"first-seen": "முதலில் பார்த்தது",
|
||||
"funnel": "புனல்",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "ข้อมูลรวม",
|
||||
"filter-raw": "ข้อมูลดิบ",
|
||||
"filters": "ตัวกรอง",
|
||||
"filters-enabled": "เปิดใช้งานตัวกรอง",
|
||||
"first-click": "คลิกแรก",
|
||||
"first-seen": "เห็นครั้งแรก",
|
||||
"funnel": "ช่องทางขาย",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "Об'єднані",
|
||||
"filter-raw": "Сирі дані",
|
||||
"filters": "Фільтри",
|
||||
"filters-enabled": "Фільтри увімкнено",
|
||||
"first-click": "Перший клік",
|
||||
"first-seen": "Перший візит",
|
||||
"funnel": "Воронка",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "مشترکہ",
|
||||
"filter-raw": "خام",
|
||||
"filters": "فلٹرز",
|
||||
"filters-enabled": "فلٹرز فعال",
|
||||
"first-click": "پہلا کلک",
|
||||
"first-seen": "پہلی بار دیکھا گیا",
|
||||
"funnel": "فنل",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "合并",
|
||||
"filter-raw": "原始",
|
||||
"filters": "筛选",
|
||||
"filters-enabled": "过滤器已启用",
|
||||
"first-click": "首次点击",
|
||||
"first-seen": "首次出现",
|
||||
"funnel": "分析",
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"filter-combined": "組合",
|
||||
"filter-raw": "原始",
|
||||
"filters": "篩選條件",
|
||||
"filters-enabled": "篩選器已啟用",
|
||||
"first-click": "首次點擊",
|
||||
"first-seen": "首次造訪",
|
||||
"funnel": "漏斗分析",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataColumn, DataTable, Dialog, Icon, MenuItem, Modal, Row, Text } from
|
||||
import Link from '@/components/common/Link';
|
||||
import { useState } from 'react';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Edit, Trash } from '@/components/icons';
|
||||
import { MenuButton } from '@/components/input/MenuButton';
|
||||
@@ -21,7 +22,11 @@ export function AdminTeamsTable({
|
||||
return (
|
||||
<>
|
||||
<DataTable data={data} {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)} width="1fr">
|
||||
<DataColumn
|
||||
id="name"
|
||||
label={<SortableLabel label={t(labels.name)} sortKey="name" />}
|
||||
width="1fr"
|
||||
>
|
||||
{(row: any) => <Link href={`/admin/teams/${row.id}`}>{row.name}</Link>}
|
||||
</DataColumn>
|
||||
<DataColumn id="websites" label={t(labels.members)} width="140px">
|
||||
@@ -41,7 +46,11 @@ export function AdminTeamsTable({
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={t(labels.created)} width="160px">
|
||||
<DataColumn
|
||||
id="created"
|
||||
label={<SortableLabel label={t(labels.created)} sortKey="createdAt" defaultDirection="desc" />}
|
||||
width="160px"
|
||||
>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
{showActions && (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataColumn, DataTable, Icon, MenuItem, Modal, Row, Text } from '@umami/
|
||||
import Link from '@/components/common/Link';
|
||||
import { useState } from 'react';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Edit, Trash } from '@/components/icons';
|
||||
import { MenuButton } from '@/components/input/MenuButton';
|
||||
@@ -22,10 +23,14 @@ export function UsersTable({
|
||||
return (
|
||||
<>
|
||||
<DataTable data={data} {...props}>
|
||||
<DataColumn id="username" label={t(labels.username)} width="2fr">
|
||||
<DataColumn
|
||||
id="username"
|
||||
label={<SortableLabel label={t(labels.username)} sortKey="username" />}
|
||||
width="2fr"
|
||||
>
|
||||
{(row: any) => <Link href={`/admin/users/${row.id}`}>{row.username}</Link>}
|
||||
</DataColumn>
|
||||
<DataColumn id="role" label={t(labels.role)}>
|
||||
<DataColumn id="role" label={<SortableLabel label={t(labels.role)} sortKey="role" />}>
|
||||
{(row: any) =>
|
||||
t(labels[Object.keys(ROLES).find(key => ROLES[key] === row.role)] || labels.unknown)
|
||||
}
|
||||
@@ -33,7 +38,10 @@ export function UsersTable({
|
||||
<DataColumn id="websites" label={t(labels.websites)}>
|
||||
{(row: any) => row._count.websites}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={t(labels.created)}>
|
||||
<DataColumn
|
||||
id="created"
|
||||
label={<SortableLabel label={t(labels.created)} sortKey="createdAt" defaultDirection="desc" />}
|
||||
>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
{showActions && (
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from '@/components/common/Link';
|
||||
import { useState } from 'react';
|
||||
import { WebsiteDeleteForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { Edit, Trash, Users } from '@/components/icons';
|
||||
import { MenuButton } from '@/components/input/MenuButton';
|
||||
@@ -14,14 +15,14 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) {
|
||||
return (
|
||||
<>
|
||||
<DataTable data={data} {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{(row: any) => (
|
||||
<Text truncate>
|
||||
<Link href={`/admin/websites/${row.id}`}>{row.name}</Link>
|
||||
</Text>
|
||||
)}
|
||||
</DataColumn>
|
||||
<DataColumn id="domain" label={t(labels.domain)}>
|
||||
<DataColumn id="domain" label={<SortableLabel label={t(labels.domain)} sortKey="domain" />}>
|
||||
{(row: any) => <Text truncate>{row.domain}</Text>}
|
||||
</DataColumn>
|
||||
<DataColumn id="owner" label={t(labels.owner)}>
|
||||
@@ -45,7 +46,11 @@ export function AdminWebsitesTable({ data = [], ...props }: { data: any[] }) {
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={t(labels.created)} width="180px">
|
||||
<DataColumn
|
||||
id="created"
|
||||
label={<SortableLabel label={t(labels.created)} sortKey="createdAt" defaultDirection="desc" />}
|
||||
width="180px"
|
||||
>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="50px">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
||||
import Link from '@/components/common/Link';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { BoardDeleteButton } from './BoardDeleteButton';
|
||||
import { BoardDesignButton } from './BoardDesignButton';
|
||||
@@ -12,16 +13,23 @@ export function BoardsTable(props: DataTableProps) {
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{({ id, name }: any) => {
|
||||
return <Link href={renderUrl(`/boards/${id}`)}>{name}</Link>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="description" label={t(labels.description)} />
|
||||
<DataColumn id="type" label={t(labels.boardType)}>
|
||||
<DataColumn
|
||||
id="description"
|
||||
label={<SortableLabel label={t(labels.description)} sortKey="description" />}
|
||||
/>
|
||||
<DataColumn id="type" label={<SortableLabel label={t(labels.boardType)} sortKey="type" />}>
|
||||
{({ type }: any) => type ? type.charAt(0).toUpperCase() + type.slice(1) : ''}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={t(labels.created)} width="200px">
|
||||
<DataColumn
|
||||
id="created"
|
||||
label={<SortableLabel label={t(labels.created)} sortKey="createdAt" defaultDirection="desc" />}
|
||||
width="200px"
|
||||
>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-ze
|
||||
import Link from '@/components/common/Link';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||
import { LinkDeleteButton } from './LinkDeleteButton';
|
||||
import { LinkEditButton } from './LinkEditButton';
|
||||
@@ -17,23 +18,27 @@ export function LinksTable({ showActions, ...props }: LinksTableProps) {
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{({ id, name }: any) => {
|
||||
return <Link href={renderUrl(`/links/${id}`)}>{name}</Link>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="slug" label={t(labels.link)}>
|
||||
<DataColumn id="slug" label={<SortableLabel label={t(labels.link)} sortKey="slug" />}>
|
||||
{({ slug }: any) => {
|
||||
const url = getSlugUrl(slug);
|
||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="url" label={t(labels.destinationUrl)}>
|
||||
<DataColumn id="url" label={<SortableLabel label={t(labels.destinationUrl)} sortKey="url" />}>
|
||||
{({ url }: any) => {
|
||||
return <ExternalLink href={url}>{url}</ExternalLink>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={t(labels.created)} width="200px">
|
||||
<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 && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-ze
|
||||
import Link from '@/components/common/Link';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||
import { PixelEditButton } from './PixelEditButton';
|
||||
@@ -17,12 +18,12 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{({ id, name }: any) => {
|
||||
return <Link href={renderUrl(`/pixels/${id}`)}>{name}</Link>;
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="url" label="URL">
|
||||
<DataColumn id="url" label={<SortableLabel label="URL" sortKey="slug" />}>
|
||||
{({ slug }: any) => {
|
||||
const url = getSlugUrl(slug);
|
||||
return (
|
||||
@@ -32,7 +33,10 @@ export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
<DataColumn id="created" label={t(labels.created)}>
|
||||
<DataColumn
|
||||
id="created"
|
||||
label={<SortableLabel label={t(labels.created)} sortKey="createdAt" defaultDirection="desc" />}
|
||||
>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
{showActions && (
|
||||
|
||||
@@ -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 } from '@umami/react-zen';
|
||||
import type { ReactNode } from 'react';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
@@ -12,7 +13,7 @@ export function TeamsTable({ renderLink, ...props }: TeamsTableProps) {
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{renderLink}
|
||||
</DataColumn>
|
||||
<DataColumn id="owner" label={t(labels.owner)}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataColumn, DataTable, Row } from '@umami/react-zen';
|
||||
import Link from '@/components/common/Link';
|
||||
import { TeamMemberEditButton } from '@/app/(main)/teams/[teamId]/TeamMemberEditButton';
|
||||
import { TeamMemberRemoveButton } from '@/app/(main)/teams/[teamId]/TeamMemberRemoveButton';
|
||||
import { SortableLabel } from '@/components/common/SortableLabel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
@@ -18,10 +19,10 @@ export function TeamWebsitesTable({
|
||||
|
||||
return (
|
||||
<DataTable data={data}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{(row: any) => <Link href={`/teams/${teamId}/websites/${row.id}`}>{row.name}</Link>}
|
||||
</DataColumn>
|
||||
<DataColumn id="domain" label={t(labels.domain)} />
|
||||
<DataColumn id="domain" label={<SortableLabel label={t(labels.domain)} sortKey="domain" />} />
|
||||
<DataColumn id="createdBy" label={t(labels.createdBy)}>
|
||||
{(row: any) => row?.createUser?.username}
|
||||
</DataColumn>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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';
|
||||
import { SquarePen } from '@/components/icons';
|
||||
|
||||
@@ -17,10 +19,17 @@ export function WebsitesTable({ showActions, renderLink, ...props }: WebsitesTab
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={t(labels.name)}>
|
||||
<DataColumn id="name" label={<SortableLabel label={t(labels.name)} sortKey="name" />}>
|
||||
{renderLink}
|
||||
</DataColumn>
|
||||
<DataColumn id="domain" label={t(labels.domain)} />
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewAllTeams } from '@/permissions';
|
||||
import { getTeams } from '@/queries/prisma/team';
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewUsers } from '@/permissions';
|
||||
import { getUsers } from '@/queries/prisma/user';
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewAllWebsites } from '@/permissions';
|
||||
import { getWebsites } from '@/queries/prisma/website';
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BOARD_TYPES, normalizeBoardType } from '@/lib/boards';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
|
||||
import { createBoard, getUserBoards } from '@/queries/prisma';
|
||||
|
||||
@@ -11,6 +11,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
|
||||
import { createLink, getUserLinks } from '@/queries/prisma';
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json } from '@/lib/response';
|
||||
import { pagingParams } from '@/lib/schema';
|
||||
import { pagingParams, sortingParams } from '@/lib/schema';
|
||||
import { getUserTeams } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json } from '@/lib/response';
|
||||
import { pagingParams } from '@/lib/schema';
|
||||
import { pagingParams, sortingParams } from '@/lib/schema';
|
||||
import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...sortingParams,
|
||||
includeTeams: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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,7 +2,7 @@ import { z } from 'zod';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
|
||||
import { createPixel, getUserPixels } from '@/queries/prisma';
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewTeam } from '@/permissions';
|
||||
import { getTeamBoards } from '@/queries/prisma';
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
const { teamId } = await params;
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewTeam } from '@/permissions';
|
||||
import { getTeamLinks } from '@/queries/prisma';
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
const { teamId } = await params;
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewTeam } from '@/permissions';
|
||||
import { getTeamPixels } from '@/queries/prisma';
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
const { teamId } = await params;
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canViewTeam } from '@/permissions';
|
||||
import { getTeamWebsites } from '@/queries/prisma';
|
||||
|
||||
@@ -9,6 +9,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
});
|
||||
const { teamId } = await params;
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -5,13 +5,14 @@ import { fetchAccount } from '@/lib/load';
|
||||
import redis from '@/lib/redis';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams } from '@/lib/schema';
|
||||
import { pagingParams, sortingParams } from '@/lib/schema';
|
||||
import { canCreateTeam } from '@/permissions';
|
||||
import { createTeam, getUserTeams } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams } from '@/lib/schema';
|
||||
import { pagingParams, sortingParams } from '@/lib/schema';
|
||||
import { getUserTeams } from '@/queries/prisma';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...sortingParams,
|
||||
});
|
||||
|
||||
const { auth, query, error } = await parseRequest(request, schema);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma/website';
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
includeTeams: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { uuid } from '@/lib/crypto';
|
||||
import { fetchAccount } from '@/lib/load';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
import { pagingParams, searchParams, sortingParams } from '@/lib/schema';
|
||||
import { canCreateTeamWebsite, canCreateWebsite } from '@/permissions';
|
||||
import { createShare, createWebsite, getWebsiteCount } from '@/queries/prisma';
|
||||
import { getAllUserWebsitesIncludingTeamAccess, getUserWebsites } from '@/queries/prisma/website';
|
||||
@@ -15,6 +15,7 @@ export async function GET(request: Request) {
|
||||
const schema = z.object({
|
||||
...pagingParams,
|
||||
...searchParams,
|
||||
...sortingParams,
|
||||
includeTeams: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 || '';
|
||||
|
||||
@@ -41,7 +41,7 @@ export function DataGrid({
|
||||
const { data, error, isLoading, isFetching } = query;
|
||||
const { router, updateParams, query: queryParams } = useNavigation();
|
||||
const [search, setSearch] = useState(queryParams?.search || data?.search || '');
|
||||
const showPager = allowPaging && data && (data.count > data.pageSize || data.isCapped);
|
||||
const showPager = allowPaging && data && data.count > 0;
|
||||
const { isMobile } = useMobile();
|
||||
const displayMode = isMobile ? 'cards' : undefined;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,18 @@ export interface PagerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerProps) {
|
||||
export function Pager({
|
||||
page,
|
||||
pageSize,
|
||||
count,
|
||||
isCapped,
|
||||
onPageChange,
|
||||
}: PagerProps) {
|
||||
const { t, labels } = useMessages();
|
||||
const maxPage = pageSize && count ? Math.ceil(+count / +pageSize) : 0;
|
||||
const lastPage = page === maxPage;
|
||||
const firstPage = page === 1;
|
||||
const showNavigation = maxPage > 1 || isCapped;
|
||||
|
||||
if (count === 0 || !maxPage) {
|
||||
return null;
|
||||
@@ -29,34 +36,40 @@ export function Pager({ page, pageSize, count, isCapped, onPageChange }: PagerPr
|
||||
}
|
||||
};
|
||||
|
||||
if (maxPage === 1 && !isCapped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayCount = isCapped ? `10,000+` : (+count).toLocaleString();
|
||||
|
||||
return (
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1}>
|
||||
<Text>{t(labels.numberOfRecords, { x: displayCount })}</Text>
|
||||
<Row alignItems="center" justifyContent="flex-end" gap="3">
|
||||
<Text>
|
||||
{t(labels.pageOf, {
|
||||
current: page.toLocaleString(),
|
||||
total: maxPage.toLocaleString(),
|
||||
})}
|
||||
</Text>
|
||||
<Row gap="1">
|
||||
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<ChevronRight />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
|
||||
<Icon size="sm">
|
||||
<ChevronRight />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1} wrap="wrap">
|
||||
<Text color="muted">{t(labels.numberOfRecords, { x: displayCount })}</Text>
|
||||
<Row
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
gap="3"
|
||||
wrap="nowrap"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{showNavigation && (
|
||||
<>
|
||||
<Text color="muted">
|
||||
{t(labels.pageOf, {
|
||||
current: page.toLocaleString(),
|
||||
total: maxPage.toLocaleString(),
|
||||
})}
|
||||
</Text>
|
||||
<Row gap="1">
|
||||
<Button variant="outline" onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<ChevronRight />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Button variant="outline" onPress={() => handlePageChange(1)} isDisabled={lastPage}>
|
||||
<Icon size="sm">
|
||||
<ChevronRight />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { Icon } from '@umami/react-zen';
|
||||
import { ChevronDown, ChevronUp } from '@/components/icons';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export interface SortableLabelProps {
|
||||
label: ReactNode;
|
||||
sortKey: string;
|
||||
defaultDirection?: SortDirection;
|
||||
}
|
||||
|
||||
export function SortableLabel({
|
||||
label,
|
||||
sortKey,
|
||||
defaultDirection = 'asc',
|
||||
}: SortableLabelProps) {
|
||||
const { router, query, updateParams } = useNavigation();
|
||||
const isActive = query.orderBy === sortKey;
|
||||
const isDescending = query.sortDescending === 'true';
|
||||
const direction = isActive ? (isDescending ? 'desc' : 'asc') : undefined;
|
||||
const activeColor = 'var(--text-primary)';
|
||||
|
||||
const getNextDirection = (): SortDirection => {
|
||||
if (!isActive) {
|
||||
return defaultDirection;
|
||||
}
|
||||
|
||||
return direction === 'desc' ? 'asc' : 'desc';
|
||||
};
|
||||
|
||||
const handleSort = () => {
|
||||
const nextDirection = getNextDirection();
|
||||
|
||||
router.push(
|
||||
updateParams({
|
||||
orderBy: sortKey,
|
||||
sortDescending: nextDirection === 'desc' ? 'true' : undefined,
|
||||
page: 1,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSort}
|
||||
className="inline-flex appearance-none items-center gap-1 border-0 bg-transparent p-0 text-inherit"
|
||||
aria-pressed={isActive}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
gap: 0,
|
||||
lineHeight: 0,
|
||||
color: 'var(--text-muted)',
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: direction === 'asc' ? activeColor : undefined,
|
||||
opacity: direction === 'asc' ? 1 : 0.55,
|
||||
transform: 'scale(0.9)',
|
||||
transformOrigin: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon size="sm" color={direction === 'asc' ? undefined : 'muted'}>
|
||||
<ChevronUp />
|
||||
</Icon>
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: direction === 'desc' ? activeColor : undefined,
|
||||
opacity: direction === 'desc' ? 1 : 0.55,
|
||||
marginTop: '-6px',
|
||||
transform: 'scale(0.9)',
|
||||
transformOrigin: 'center',
|
||||
}}
|
||||
>
|
||||
<Icon size="sm" color={direction === 'desc' ? undefined : 'muted'}>
|
||||
<ChevronDown />
|
||||
</Icon>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useApi } from '../useApi';
|
||||
import { useModified } from '../useModified';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
|
||||
export function useUserTeamsQuery(userId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`teams`);
|
||||
|
||||
return useQuery({
|
||||
return usePagedQuery({
|
||||
queryKey: ['teams', { userId, modified }],
|
||||
queryFn: () => {
|
||||
return get(`/users/${userId}/teams`);
|
||||
queryFn: params => {
|
||||
return get(`/users/${userId}/teams`, params);
|
||||
},
|
||||
enabled: !!userId,
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigation } from './useNavigation';
|
||||
|
||||
export function usePageParameters() {
|
||||
const {
|
||||
query: { page, pageSize, search },
|
||||
query: { page, pageSize, search, orderBy, sortDescending },
|
||||
} = useNavigation();
|
||||
|
||||
return useMemo(() => {
|
||||
@@ -11,6 +11,8 @@ export function usePageParameters() {
|
||||
page,
|
||||
pageSize,
|
||||
search,
|
||||
orderBy,
|
||||
sortDescending,
|
||||
};
|
||||
}, [page, pageSize, search]);
|
||||
}, [orderBy, page, pageSize, search, sortDescending]);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ export function usePagedQuery<TData = any, TError = Error>({
|
||||
queryFn: (params?: object) => Promise<PageResult<TData>> | PageResult<TData>;
|
||||
}): UseQueryResult<PageResult<TData>, TError> {
|
||||
const {
|
||||
query: { page, search },
|
||||
query: { page, search, orderBy, sortDescending },
|
||||
} = useNavigation();
|
||||
const { useQuery } = useApi();
|
||||
|
||||
return useQuery<PageResult<TData>, TError>({
|
||||
queryKey: [...queryKey, page, search] as const,
|
||||
queryFn: () => queryFn({ page, search }),
|
||||
queryKey: [...queryKey, page, search, orderBy, sortDescending] as const,
|
||||
queryFn: () => queryFn({ page, search, orderBy, sortDescending }),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user