Add personal dashboard flow and per-component website selection
Some checks are pending
Node.js CI / build (push) Waiting to run

This introduces a user-scoped dashboard with board-style view/edit pages while keeping it unavailable in team context, and moves website targeting to component config so dashboard components can each select their own website.
This commit is contained in:
Mike Cao 2026-02-13 11:48:15 -08:00
parent 2633697585
commit 631cc46f7f
73 changed files with 418 additions and 88 deletions

View file

@ -116,6 +116,16 @@ if (collectApiEndpoint) {
} }
const redirects = [ const redirects = [
{
source: '/teams/:id/dashboard/edit',
destination: '/dashboard/edit',
permanent: false,
},
{
source: '/teams/:id/dashboard',
destination: '/dashboard',
permanent: false,
},
{ {
source: '/settings', source: '/settings',
destination: '/settings/preferences', destination: '/settings/preferences',

View file

@ -98,7 +98,6 @@
"download": "تحميل", "download": "تحميل",
"dropoff": "إنزال", "dropoff": "إنزال",
"edit": "تعديل", "edit": "تعديل",
"edit-dashboard": "عدّل لوحة التحكم",
"edit-member": "عدّل العضو", "edit-member": "عدّل العضو",
"email": "بريد إلكتروني", "email": "بريد إلكتروني",
"enable-share-url": "فعّل مشاركة الرابط", "enable-share-url": "فعّل مشاركة الرابط",

View file

@ -98,7 +98,6 @@
"download": "Спампаваць", "download": "Спампаваць",
"dropoff": "Адмовы", "dropoff": "Адмовы",
"edit": "Змяніць", "edit": "Змяніць",
"edit-dashboard": "Змяніць інфармацыйную панэль",
"edit-member": "Рэдагаваць удзельніка", "edit-member": "Рэдагаваць удзельніка",
"email": "Электронная пошта", "email": "Электронная пошта",
"enable-share-url": "Дазволіць дзяліцца спасылкай", "enable-share-url": "Дазволіць дзяліцца спасылкай",

View file

@ -98,7 +98,6 @@
"download": "Изтегляне", "download": "Изтегляне",
"dropoff": "Отпадане", "dropoff": "Отпадане",
"edit": "Редактирай", "edit": "Редактирай",
"edit-dashboard": "Редактирай табло",
"edit-member": "Редактирай член", "edit-member": "Редактирай член",
"email": "Имейл", "email": "Имейл",
"enable-share-url": "Активирай Линк за споделяне", "enable-share-url": "Активирай Линк за споделяне",

View file

@ -98,7 +98,6 @@
"download": "ডাউনলোড", "download": "ডাউনলোড",
"dropoff": "ছেড়ে যাওয়া", "dropoff": "ছেড়ে যাওয়া",
"edit": "সম্পাদনা করুন", "edit": "সম্পাদনা করুন",
"edit-dashboard": "ড্যাশবোর্ড সম্পাদনা করুন",
"edit-member": "সদস্য সম্পাদনা করুন", "edit-member": "সদস্য সম্পাদনা করুন",
"email": "ইমেইল", "email": "ইমেইল",
"enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন", "enable-share-url": "শেয়ার ইউআরএল শেয়ার করুন",

View file

@ -98,7 +98,6 @@
"download": "Preuzmi", "download": "Preuzmi",
"dropoff": "Odlazak", "dropoff": "Odlazak",
"edit": "Uredi", "edit": "Uredi",
"edit-dashboard": "Uredi dashboard",
"edit-member": "Uredi člana", "edit-member": "Uredi člana",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Omogući URL za dijeljenje", "enable-share-url": "Omogući URL za dijeljenje",

View file

@ -98,7 +98,6 @@
"download": "Descarregar", "download": "Descarregar",
"dropoff": "Abandonament", "dropoff": "Abandonament",
"edit": "Edita", "edit": "Edita",
"edit-dashboard": "Edita panell",
"edit-member": "Edita membre", "edit-member": "Edita membre",
"email": "Correu electrònic", "email": "Correu electrònic",
"enable-share-url": "Activa l'enllaç per compartir", "enable-share-url": "Activa l'enllaç per compartir",

View file

@ -98,7 +98,6 @@
"download": "Stáhnout", "download": "Stáhnout",
"dropoff": "Opuštění", "dropoff": "Opuštění",
"edit": "Upravit", "edit": "Upravit",
"edit-dashboard": "Upravit dashboard",
"edit-member": "Upravit člena", "edit-member": "Upravit člena",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Povolit sdílení URL", "enable-share-url": "Povolit sdílení URL",

View file

@ -98,7 +98,6 @@
"download": "Download", "download": "Download",
"dropoff": "Frafald", "dropoff": "Frafald",
"edit": "Rediger", "edit": "Rediger",
"edit-dashboard": "Rediger betjeningspanel",
"edit-member": "Rediger medlem", "edit-member": "Rediger medlem",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Aktivér delings-URL", "enable-share-url": "Aktivér delings-URL",

View file

@ -98,7 +98,6 @@
"download": "Herunterladen", "download": "Herunterladen",
"dropoff": "Absprung", "dropoff": "Absprung",
"edit": "Bearbeite", "edit": "Bearbeite",
"edit-dashboard": "Dashboard bearbeite",
"edit-member": "Mitglied bearbeite", "edit-member": "Mitglied bearbeite",
"email": "E-Mail", "email": "E-Mail",
"enable-share-url": "Freigab-URL aktiviere", "enable-share-url": "Freigab-URL aktiviere",

View file

@ -98,7 +98,6 @@
"download": "Herunterladen", "download": "Herunterladen",
"dropoff": "Absprung", "dropoff": "Absprung",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"edit-dashboard": "Dashboard bearbeiten",
"edit-member": "Mitglied bearbeiten", "edit-member": "Mitglied bearbeiten",
"email": "E-Mail", "email": "E-Mail",
"enable-share-url": "Freigabe-URL aktivieren", "enable-share-url": "Freigabe-URL aktivieren",

View file

@ -98,7 +98,6 @@
"download": "Λήψη", "download": "Λήψη",
"dropoff": "Εγκατάλειψη", "dropoff": "Εγκατάλειψη",
"edit": "Επεξεργασία", "edit": "Επεξεργασία",
"edit-dashboard": "Επεξεργασία πίνακα ελέγχου",
"edit-member": "Επεξεργασία μέλους", "edit-member": "Επεξεργασία μέλους",
"email": "Email", "email": "Email",
"enable-share-url": "Ενεργοποίηση κοινής χρήσης URL", "enable-share-url": "Ενεργοποίηση κοινής χρήσης URL",

View file

@ -98,7 +98,6 @@
"download": "Download", "download": "Download",
"dropoff": "Dropoff", "dropoff": "Dropoff",
"edit": "Edit", "edit": "Edit",
"edit-dashboard": "Edit dashboard",
"edit-member": "Edit member", "edit-member": "Edit member",
"email": "Email", "email": "Email",
"enable-share-url": "Enable share URL", "enable-share-url": "Enable share URL",

View file

@ -98,7 +98,6 @@
"download": "Download", "download": "Download",
"dropoff": "Dropoff", "dropoff": "Dropoff",
"edit": "Edit", "edit": "Edit",
"edit-dashboard": "Edit dashboard",
"edit-member": "Edit member", "edit-member": "Edit member",
"email": "Email", "email": "Email",
"enable-share-url": "Enable share URL", "enable-share-url": "Enable share URL",
@ -351,6 +350,7 @@
"delete-team-warning": "Deleting a team will also delete all team websites.", "delete-team-warning": "Deleting a team will also delete all team websites.",
"delete-website-warning": "All website data will be deleted.", "delete-website-warning": "All website data will be deleted.",
"error": "Something went wrong.", "error": "Something went wrong.",
"empty-dashboard": "Your dashboard is empty. Click Edit to add your first component.",
"event-log": "<b>{event}</b> on <a>{url}</a>", "event-log": "<b>{event}</b> on <a>{url}</a>",
"forbidden": "Forbidden", "forbidden": "Forbidden",
"go-to-settings": "Go to settings", "go-to-settings": "Go to settings",

View file

@ -98,7 +98,6 @@
"download": "Descargar", "download": "Descargar",
"dropoff": "Abandono", "dropoff": "Abandono",
"edit": "Editar", "edit": "Editar",
"edit-dashboard": "Editar panel",
"edit-member": "Editar miembro", "edit-member": "Editar miembro",
"email": "Correo electrónico", "email": "Correo electrónico",
"enable-share-url": "Habilitar compartir URL", "enable-share-url": "Habilitar compartir URL",

View file

@ -98,7 +98,6 @@
"download": "دانلود", "download": "دانلود",
"dropoff": "رها کردن", "dropoff": "رها کردن",
"edit": "ویرایش", "edit": "ویرایش",
"edit-dashboard": "ویرایش داشبورد",
"edit-member": "ویرایش عضو", "edit-member": "ویرایش عضو",
"email": "ایمیل", "email": "ایمیل",
"enable-share-url": "فعال کردن اشتراک گذاری آدرس اینترنتی", "enable-share-url": "فعال کردن اشتراک گذاری آدرس اینترنتی",

View file

@ -98,7 +98,6 @@
"download": "Lataa", "download": "Lataa",
"dropoff": "Poistuminen", "dropoff": "Poistuminen",
"edit": "Muokkaa", "edit": "Muokkaa",
"edit-dashboard": "Muokkaa ohjauspaneelia",
"edit-member": "Muokkaa jäsentä", "edit-member": "Muokkaa jäsentä",
"email": "Sähköposti", "email": "Sähköposti",
"enable-share-url": "Ota jakamisen URL-osoite käyttöön", "enable-share-url": "Ota jakamisen URL-osoite käyttöön",

View file

@ -98,7 +98,6 @@
"download": "Tak niður", "download": "Tak niður",
"dropoff": "Fráfall", "dropoff": "Fráfall",
"edit": "Ger broyting", "edit": "Ger broyting",
"edit-dashboard": "Ritstjórna yvirlitsskíggja",
"edit-member": "Ritstjórna lim", "edit-member": "Ritstjórna lim",
"email": "Teldupostur", "email": "Teldupostur",
"enable-share-url": "Virkja deili leinki", "enable-share-url": "Virkja deili leinki",

View file

@ -98,7 +98,6 @@
"download": "Télécharger", "download": "Télécharger",
"dropoff": "Abandons", "dropoff": "Abandons",
"edit": "Modifier", "edit": "Modifier",
"edit-dashboard": "Modifier le tableau de bord",
"edit-member": "Modifier le membre", "edit-member": "Modifier le membre",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Activer l'URL de partage", "enable-share-url": "Activer l'URL de partage",

View file

@ -98,7 +98,6 @@
"download": "Descargar", "download": "Descargar",
"dropoff": "Disminución", "dropoff": "Disminución",
"edit": "Editar", "edit": "Editar",
"edit-dashboard": "Editar taboleiro",
"edit-member": "Editar membro", "edit-member": "Editar membro",
"email": "Correo electrónico", "email": "Correo electrónico",
"enable-share-url": "Activar URL de compartición", "enable-share-url": "Activar URL de compartición",

View file

@ -98,7 +98,6 @@
"download": "הורדה", "download": "הורדה",
"dropoff": "עזיבה", "dropoff": "עזיבה",
"edit": "עריכה", "edit": "עריכה",
"edit-dashboard": "ערוך לוח מחוונים",
"edit-member": "ערוך חבר", "edit-member": "ערוך חבר",
"email": "אימייל", "email": "אימייל",
"enable-share-url": "הפעלת URL שיתוף", "enable-share-url": "הפעלת URL שיתוף",

View file

@ -98,7 +98,6 @@
"download": "डाउनलोड", "download": "डाउनलोड",
"dropoff": "ड्रॉपऑफ", "dropoff": "ड्रॉपऑफ",
"edit": "संपादित करें", "edit": "संपादित करें",
"edit-dashboard": "डैशबोर्ड संपादित करें",
"edit-member": "सदस्य संपादित करें", "edit-member": "सदस्य संपादित करें",
"email": "ईमेल", "email": "ईमेल",
"enable-share-url": "शेयर URL सक्षम करें", "enable-share-url": "शेयर URL सक्षम करें",

View file

@ -98,7 +98,6 @@
"download": "Preuzmi", "download": "Preuzmi",
"dropoff": "Odlazak", "dropoff": "Odlazak",
"edit": "Uredi", "edit": "Uredi",
"edit-dashboard": "Uredi nadzornu ploču",
"edit-member": "Uredi člana", "edit-member": "Uredi člana",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Omogući dijeljenje poveznice", "enable-share-url": "Omogući dijeljenje poveznice",

View file

@ -98,7 +98,6 @@
"download": "Letöltés", "download": "Letöltés",
"dropoff": "Lemorzsolódás", "dropoff": "Lemorzsolódás",
"edit": "Módosítás", "edit": "Módosítás",
"edit-dashboard": "Irányítópult szerkesztése",
"edit-member": "Tag szerkesztése", "edit-member": "Tag szerkesztése",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "URL-megosztás engedélyezése", "enable-share-url": "URL-megosztás engedélyezése",

View file

@ -98,7 +98,6 @@
"download": "Unduh", "download": "Unduh",
"dropoff": "Penurunan", "dropoff": "Penurunan",
"edit": "Sunting", "edit": "Sunting",
"edit-dashboard": "Sunting dasbor",
"edit-member": "Sunting anggota", "edit-member": "Sunting anggota",
"email": "Email", "email": "Email",
"enable-share-url": "Aktifkan URL berbagi", "enable-share-url": "Aktifkan URL berbagi",

View file

@ -98,7 +98,6 @@
"download": "Scarica", "download": "Scarica",
"dropoff": "Abbandono", "dropoff": "Abbandono",
"edit": "Modifica", "edit": "Modifica",
"edit-dashboard": "Modifica pannello di controllo",
"edit-member": "Modifica membro", "edit-member": "Modifica membro",
"email": "Email", "email": "Email",
"enable-share-url": "Abilita URL di condivisione", "enable-share-url": "Abilita URL di condivisione",

View file

@ -98,7 +98,6 @@
"download": "ダウンロード", "download": "ダウンロード",
"dropoff": "切り捨て", "dropoff": "切り捨て",
"edit": "編集", "edit": "編集",
"edit-dashboard": "ダッシュボードの編集",
"edit-member": "メンバーの編集", "edit-member": "メンバーの編集",
"email": "メール", "email": "メール",
"enable-share-url": "共有URLを有効にする", "enable-share-url": "共有URLを有効にする",

View file

@ -98,7 +98,6 @@
"download": "ទាញយក", "download": "ទាញយក",
"dropoff": "ការចាកចេញ", "dropoff": "ការចាកចេញ",
"edit": "កែប្រែ", "edit": "កែប្រែ",
"edit-dashboard": "កែផ្ទាំងគ្រប់គ្រង",
"edit-member": "កែព័ត៌មានសមាជិក", "edit-member": "កែព័ត៌មានសមាជិក",
"email": "អ៊ីមែល", "email": "អ៊ីមែល",
"enable-share-url": "បើកការចែករំលែក URL", "enable-share-url": "បើកការចែករំលែក URL",

View file

@ -98,7 +98,6 @@
"download": "다운로드", "download": "다운로드",
"dropoff": "이탈", "dropoff": "이탈",
"edit": "편집", "edit": "편집",
"edit-dashboard": "대시보드 편집",
"edit-member": "멤버 편집", "edit-member": "멤버 편집",
"email": "이메일", "email": "이메일",
"enable-share-url": "URL 공유 활성화", "enable-share-url": "URL 공유 활성화",

View file

@ -98,7 +98,6 @@
"download": "Atsisiųsti", "download": "Atsisiųsti",
"dropoff": "Atsitraukimas", "dropoff": "Atsitraukimas",
"edit": "Redaguoti", "edit": "Redaguoti",
"edit-dashboard": "Redaguoti švieslentę",
"edit-member": "Redaguoti narį", "edit-member": "Redaguoti narį",
"email": "El. paštas", "email": "El. paštas",
"enable-share-url": "Įjungti bendrinimą su nuoroda", "enable-share-url": "Įjungti bendrinimą su nuoroda",

View file

@ -98,7 +98,6 @@
"download": "Татах", "download": "Татах",
"dropoff": "Уналт", "dropoff": "Уналт",
"edit": "Засах", "edit": "Засах",
"edit-dashboard": "Хянах самбар засах",
"edit-member": "Гишүүн засах", "edit-member": "Гишүүн засах",
"email": "Имэйл", "email": "Имэйл",
"enable-share-url": "Хуваалцах холбоос идэвхжүүлэх", "enable-share-url": "Хуваалцах холбоос идэвхжүүлэх",

View file

@ -98,7 +98,6 @@
"download": "Muat turun", "download": "Muat turun",
"dropoff": "Tercicir", "dropoff": "Tercicir",
"edit": "Sunting", "edit": "Sunting",
"edit-dashboard": "Sunting papan pemuka",
"edit-member": "Sunting ahli", "edit-member": "Sunting ahli",
"email": "E-mel", "email": "E-mel",
"enable-share-url": "Aktifkan url berkongsi", "enable-share-url": "Aktifkan url berkongsi",

View file

@ -98,7 +98,6 @@
"download": "ဒေါင်းလုဒ်လုပ်မည်", "download": "ဒေါင်းလုဒ်လုပ်မည်",
"dropoff": "ထွက်ခွာမှု", "dropoff": "ထွက်ခွာမှု",
"edit": "ပြုပြင်မည်", "edit": "ပြုပြင်မည်",
"edit-dashboard": "ဒက်ရှ်ဘုတ်ကို ပြုပြင်မည်",
"edit-member": "အဖွဲ့ဝင် ပြင်ဆင်မည်", "edit-member": "အဖွဲ့ဝင် ပြင်ဆင်မည်",
"email": "အီးမေးလ်", "email": "အီးမေးလ်",
"enable-share-url": "ဝေငှခြင်းကိုလင့်ကို ဖွင့်မည်", "enable-share-url": "ဝေငှခြင်းကိုလင့်ကို ဖွင့်မည်",

View file

@ -98,7 +98,6 @@
"download": "Last ned", "download": "Last ned",
"dropoff": "Frafall", "dropoff": "Frafall",
"edit": "Rediger", "edit": "Rediger",
"edit-dashboard": "Rediger dashboard",
"edit-member": "Rediger bruker", "edit-member": "Rediger bruker",
"email": "E-post", "email": "E-post",
"enable-share-url": "Aktiver delings-URL", "enable-share-url": "Aktiver delings-URL",

View file

@ -98,7 +98,6 @@
"download": "Downloaden", "download": "Downloaden",
"dropoff": "Uitval", "dropoff": "Uitval",
"edit": "Bewerken", "edit": "Bewerken",
"edit-dashboard": "Dashboard aanpassen",
"edit-member": "Gebruiker aanpassen", "edit-member": "Gebruiker aanpassen",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Sta delen via openbare URL toe", "enable-share-url": "Sta delen via openbare URL toe",

View file

@ -98,7 +98,6 @@
"download": "Pobierz", "download": "Pobierz",
"dropoff": "Odpływ", "dropoff": "Odpływ",
"edit": "Edytuj", "edit": "Edytuj",
"edit-dashboard": "Edytuj panel",
"edit-member": "Edytuj członka", "edit-member": "Edytuj członka",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Włącz udostępnianie adresu URL", "enable-share-url": "Włącz udostępnianie adresu URL",

View file

@ -98,7 +98,6 @@
"download": "Download", "download": "Download",
"dropoff": "Abandono", "dropoff": "Abandono",
"edit": "Editar", "edit": "Editar",
"edit-dashboard": "Editar painel",
"edit-member": "Editar membro", "edit-member": "Editar membro",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Ativar link para compartilhar", "enable-share-url": "Ativar link para compartilhar",

View file

@ -98,7 +98,6 @@
"download": "Transferir", "download": "Transferir",
"dropoff": "Abandono", "dropoff": "Abandono",
"edit": "Editar", "edit": "Editar",
"edit-dashboard": "Editar painel",
"edit-member": "Editar membro", "edit-member": "Editar membro",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Ativar link de partilha", "enable-share-url": "Ativar link de partilha",

View file

@ -98,7 +98,6 @@
"download": "Descarcă", "download": "Descarcă",
"dropoff": "Rată de abandon", "dropoff": "Rată de abandon",
"edit": "Editare", "edit": "Editare",
"edit-dashboard": "Editare tablou de bord",
"edit-member": "Editare membru", "edit-member": "Editare membru",
"email": "Email", "email": "Email",
"enable-share-url": "Activare adresă URL de distribuire", "enable-share-url": "Activare adresă URL de distribuire",

View file

@ -98,7 +98,6 @@
"download": "Скачать", "download": "Скачать",
"dropoff": "Высадка", "dropoff": "Высадка",
"edit": "Изменить", "edit": "Изменить",
"edit-dashboard": "Редактировать дашборд",
"edit-member": "Редактировать участника", "edit-member": "Редактировать участника",
"email": "Электронная почта", "email": "Электронная почта",
"enable-share-url": "Разрешить делиться ссылкой", "enable-share-url": "Разрешить делиться ссылкой",

View file

@ -98,7 +98,6 @@
"download": "බාගන්න", "download": "බාගන්න",
"dropoff": "අතහැර යෑම", "dropoff": "අතහැර යෑම",
"edit": "සංස්කරණය කරන්න", "edit": "සංස්කරණය කරන්න",
"edit-dashboard": "උපකරණ පුවරුව සංස්කරණය",
"edit-member": "සාමාජිකයා සංස්කරණය", "edit-member": "සාමාජිකයා සංස්කරණය",
"email": "විද්‍යුත් තැපෑල", "email": "විද්‍යුත් තැපෑල",
"enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න", "enable-share-url": "බෙදාගැනීමේ URL සබල කරන්න",

View file

@ -98,7 +98,6 @@
"download": "Stiahnuť", "download": "Stiahnuť",
"dropoff": "Odchod", "dropoff": "Odchod",
"edit": "Upraviť", "edit": "Upraviť",
"edit-dashboard": "Upraviť prehľad",
"edit-member": "Upraviť člena", "edit-member": "Upraviť člena",
"email": "E-mail", "email": "E-mail",
"enable-share-url": "Povoliť zdielanie URL", "enable-share-url": "Povoliť zdielanie URL",

View file

@ -98,7 +98,6 @@
"download": "Prenesi", "download": "Prenesi",
"dropoff": "Zapustitev", "dropoff": "Zapustitev",
"edit": "Uredi", "edit": "Uredi",
"edit-dashboard": "Uredi nadzorno ploščo",
"edit-member": "Uredi člana", "edit-member": "Uredi člana",
"email": "E-pošta", "email": "E-pošta",
"enable-share-url": "Omogoči povezavo za deljenje", "enable-share-url": "Omogoči povezavo za deljenje",

View file

@ -98,7 +98,6 @@
"download": "Ladda ner", "download": "Ladda ner",
"dropoff": "Bortfall", "dropoff": "Bortfall",
"edit": "Redigera", "edit": "Redigera",
"edit-dashboard": "Redigera översikt",
"edit-member": "Redigera medlem", "edit-member": "Redigera medlem",
"email": "E-post", "email": "E-post",
"enable-share-url": "Aktivera delningslänk", "enable-share-url": "Aktivera delningslänk",

View file

@ -98,7 +98,6 @@
"download": "பதிவிறக்கு", "download": "பதிவிறக்கு",
"dropoff": "விலகல்", "dropoff": "விலகல்",
"edit": "திருத்துதல்", "edit": "திருத்துதல்",
"edit-dashboard": "முகப்பைத் திருத்து",
"edit-member": "உறுப்பினரைத் திருத்து", "edit-member": "உறுப்பினரைத் திருத்து",
"email": "மின்னஞ்சல்", "email": "மின்னஞ்சல்",
"enable-share-url": "கள முகவரியை பகிரலாம்", "enable-share-url": "கள முகவரியை பகிரலாம்",

View file

@ -98,7 +98,6 @@
"download": "ดาวน์โหลด", "download": "ดาวน์โหลด",
"dropoff": "การออกจากระบบ", "dropoff": "การออกจากระบบ",
"edit": "แก้ไข", "edit": "แก้ไข",
"edit-dashboard": "แก้ไขแดชบอร์ด",
"edit-member": "แก้ไขสมาชิก", "edit-member": "แก้ไขสมาชิก",
"email": "อีเมล", "email": "อีเมล",
"enable-share-url": "เปิดใช้งานการแชร์ลิงก์", "enable-share-url": "เปิดใช้งานการแชร์ลิงก์",

View file

@ -98,7 +98,6 @@
"download": "İndir", "download": "İndir",
"dropoff": "Bırakma", "dropoff": "Bırakma",
"edit": "Düzenle", "edit": "Düzenle",
"edit-dashboard": "Kontrol panelini düzenle",
"edit-member": "Üyeyi düzenle", "edit-member": "Üyeyi düzenle",
"email": "E-posta", "email": "E-posta",
"enable-share-url": "Anonim paylaşım URL'i aktif", "enable-share-url": "Anonim paylaşım URL'i aktif",

View file

@ -98,7 +98,6 @@
"download": "Завантажити", "download": "Завантажити",
"dropoff": "Відсів", "dropoff": "Відсів",
"edit": "Редагувати", "edit": "Редагувати",
"edit-dashboard": "Редагувати панель",
"edit-member": "Редагувати учасника", "edit-member": "Редагувати учасника",
"email": "Електронна пошта", "email": "Електронна пошта",
"enable-share-url": "Увімкнути спільне посилання", "enable-share-url": "Увімкнути спільне посилання",

View file

@ -98,7 +98,6 @@
"download": "ڈاؤن لوڈ", "download": "ڈاؤن لوڈ",
"dropoff": "ڈراپ آف", "dropoff": "ڈراپ آف",
"edit": "ترمیم", "edit": "ترمیم",
"edit-dashboard": "ڈیش بورڈ میں ترمیم",
"edit-member": "رکن میں ترمیم", "edit-member": "رکن میں ترمیم",
"email": "ای میل", "email": "ای میل",
"enable-share-url": "شیئر یو آر ایل کو فعال کریں", "enable-share-url": "شیئر یو آر ایل کو فعال کریں",

View file

@ -98,7 +98,6 @@
"download": "Yuklab olish", "download": "Yuklab olish",
"dropoff": "Tashlab ketish", "dropoff": "Tashlab ketish",
"edit": "Tahrirlash", "edit": "Tahrirlash",
"edit-dashboard": "Boshqaruv panelini tahrirlash",
"edit-member": "A'zoni tahrirlash", "edit-member": "A'zoni tahrirlash",
"email": "Elektron pochta", "email": "Elektron pochta",
"enable-share-url": "Ulashish URL'ini yoqish", "enable-share-url": "Ulashish URL'ini yoqish",

View file

@ -98,7 +98,6 @@
"download": "Tải xuống", "download": "Tải xuống",
"dropoff": "Tỷ lệ bỏ qua", "dropoff": "Tỷ lệ bỏ qua",
"edit": "Chỉnh sửa", "edit": "Chỉnh sửa",
"edit-dashboard": "Chỉnh sửa bảng điều khiển",
"edit-member": "Chỉnh sửa thành viên", "edit-member": "Chỉnh sửa thành viên",
"email": "Email", "email": "Email",
"enable-share-url": "Bật chia sẻ URL", "enable-share-url": "Bật chia sẻ URL",

View file

@ -98,7 +98,6 @@
"download": "下载", "download": "下载",
"dropoff": "丢弃", "dropoff": "丢弃",
"edit": "编辑", "edit": "编辑",
"edit-dashboard": "编辑仪表盘",
"edit-member": "编辑成员", "edit-member": "编辑成员",
"email": "邮箱", "email": "邮箱",
"enable-share-url": "启用共享链接", "enable-share-url": "启用共享链接",

View file

@ -98,7 +98,6 @@
"download": "下載", "download": "下載",
"dropoff": "離開", "dropoff": "離開",
"edit": "編輯", "edit": "編輯",
"edit-dashboard": "編輯儀表板",
"edit-member": "編輯成員", "edit-member": "編輯成員",
"email": "電子郵件", "email": "電子郵件",
"enable-share-url": "啟用分享連結", "enable-share-url": "啟用分享連結",

View file

@ -14,16 +14,33 @@ import { SettingsNav } from '@/app/(main)/settings/SettingsNav';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav'; import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { IconLabel } from '@/components/common/IconLabel'; import { IconLabel } from '@/components/common/IconLabel';
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks'; import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons'; import {
Globe,
Grid2x2,
LayoutDashboard,
LayoutGrid,
LinkIcon,
PanelLeft,
} from '@/components/icons';
import { UserButton } from '@/components/input/UserButton'; import { UserButton } from '@/components/input/UserButton';
import { Logo } from '@/components/svg'; import { Logo } from '@/components/svg';
export function SideNav(props: any) { export function SideNav(props: any) {
const { t, labels } = useMessages(); const { t, labels } = useMessages();
const { pathname, renderUrl, websiteId } = useNavigation(); const { pathname, renderUrl, websiteId, teamId } = useNavigation();
const [isCollapsed] = useGlobalState('sidenav-collapsed', false); const [isCollapsed] = useGlobalState('sidenav-collapsed', false);
const links = [ const links = [
...(!teamId
? [
{
id: 'dashboard',
label: t(labels.dashboard),
path: '/dashboard',
icon: <LayoutGrid />,
},
]
: []),
{ {
id: 'boards', id: 'boards',
label: t(labels.boards), label: t(labels.boards),

View file

@ -8,7 +8,7 @@ function BoardComponentRendererComponent({
websiteId, websiteId,
}: { }: {
config: BoardComponentConfig; config: BoardComponentConfig;
websiteId: string; websiteId?: string;
}) { }) {
const definition = getComponentDefinition(config.type); const definition = getComponentDefinition(config.type);
@ -22,6 +22,14 @@ function BoardComponentRendererComponent({
const Component = definition.component; const Component = definition.component;
if (!websiteId) {
return (
<Column alignItems="center" justifyContent="center" width="100%" height="100%">
<Text color="muted">Select a website</Text>
</Column>
);
}
return <Component websiteId={websiteId} {...config.props} />; return <Component websiteId={websiteId} {...config.props} />;
} }

View file

@ -11,6 +11,7 @@ import {
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import type { BoardComponentConfig } from '@/lib/types'; import type { BoardComponentConfig } from '@/lib/types';
import { import {
CATEGORIES, CATEGORIES,
@ -21,12 +22,16 @@ import {
import { BoardComponentRenderer } from './BoardComponentRenderer'; import { BoardComponentRenderer } from './BoardComponentRenderer';
export function BoardComponentSelect({ export function BoardComponentSelect({
teamId,
websiteId, websiteId,
defaultWebsiteId,
initialConfig, initialConfig,
onSelect, onSelect,
onClose, onClose,
}: { }: {
websiteId: string; teamId?: string;
websiteId?: string;
defaultWebsiteId?: string;
initialConfig?: BoardComponentConfig; initialConfig?: BoardComponentConfig;
onSelect: (config: BoardComponentConfig) => void; onSelect: (config: BoardComponentConfig) => void;
onClose: () => void; onClose: () => void;
@ -34,6 +39,9 @@ export function BoardComponentSelect({
const { t, labels, messages } = useMessages(); const { t, labels, messages } = useMessages();
const [selectedDef, setSelectedDef] = useState<ComponentDefinition | null>(null); const [selectedDef, setSelectedDef] = useState<ComponentDefinition | null>(null);
const [configValues, setConfigValues] = useState<Record<string, any>>({}); const [configValues, setConfigValues] = useState<Record<string, any>>({});
const [selectedWebsiteId, setSelectedWebsiteId] = useState(
initialConfig?.websiteId || websiteId || defaultWebsiteId,
);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
@ -73,9 +81,10 @@ export function BoardComponentSelect({
setSelectedDef(definition); setSelectedDef(definition);
setConfigValues(getDefaultConfigValues(definition, initialConfig)); setConfigValues(getDefaultConfigValues(definition, initialConfig));
setSelectedWebsiteId(initialConfig.websiteId || websiteId || defaultWebsiteId);
setTitle(initialConfig.title ?? ''); setTitle(initialConfig.title ?? '');
setDescription(initialConfig.description || ''); setDescription(initialConfig.description || '');
}, [initialConfig, allDefinitions]); }, [initialConfig, allDefinitions, websiteId, defaultWebsiteId]);
const handleSelectComponent = (def: ComponentDefinition) => { const handleSelectComponent = (def: ComponentDefinition) => {
setSelectedDef(def); setSelectedDef(def);
@ -107,6 +116,7 @@ export function BoardComponentSelect({
const config: BoardComponentConfig = { const config: BoardComponentConfig = {
type: selectedDef.type, type: selectedDef.type,
websiteId: selectedWebsiteId,
title, title,
description, description,
}; };
@ -172,12 +182,14 @@ export function BoardComponentSelect({
<Column gap="3" flexGrow={1} style={{ minWidth: 0 }}> <Column gap="3" flexGrow={1} style={{ minWidth: 0 }}>
<Panel maxHeight="100%"> <Panel maxHeight="100%">
{previewConfig && websiteId ? ( {previewConfig && selectedWebsiteId ? (
<BoardComponentRenderer config={previewConfig} websiteId={websiteId} /> <BoardComponentRenderer config={previewConfig} websiteId={selectedWebsiteId} />
) : ( ) : (
<Column alignItems="center" justifyContent="center" height="100%"> <Column alignItems="center" justifyContent="center" height="100%">
<Text color="muted"> <Text color="muted">
{websiteId ? t(messages.selectComponentPreview) : t(messages.selectWebsiteFirst)} {selectedWebsiteId
? t(messages.selectComponentPreview)
: t(messages.selectWebsiteFirst)}
</Text> </Text>
</Column> </Column>
)} )}
@ -187,6 +199,18 @@ export function BoardComponentSelect({
<Column gap="3" style={{ width: 320, flexShrink: 0, overflowY: 'auto' }}> <Column gap="3" style={{ width: 320, flexShrink: 0, overflowY: 'auto' }}>
<Text weight="bold">{t(labels.properties)}</Text> <Text weight="bold">{t(labels.properties)}</Text>
<Column gap="2">
<Text size="sm" color="muted">
{t(labels.website)}
</Text>
<WebsiteSelect
websiteId={selectedWebsiteId}
teamId={teamId}
placeholder={t(labels.selectWebsite)}
onChange={setSelectedWebsiteId}
/>
</Column>
<Column gap="2"> <Column gap="2">
<Text size="sm" color="muted"> <Text size="sm" color="muted">
{t(labels.title)} {t(labels.title)}

View file

@ -8,7 +8,7 @@ import { GripHorizontal, Plus } from '@/components/icons';
import { BoardEditRow } from './BoardEditRow'; import { BoardEditRow } from './BoardEditRow';
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants'; import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
export function BoardEditBody() { export function BoardEditBody({ requiresBoardWebsite = true }: { requiresBoardWebsite?: boolean }) {
const { board, updateBoard, registerLayoutGetter } = useBoard(); const { board, updateBoard, registerLayoutGetter } = useBoard();
const rowGroupRef = useRef<GroupImperativeHandle>(null); const rowGroupRef = useRef<GroupImperativeHandle>(null);
const columnGroupRefs = useRef<Map<string, GroupImperativeHandle>>(new Map()); const columnGroupRefs = useRef<Map<string, GroupImperativeHandle>>(new Map());
@ -103,6 +103,7 @@ export function BoardEditBody() {
}; };
const websiteId = board?.parameters?.websiteId; const websiteId = board?.parameters?.websiteId;
const canEdit = requiresBoardWebsite ? !!websiteId : true;
const rows = board?.parameters?.rows ?? []; const rows = board?.parameters?.rows ?? [];
const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT; const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
@ -122,7 +123,7 @@ export function BoardEditBody() {
rowId={row.id} rowId={row.id}
rowIndex={index} rowIndex={index}
rowCount={rows.length} rowCount={rows.length}
canEdit={!!websiteId} canEdit={canEdit}
onRemove={handleRemoveRow} onRemove={handleRemoveRow}
onMoveUp={handleMoveRowUp} onMoveUp={handleMoveRowUp}
onMoveDown={handleMoveRowDown} onMoveDown={handleMoveRowDown}
@ -157,7 +158,7 @@ export function BoardEditBody() {
)} )}
</Fragment> </Fragment>
))} ))}
{!!websiteId && ( {canEdit && (
<Panel minSize={BUTTON_ROW_HEIGHT}> <Panel minSize={BUTTON_ROW_HEIGHT}>
<Row padding="3"> <Row padding="3">
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>

View file

@ -11,7 +11,7 @@ import {
} from '@umami/react-zen'; } from '@umami/react-zen';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { useBoard, useMessages } from '@/components/hooks'; import { useBoard, useMessages, useNavigation } from '@/components/hooks';
import { Pencil, Plus, X } from '@/components/icons'; import { Pencil, Plus, X } from '@/components/icons';
import type { BoardComponentConfig } from '@/lib/types'; import type { BoardComponentConfig } from '@/lib/types';
import { BoardComponentRenderer } from './BoardComponentRenderer'; import { BoardComponentRenderer } from './BoardComponentRenderer';
@ -36,7 +36,9 @@ export function BoardEditColumn({
const [showActions, setShowActions] = useState(false); const [showActions, setShowActions] = useState(false);
const { board } = useBoard(); const { board } = useBoard();
const { t, labels } = useMessages(); const { t, labels } = useMessages();
const websiteId = board?.parameters?.websiteId; const { teamId } = useNavigation();
const boardWebsiteId = board?.parameters?.websiteId;
const websiteId = component?.websiteId || boardWebsiteId;
const renderedComponent = useMemo(() => { const renderedComponent = useMemo(() => {
if (!component || !websiteId) { if (!component || !websiteId) {
return null; return null;
@ -126,7 +128,9 @@ export function BoardEditColumn({
> >
{() => ( {() => (
<BoardComponentSelect <BoardComponentSelect
teamId={teamId}
websiteId={websiteId} websiteId={websiteId}
defaultWebsiteId={boardWebsiteId}
initialConfig={component} initialConfig={component}
onSelect={handleSelect} onSelect={handleSelect}
onClose={() => setShowSelect(false)} onClose={() => setShowSelect(false)}

View file

@ -6,7 +6,7 @@ import { BoardComponentRenderer } from './BoardComponentRenderer';
export function BoardViewColumn({ component }: { component?: BoardComponentConfig }) { export function BoardViewColumn({ component }: { component?: BoardComponentConfig }) {
const { board } = useBoard(); const { board } = useBoard();
const websiteId = board?.parameters?.websiteId; const websiteId = component?.websiteId || board?.parameters?.websiteId;
if (!component || !websiteId) { if (!component || !websiteId) {
return null; return null;

View file

@ -0,0 +1,27 @@
import { Button, LoadingButton, Row } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader';
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
export function DashboardEditHeader() {
const { saveBoard, isPending } = useBoard();
const { t, labels } = useMessages();
const { router, renderUrl } = useNavigation();
const handleSave = async () => {
await saveBoard();
router.push(renderUrl('/dashboard', false));
};
return (
<PageHeader title={t(labels.dashboard)}>
<Row gap="3">
<Button variant="quiet" onPress={() => router.push(renderUrl('/dashboard', false))}>
{t(labels.cancel)}
</Button>
<LoadingButton variant="primary" onPress={handleSave} isLoading={isPending}>
{t(labels.save)}
</LoadingButton>
</Row>
</PageHeader>
);
}

View file

@ -0,0 +1,33 @@
'use client';
import { Column } from '@umami/react-zen';
import { useEffect } from 'react';
import { BoardEditBody } from '@/app/(main)/boards/[boardId]/BoardEditBody';
import { PageBody } from '@/components/common/PageBody';
import { useNavigation } from '@/components/hooks';
import { DashboardEditHeader } from './DashboardEditHeader';
import { DashboardProvider } from './DashboardProvider';
export function DashboardEditPage() {
const { teamId, router } = useNavigation();
useEffect(() => {
if (teamId) {
router.replace('/dashboard/edit');
}
}, [teamId, router]);
if (teamId) {
return null;
}
return (
<DashboardProvider editing>
<PageBody>
<Column>
<DashboardEditHeader />
<BoardEditBody requiresBoardWebsite={false} />
</Column>
</PageBody>
</DashboardProvider>
);
}

View file

@ -1,17 +0,0 @@
'use client';
import { Column } from '@umami/react-zen';
import { PageBody } from '@/components/common/PageBody';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages } from '@/components/hooks';
export function DashboardPage() {
const { t, labels } = useMessages();
return (
<PageBody>
<Column margin="2">
<PageHeader title={t(labels.dashboard)}></PageHeader>
</Column>
</PageBody>
);
}

View file

@ -0,0 +1,111 @@
'use client';
import { Loading, useToast } from '@umami/react-zen';
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { BoardContext, type LayoutGetter } from '@/app/(main)/boards/BoardProvider';
import { getComponentDefinition } from '@/app/(main)/boards/boardComponentRegistry';
import { useApi, useDashboardQuery, useMessages, useModified } from '@/components/hooks';
import type { Board, BoardParameters } from '@/lib/types';
const createDefaultBoard = (): Partial<Board> => ({
name: '',
description: '',
parameters: {
rows: [{ id: uuid(), columns: [{ id: uuid(), component: null }] }],
},
});
function sanitizeBoardParameters(parameters?: BoardParameters): BoardParameters | undefined {
if (!parameters?.rows) {
return parameters;
}
return {
...parameters,
rows: parameters.rows.map(row => ({
...row,
columns: row.columns.map(column => {
if (column.component && !getComponentDefinition(column.component.type)) {
return {
...column,
component: null,
};
}
return column;
}),
})),
};
}
export function DashboardProvider({
editing = false,
children,
}: {
editing?: boolean;
children: ReactNode;
}) {
const { data, isFetching, isLoading } = useDashboardQuery();
const { post, useMutation } = useApi();
const { touch } = useModified();
const { toast } = useToast();
const { t, labels, messages } = useMessages();
const [board, setBoard] = useState<Partial<Board>>(data ?? createDefaultBoard());
const layoutGetterRef = useRef<LayoutGetter | null>(null);
const registerLayoutGetter = useCallback((getter: LayoutGetter) => {
layoutGetterRef.current = getter;
}, []);
useEffect(() => {
if (data) {
setBoard({
...data,
parameters: sanitizeBoardParameters(data.parameters),
});
}
}, [data]);
const { mutateAsync, isPending } = useMutation({
mutationFn: (boardData: Partial<Board>) => {
return post('/dashboard', boardData);
},
});
const updateBoard = useCallback((data: Partial<Board>) => {
setBoard(current => ({ ...current, ...data }));
}, []);
const saveBoard = useCallback(async () => {
const dashboardName = t(labels.dashboard);
const layoutData = layoutGetterRef.current?.();
const parameters = sanitizeBoardParameters(
layoutData ? { ...board.parameters, ...layoutData } : board.parameters,
);
const result = await mutateAsync({
...board,
name: dashboardName,
description: '',
parameters,
});
toast(t(messages.saved));
touch('dashboard');
touch('boards');
return result;
}, [board, labels.dashboard, messages.saved, mutateAsync, t, toast, touch]);
if (isFetching && isLoading) {
return <Loading placement="absolute" />;
}
return (
<BoardContext.Provider
value={{ board, editing, updateBoard, saveBoard, isPending, registerLayoutGetter }}
>
{children}
</BoardContext.Provider>
);
}

View file

@ -0,0 +1,18 @@
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, useNavigation } from '@/components/hooks';
import { Edit } from '@/components/icons';
export function DashboardViewHeader() {
const { t, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
<PageHeader title={t(labels.dashboard)}>
<LinkButton href={renderUrl('/dashboard/edit', false)}>
<IconLabel icon={<Edit />}>{t(labels.edit)}</IconLabel>
</LinkButton>
</PageHeader>
);
}

View file

@ -0,0 +1,49 @@
'use client';
import { Column } from '@umami/react-zen';
import { useEffect } from 'react';
import { BoardViewBody } from '@/app/(main)/boards/[boardId]/BoardViewBody';
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
import { PageBody } from '@/components/common/PageBody';
import { useBoard, useMessages, useNavigation } from '@/components/hooks';
import { DashboardProvider } from './DashboardProvider';
import { DashboardViewHeader } from './DashboardViewHeader';
function DashboardContent() {
const { board } = useBoard();
const { t, labels, messages } = useMessages();
const rows = board?.parameters?.rows ?? [];
const hasComponents = rows.some(row => row.columns?.some(column => !!column.component));
if (!hasComponents) {
return (
<EmptyPlaceholder title={t(labels.dashboard)} description={t(messages.emptyDashboard)} />
);
}
return <BoardViewBody />;
}
export function DashboardViewPage() {
const { teamId, router } = useNavigation();
useEffect(() => {
if (teamId) {
router.replace('/dashboard');
}
}, [teamId, router]);
if (teamId) {
return null;
}
return (
<DashboardProvider>
<PageBody>
<Column>
<DashboardViewHeader />
<DashboardContent />
</Column>
</PageBody>
</DashboardProvider>
);
}

View file

@ -0,0 +1,10 @@
import type { Metadata } from 'next';
import { DashboardEditPage } from '../DashboardEditPage';
export default function () {
return <DashboardEditPage />;
}
export const metadata: Metadata = {
title: 'Edit Dashboard',
};

View file

@ -1,8 +1,8 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { DashboardPage } from './DashboardPage'; import { DashboardViewPage } from './DashboardViewPage';
export default async function () { export default async function () {
return <DashboardPage />; return <DashboardViewPage />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {

View file

@ -0,0 +1,64 @@
import { z } from 'zod';
import { uuid } from '@/lib/crypto';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { createBoard, getBoard, updateBoard } from '@/queries/prisma';
export async function GET(request: Request) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
const board = await getBoard(auth.user.id);
if (board && board.userId !== auth.user.id) {
return unauthorized();
}
return json(board);
}
export async function POST(request: Request) {
const schema = z.object({
name: z.string().optional(),
description: z.string().optional(),
parameters: z.object({}).passthrough().optional(),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const userId = auth.user.id;
const existing = await getBoard(userId);
if (existing && existing.userId !== userId) {
return unauthorized();
}
const data = {
name: body.name,
description: body.description,
parameters: body.parameters ?? {},
};
if (existing) {
const result = await updateBoard(userId, data);
return json(result);
}
const result = await createBoard({
id: userId,
type: 'dashboard',
slug: uuid(),
userId,
...data,
});
return json(result);
}

View file

@ -13,6 +13,7 @@ export * from './context/useWebsite';
export * from './queries/useActiveUsersQuery'; export * from './queries/useActiveUsersQuery';
export * from './queries/useBoardQuery'; export * from './queries/useBoardQuery';
export * from './queries/useBoardsQuery'; export * from './queries/useBoardsQuery';
export * from './queries/useDashboardQuery';
export * from './queries/useDateRangeQuery'; export * from './queries/useDateRangeQuery';
export * from './queries/useDeleteQuery'; export * from './queries/useDeleteQuery';
export * from './queries/useEventDataEventsQuery'; export * from './queries/useEventDataEventsQuery';

View file

@ -0,0 +1,16 @@
import { keepPreviousData } from '@tanstack/react-query';
import type { ReactQueryOptions } from '@/lib/types';
import { useApi } from '../useApi';
import { useModified } from '../useModified';
export function useDashboardQuery(options?: ReactQueryOptions) {
const { get, useQuery } = useApi();
const { modified } = useModified('dashboard');
return useQuery({
queryKey: ['dashboard', { modified }],
queryFn: () => get('/dashboard'),
placeholderData: keepPreviousData,
...options,
});
}

View file

@ -24,7 +24,7 @@ export function WebsiteSelect({
includeTeams?: boolean; includeTeams?: boolean;
isCollapsed?: boolean; isCollapsed?: boolean;
} & SelectProps) { } & SelectProps) {
const { t, messages } = useMessages(); const { t, labels, messages } = useMessages();
const { data: website } = useWebsiteQuery(websiteId); const { data: website } = useWebsiteQuery(websiteId);
const [name, setName] = useState<string>(website?.name); const [name, setName] = useState<string>(website?.name);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
@ -57,12 +57,16 @@ export function WebsiteSelect({
return ''; return '';
} }
const value = name || props.placeholder || t(labels.selectWebsite);
return ( return (
<Row alignItems="center" gap> <Row alignItems="center" gap>
<Icon> <Icon>
<Globe /> <Globe />
</Icon> </Icon>
<Text truncate>{name}</Text> <Text truncate color={name ? undefined : 'muted'}>
{value}
</Text>
</Row> </Row>
); );
}; };

View file

@ -155,7 +155,6 @@ export const labels: Record<string, string> = {
tablet: 'label.tablet', tablet: 'label.tablet',
mobile: 'label.mobile', mobile: 'label.mobile',
toggleCharts: 'label.toggle-charts', toggleCharts: 'label.toggle-charts',
editDashboard: 'label.edit-dashboard',
title: 'label.title', title: 'label.title',
view: 'label.view', view: 'label.view',
cities: 'label.cities', cities: 'label.cities',
@ -368,6 +367,7 @@ export const messages: Record<string, string> = {
noResultsFound: 'message.no-results-found', noResultsFound: 'message.no-results-found',
noWebsitesConfigured: 'message.no-websites-configured', noWebsitesConfigured: 'message.no-websites-configured',
noTeamWebsites: 'message.no-team-websites', noTeamWebsites: 'message.no-team-websites',
emptyDashboard: 'message.empty-dashboard',
selectComponentPreview: 'message.select-component-preview', selectComponentPreview: 'message.select-component-preview',
selectWebsiteFirst: 'message.select-website-first', selectWebsiteFirst: 'message.select-website-first',
teamWebsitesInfo: 'message.team-websites-info', teamWebsitesInfo: 'message.team-websites-info',

View file

@ -146,6 +146,7 @@ export interface ApiError extends Error {
export interface BoardComponentConfig { export interface BoardComponentConfig {
type: string; type: string;
websiteId?: string;
title?: string; title?: string;
description?: string; description?: string;
props?: Record<string, any>; props?: Record<string, any>;