diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx index 30137fba40..3de42e9d04 100644 --- a/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/analysis/charts/page.tsx @@ -1,9 +1,8 @@ -const ChartsPage = () => { - return ( -
- Charts will appear here. -
- ); +import { ChartsListPage } from "@/modules/ee/analysis/charts/components/charts-list-page"; + +const ChartsPage = async (props: Readonly<{ params: Promise<{ environmentId: string }> }>) => { + const { environmentId } = await props.params; + return ; }; export default ChartsPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx index f0aa426ff1..05bc0fd75e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/[dashboardId]/page.tsx @@ -1,4 +1,4 @@ -const DashboardDetailPage = async (props: { params: Promise<{ dashboardId: string }> }) => { +const DashboardDetailPage = async (props: Readonly<{ params: Promise<{ dashboardId: string }> }>) => { const { dashboardId } = await props.params; return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx index d21f752e70..2ab40d21a4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/analysis/dashboards/page.tsx @@ -1 +1,8 @@ -export { DashboardsListPage as default } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page"; +import { DashboardsListPage } from "@/modules/ee/analysis/dashboards/pages/dashboards-list-page"; + +const DashboardsPage = async (props: Readonly<{ params: Promise<{ environmentId: string }> }>) => { + const { environmentId } = await props.params; + return ; +}; + +export default DashboardsPage; diff --git a/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx b/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx index 0de9ee1a64..c2ab125626 100644 --- a/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/analysis/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -const AnalysisPage = async (props: { params: Promise<{ environmentId: string }> }) => { +const AnalysisPage = async (props: Readonly<{ params: Promise<{ environmentId: string }> }>) => { const { environmentId } = await props.params; return redirect(`/environments/${environmentId}/analysis/dashboards`); }; diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index d94a6a253d..426ad21f60 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -123,6 +123,7 @@ checksums: common/bottom_right: aaef9a70ef795affc806c6d1853d8373 common/cancel: 2e2a849c2223911717de8caa2c71bade common/centered_modal: 982ff411cb7e91e30300c2ed56b7e507 + common/chart: 6f4d9c56e45ceb8fc22d2f74454cd813 common/charts: 1da4564d89264c89de4ed28d7451b43e common/choices: 8a7a77a71ec6eebc363c5dc0f8490a4d common/choose_environment: 5762cd499529815fc3e6a7feea39f90b @@ -203,6 +204,7 @@ checksums: common/failed_to_copy_to_clipboard: de836a7d628d36c832809252f188f784 common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4 + common/filter: 626325a05e4c8800f7ede7012b0cadaf common/finish: ffa7a10f71182b48fefed7135bee24fa common/first_name: cf040a5d6a9fd696be400380cc99f54b common/follow_these: 3a730b242bb17a3f95e01bf0dae86885 @@ -585,6 +587,20 @@ checksums: environments/actions/you_can_track_code_action_anywhere_in_your_app_using: 3c0bbf160b8ddbeef142403103b70554 environments/actions/your_survey_would_be_shown_on_this_url: 766fdeeb52d170c156af5d035a1f8c37 environments/actions/your_survey_would_not_be_shown: af44fe160f449ff9557ebe5d3686832d + environments/analysis/charts/action_coming_soon: ee2b0671e00972773210c5be5a9ccb89 + environments/analysis/charts/chart_deleted_successfully: 79148f471cd9acc2c8d0d033fb85437e + environments/analysis/charts/chart_deletion_error: 267eb65c168e726075d7cea678dd32e0 + environments/analysis/charts/chart_duplicated_successfully: 755c4ce5bf533764d549a53c33e32165 + environments/analysis/charts/chart_duplication_error: 90d7166c85188b52f821c9d9f53ff8c4 + environments/analysis/charts/chart_type_area: 535754c6425f045f17e1dcb551840c93 + environments/analysis/charts/chart_type_bar: c11d460595d3ddfe8efd67ac068574c5 + environments/analysis/charts/chart_type_big_number: 9d17fb96241507c955dca25e143ae67a + environments/analysis/charts/chart_type_line: f42dd53238ed4d44def306a61d47d5c4 + environments/analysis/charts/chart_type_pie: 068a797404233ccf68d07ad63af7b50c + environments/analysis/charts/create_chart: ca7fdcc964e01f42ea9709924221edba + environments/analysis/charts/delete_chart_confirmation: f7fd7b0a08e81c9b392b08c9c1ad2147 + environments/analysis/charts/no_charts_found: d4a27d5b56e49ebdd38bf28791dbcc42 + environments/analysis/charts/open_options: 2c6a35fec9b9d008e41728594bcd07d7 environments/analysis/dashboards/create_dashboard: 9396aec1ea4a9b05ada94483655d1373 environments/analysis/dashboards/create_dashboard_description: d29f60615f6d8c96cc4265541e75ec26 environments/analysis/dashboards/create_failed: 7b58f15568047a35220b3a47cc3b0f71 diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 724aea290e..e8982b84f7 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -150,6 +150,7 @@ "bottom_right": "Unten rechts", "cancel": "Abbrechen", "centered_modal": "Zentriertes Modalfenster", + "chart": "Diagramm", "charts": "Diagramme", "choices": "Entscheidungen", "choose_environment": "Umgebung auswählen", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Ihre Umfrage wäre nicht angezeigt." }, "analysis": { + "charts": { + "action_coming_soon": "Kommt bald", + "chart_deleted_successfully": "Diagramm erfolgreich gelöscht", + "chart_deletion_error": "Diagramm konnte nicht gelöscht werden", + "chart_duplicated_successfully": "Diagramm erfolgreich dupliziert", + "chart_duplication_error": "Diagramm konnte nicht dupliziert werden", + "chart_type_area": "Flächendiagramm", + "chart_type_bar": "Balkendiagramm", + "chart_type_big_number": "Große Zahl", + "chart_type_line": "Liniendiagramm", + "chart_type_pie": "Kreisdiagramm", + "create_chart": "Diagramm erstellen", + "delete_chart_confirmation": "Bist du sicher, dass du dieses Diagramm löschen möchtest?", + "no_charts_found": "Keine Diagramme gefunden.", + "open_options": "Diagrammoptionen öffnen" + }, "dashboards": { "create_dashboard": "Dashboard erstellen", "create_dashboard_description": "Gib einen Namen für dein neues Dashboard ein.", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index a87117513e..0151452ab6 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -150,6 +150,7 @@ "bottom_right": "Bottom Right", "cancel": "Cancel", "centered_modal": "Centered Modal", + "chart": "Chart", "charts": "Charts", "choices": "Choices", "choose_environment": "Choose environment", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Your survey would not be shown." }, "analysis": { + "charts": { + "action_coming_soon": "Coming soon", + "chart_deleted_successfully": "Chart deleted successfully", + "chart_deletion_error": "Failed to delete chart", + "chart_duplicated_successfully": "Chart duplicated successfully", + "chart_duplication_error": "Failed to duplicate chart", + "chart_type_area": "Area Chart", + "chart_type_bar": "Bar Chart", + "chart_type_big_number": "Big Number", + "chart_type_line": "Line Chart", + "chart_type_pie": "Pie Chart", + "create_chart": "Create Chart", + "delete_chart_confirmation": "Are you sure you want to delete this chart?", + "no_charts_found": "No charts found.", + "open_options": "Open chart options" + }, "dashboards": { "create_dashboard": "Create Dashboard", "create_dashboard_description": "Enter a name for your new dashboard.", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index ad879ee5d9..fddc819817 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -150,6 +150,7 @@ "bottom_right": "Inferior derecha", "cancel": "Cancelar", "centered_modal": "Modal centrado", + "chart": "Gráfico", "charts": "Gráficos", "choices": "Opciones", "choose_environment": "Elegir entorno", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Tu encuesta no se mostraría." }, "analysis": { + "charts": { + "action_coming_soon": "Próximamente", + "chart_deleted_successfully": "Gráfico eliminado correctamente", + "chart_deletion_error": "Error al eliminar el gráfico", + "chart_duplicated_successfully": "Gráfico duplicado correctamente", + "chart_duplication_error": "Error al duplicar el gráfico", + "chart_type_area": "Gráfico de área", + "chart_type_bar": "Gráfico de barras", + "chart_type_big_number": "Número grande", + "chart_type_line": "Gráfico de líneas", + "chart_type_pie": "Gráfico circular", + "create_chart": "Crear gráfico", + "delete_chart_confirmation": "¿Estás seguro de que quieres eliminar este gráfico?", + "no_charts_found": "No se encontraron gráficos.", + "open_options": "Abrir opciones del gráfico" + }, "dashboards": { "create_dashboard": "Crear panel de control", "create_dashboard_description": "Introduce un nombre para tu panel de control nuevo.", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index ff23cacc8e..4c33a92266 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -112,7 +112,6 @@ "link_expired_description": "Le lien que vous avez utilisé n'est plus valide." }, "common": { - "Filter": "Filtrer", "accepted": "Accepté", "account": "Compte", "account_settings": "Paramètres du compte", @@ -151,6 +150,7 @@ "bottom_right": "En bas à droite", "cancel": "Annuler", "centered_modal": "Au centre", + "chart": "Graphique", "charts": "Graphiques", "choices": "Choix", "choose_environment": "Choisir l'environnement", @@ -621,6 +621,22 @@ "your_survey_would_not_be_shown": "Votre enquête ne serait pas affichée." }, "analysis": { + "charts": { + "action_coming_soon": "À venir bientôt", + "chart_deleted_successfully": "Graphique supprimé avec succès", + "chart_deletion_error": "Échec de la suppression du graphique", + "chart_duplicated_successfully": "Graphique dupliqué avec succès", + "chart_duplication_error": "Échec de la duplication du graphique", + "chart_type_area": "Graphique en aires", + "chart_type_bar": "Graphique à barres", + "chart_type_big_number": "Grand nombre", + "chart_type_line": "Graphique linéaire", + "chart_type_pie": "Graphique circulaire", + "create_chart": "Créer un graphique", + "delete_chart_confirmation": "Êtes-vous sûr de vouloir supprimer ce graphique ?", + "no_charts_found": "Aucun graphique trouvé.", + "open_options": "Ouvrir les options du graphique" + }, "dashboards": { "create_dashboard": "Créer un tableau de bord", "create_dashboard_description": "Saisissez un nom pour votre nouveau tableau de bord.", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 61e5ffe719..3e0b4b66cc 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -150,6 +150,7 @@ "bottom_right": "Jobbra lent", "cancel": "Mégse", "centered_modal": "Középre helyezett kizárólagos", + "chart": "Diagram", "charts": "Diagramok", "choices": "Választási lehetőségek", "choose_environment": "Környezet kiválasztása", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "A kérdőív nem jelenne meg." }, "analysis": { + "charts": { + "action_coming_soon": "Hamarosan", + "chart_deleted_successfully": "A diagram sikeresen törölve", + "chart_deletion_error": "A diagram törlése sikertelen", + "chart_duplicated_successfully": "A diagram sikeresen duplikálva", + "chart_duplication_error": "A diagram duplikálása sikertelen", + "chart_type_area": "Területdiagram", + "chart_type_bar": "Oszlopdiagram", + "chart_type_big_number": "Nagy szám", + "chart_type_line": "Vonaldiagram", + "chart_type_pie": "Kördiagram", + "create_chart": "Diagram létrehozása", + "delete_chart_confirmation": "Biztosan törölni szeretnéd ezt a diagramot?", + "no_charts_found": "Nem található diagram.", + "open_options": "Diagram beállításainak megnyitása" + }, "dashboards": { "create_dashboard": "Vezérlőpult létrehozása", "create_dashboard_description": "Adjon nevet az új vezérlőpultnak.", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index 91e4697aa3..494fedc67c 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -150,6 +150,7 @@ "bottom_right": "右下", "cancel": "キャンセル", "centered_modal": "中央モーダル", + "chart": "チャート", "charts": "チャート", "choices": "選択肢", "choose_environment": "環境を選択", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "あなたのフォームは表示されません。" }, "analysis": { + "charts": { + "action_coming_soon": "近日公開", + "chart_deleted_successfully": "チャートを削除しました", + "chart_deletion_error": "チャートの削除に失敗しました", + "chart_duplicated_successfully": "チャートを複製しました", + "chart_duplication_error": "チャートの複製に失敗しました", + "chart_type_area": "エリアチャート", + "chart_type_bar": "棒グラフ", + "chart_type_big_number": "大きな数値", + "chart_type_line": "折れ線グラフ", + "chart_type_pie": "円グラフ", + "create_chart": "チャートを作成", + "delete_chart_confirmation": "このチャートを削除してもよろしいですか?", + "no_charts_found": "チャートが見つかりません。", + "open_options": "チャートオプションを開く" + }, "dashboards": { "create_dashboard": "ダッシュボードを作成", "create_dashboard_description": "新しいダッシュボードの名前を入力してください。", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index 38b02e0094..afb4cee5a7 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -150,6 +150,7 @@ "bottom_right": "Rechtsonder", "cancel": "Annuleren", "centered_modal": "Gecentreerd modaal", + "chart": "Grafiek", "charts": "Grafieken", "choices": "Keuzes", "choose_environment": "Kies omgeving", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Uw enquête wordt niet getoond." }, "analysis": { + "charts": { + "action_coming_soon": "Binnenkort beschikbaar", + "chart_deleted_successfully": "Grafiek succesvol verwijderd", + "chart_deletion_error": "Verwijderen van grafiek mislukt", + "chart_duplicated_successfully": "Grafiek succesvol gedupliceerd", + "chart_duplication_error": "Dupliceren van grafiek mislukt", + "chart_type_area": "Vlakdiagram", + "chart_type_bar": "Staafdiagram", + "chart_type_big_number": "Groot getal", + "chart_type_line": "Lijndiagram", + "chart_type_pie": "Cirkeldiagram", + "create_chart": "Diagram maken", + "delete_chart_confirmation": "Weet je zeker dat je deze grafiek wilt verwijderen?", + "no_charts_found": "Geen diagrammen gevonden.", + "open_options": "Open diagramopties" + }, "dashboards": { "create_dashboard": "Dashboard creëren", "create_dashboard_description": "Voer een naam in voor je nieuwe dashboard.", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index a5e0a02aef..4d58ddd62b 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -150,6 +150,7 @@ "bottom_right": "Canto Inferior Direito", "cancel": "Cancelar", "centered_modal": "Modal Centralizado", + "chart": "Gráfico", "charts": "Gráficos", "choices": "Escolhas", "choose_environment": "Escolher ambiente", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Sua pesquisa não seria exibida." }, "analysis": { + "charts": { + "action_coming_soon": "Em breve", + "chart_deleted_successfully": "Gráfico excluído com sucesso", + "chart_deletion_error": "Falha ao excluir gráfico", + "chart_duplicated_successfully": "Gráfico duplicado com sucesso", + "chart_duplication_error": "Falha ao duplicar gráfico", + "chart_type_area": "Gráfico de área", + "chart_type_bar": "Gráfico de barras", + "chart_type_big_number": "Número grande", + "chart_type_line": "Gráfico de linhas", + "chart_type_pie": "Gráfico de pizza", + "create_chart": "Criar gráfico", + "delete_chart_confirmation": "Tem certeza de que deseja excluir este gráfico?", + "no_charts_found": "Nenhum gráfico encontrado.", + "open_options": "Abrir opções do gráfico" + }, "dashboards": { "create_dashboard": "Criar painel", "create_dashboard_description": "Digite um nome para o seu novo painel.", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 0ade50ac37..8354adae0b 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -150,6 +150,7 @@ "bottom_right": "Inferior Direito", "cancel": "Cancelar", "centered_modal": "Modal Centralizado", + "chart": "Gráfico", "charts": "Gráficos", "choices": "Escolhas", "choose_environment": "Escolha o ambiente", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "O seu inquérito não seria mostrado." }, "analysis": { + "charts": { + "action_coming_soon": "Em breve", + "chart_deleted_successfully": "Gráfico eliminado com sucesso", + "chart_deletion_error": "Falha ao eliminar gráfico", + "chart_duplicated_successfully": "Gráfico duplicado com sucesso", + "chart_duplication_error": "Falha ao duplicar gráfico", + "chart_type_area": "Gráfico de área", + "chart_type_bar": "Gráfico de barras", + "chart_type_big_number": "Número grande", + "chart_type_line": "Gráfico de linhas", + "chart_type_pie": "Gráfico circular", + "create_chart": "Criar gráfico", + "delete_chart_confirmation": "Tens a certeza de que queres eliminar este gráfico?", + "no_charts_found": "Nenhum gráfico encontrado.", + "open_options": "Abrir opções do gráfico" + }, "dashboards": { "create_dashboard": "Criar painel", "create_dashboard_description": "Introduza um nome para o seu novo painel.", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 8284dabc9a..7499bdcf7e 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -150,6 +150,7 @@ "bottom_right": "Dreapta Jos", "cancel": "Anulare", "centered_modal": "Modală centralizată", + "chart": "Grafic", "charts": "Grafice", "choices": "Alegeri", "choose_environment": "Alege mediul", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Sondajul dumneavoastră nu va fi afișat." }, "analysis": { + "charts": { + "action_coming_soon": "În curând", + "chart_deleted_successfully": "Graficul a fost șters cu succes", + "chart_deletion_error": "Nu s-a putut șterge graficul", + "chart_duplicated_successfully": "Graficul a fost duplicat cu succes", + "chart_duplication_error": "Nu s-a putut duplica graficul", + "chart_type_area": "Grafic de tip arie", + "chart_type_bar": "Grafic de tip bară", + "chart_type_big_number": "Număr mare", + "chart_type_line": "Grafic de tip linie", + "chart_type_pie": "Grafic de tip plăcintă", + "create_chart": "Creează grafic", + "delete_chart_confirmation": "Ești sigur că vrei să ștergi acest grafic?", + "no_charts_found": "Nu s-au găsit grafice.", + "open_options": "Deschide opțiunile graficului" + }, "dashboards": { "create_dashboard": "Creează tablou de bord", "create_dashboard_description": "Introdu un nume pentru noul tău tablou de bord.", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index e647dcbd23..63e7f11d05 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -112,7 +112,6 @@ "link_expired_description": "Ссылка, которой вы воспользовались, больше не действительна." }, "common": { - "Filter": "Фильтр", "accepted": "Принято", "account": "Аккаунт", "account_settings": "Настройки аккаунта", @@ -151,6 +150,7 @@ "bottom_right": "Внизу справа", "cancel": "Отмена", "centered_modal": "Центрированное модальное окно", + "chart": "График", "charts": "Графики", "choices": "Варианты", "choose_environment": "Выберите среду", @@ -621,6 +621,22 @@ "your_survey_would_not_be_shown": "Ваш опрос не будет отображаться." }, "analysis": { + "charts": { + "action_coming_soon": "Скоро будет", + "chart_deleted_successfully": "График успешно удалён", + "chart_deletion_error": "Не удалось удалить график", + "chart_duplicated_successfully": "График успешно дублирован", + "chart_duplication_error": "Не удалось дублировать график", + "chart_type_area": "График областью", + "chart_type_bar": "Столбчатая диаграмма", + "chart_type_big_number": "Большое число", + "chart_type_line": "Линейный график", + "chart_type_pie": "Круговая диаграмма", + "create_chart": "Создать график", + "delete_chart_confirmation": "Ты уверен, что хочешь удалить этот график?", + "no_charts_found": "Графики не найдены.", + "open_options": "Открыть настройки графика" + }, "dashboards": { "create_dashboard": "Создать панель управления", "create_dashboard_description": "Введите название для новой панели управления.", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index 09eaf5c732..4aae9a5a92 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -150,6 +150,7 @@ "bottom_right": "Nedre höger", "cancel": "Avbryt", "centered_modal": "Centrerad modal", + "chart": "Diagram", "charts": "Diagram", "choices": "Val", "choose_environment": "Välj miljö", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "Din enkät skulle inte visas." }, "analysis": { + "charts": { + "action_coming_soon": "Kommer snart", + "chart_deleted_successfully": "Diagrammet har tagits bort", + "chart_deletion_error": "Det gick inte att ta bort diagrammet", + "chart_duplicated_successfully": "Diagrammet har duplicerats", + "chart_duplication_error": "Det gick inte att duplicera diagrammet", + "chart_type_area": "Ytdiagram", + "chart_type_bar": "Stapeldiagram", + "chart_type_big_number": "Stort tal", + "chart_type_line": "Linjediagram", + "chart_type_pie": "Cirkeldiagram", + "create_chart": "Skapa diagram", + "delete_chart_confirmation": "Är du säker på att du vill ta bort det här diagrammet?", + "no_charts_found": "Inga diagram hittades.", + "open_options": "Öppna diagramalternativ" + }, "dashboards": { "create_dashboard": "Skapa instrumentpanel", "create_dashboard_description": "Ange ett namn för din nya instrumentpanel.", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 768550b189..6b4e56fa05 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -150,6 +150,7 @@ "bottom_right": "右下", "cancel": "取消", "centered_modal": "居中 模态", + "chart": "图表", "charts": "图表", "choices": "选项", "choose_environment": "选择 环境", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "您的 调查 不会 显示。" }, "analysis": { + "charts": { + "action_coming_soon": "即将推出", + "chart_deleted_successfully": "图表删除成功", + "chart_deletion_error": "图表删除失败", + "chart_duplicated_successfully": "图表复制成功", + "chart_duplication_error": "图表复制失败", + "chart_type_area": "面积图", + "chart_type_bar": "柱状图", + "chart_type_big_number": "大数字", + "chart_type_line": "折线图", + "chart_type_pie": "饼图", + "create_chart": "创建图表", + "delete_chart_confirmation": "你确定要删除这个图表吗?", + "no_charts_found": "未找到图表。", + "open_options": "打开图表选项" + }, "dashboards": { "create_dashboard": "创建 Dashboard", "create_dashboard_description": "请输入新 Dashboard 的名称。", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 6fcb352159..6a96fa577a 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -150,6 +150,7 @@ "bottom_right": "右下", "cancel": "取消", "centered_modal": "置中彈窗", + "chart": "圖表", "charts": "圖表", "choices": "選項", "choose_environment": "選擇環境", @@ -620,6 +621,22 @@ "your_survey_would_not_be_shown": "您的問卷將不會顯示。" }, "analysis": { + "charts": { + "action_coming_soon": "即將推出", + "chart_deleted_successfully": "圖表已成功刪除", + "chart_deletion_error": "刪除圖表失敗", + "chart_duplicated_successfully": "圖表已成功複製", + "chart_duplication_error": "圖表複製失敗", + "chart_type_area": "區域圖", + "chart_type_bar": "長條圖", + "chart_type_big_number": "大數字", + "chart_type_line": "折線圖", + "chart_type_pie": "圓餅圖", + "create_chart": "建立圖表", + "delete_chart_confirmation": "你確定要刪除此圖表嗎?", + "no_charts_found": "找不到圖表。", + "open_options": "開啟圖表選項" + }, "dashboards": { "create_dashboard": "建立儀表板", "create_dashboard_description": "請輸入新儀表板的名稱。", diff --git a/apps/web/modules/ee/analysis/charts/components/chart-dropdown-menu.tsx b/apps/web/modules/ee/analysis/charts/components/chart-dropdown-menu.tsx new file mode 100644 index 0000000000..95482ec660 --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/components/chart-dropdown-menu.tsx @@ -0,0 +1,125 @@ +"use client"; + +import { CopyIcon, MoreVertical, SquarePenIcon, TrashIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { deleteChartAction, duplicateChartAction } from "@/modules/ee/analysis/charts/actions"; +import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis"; +import { Button } from "@/modules/ui/components/button"; +import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/modules/ui/components/dropdown-menu"; + +interface ChartDropdownMenuProps { + environmentId: string; + chart: TChartWithCreator; +} + +export function ChartDropdownMenu({ environmentId, chart }: Readonly) { + const { t } = useTranslation(); + const router = useRouter(); + const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isDuplicating, setIsDuplicating] = useState(false); + const [isDropDownOpen, setIsDropDownOpen] = useState(false); + + const handleDuplicate = async () => { + setIsDuplicating(true); + try { + const result = await duplicateChartAction({ environmentId, chartId: chart.id }); + if (result?.data) { + toast.success(t("environments.analysis.charts.chart_duplicated_successfully")); + router.refresh(); + } else { + toast.error( + getFormattedErrorMessage(result) || t("environments.analysis.charts.chart_duplication_error") + ); + } + } catch { + toast.error(t("environments.analysis.charts.chart_duplication_error")); + } finally { + setIsDuplicating(false); + } + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + const result = await deleteChartAction({ environmentId, chartId: chart.id }); + if (result?.data) { + toast.success(t("environments.analysis.charts.chart_deleted_successfully")); + setDeleteDialogOpen(false); + router.refresh(); + } else { + const msg = + getFormattedErrorMessage(result) || t("environments.analysis.charts.chart_deletion_error"); + toast.error(msg); + } + } catch { + toast.error(t("common.something_went_wrong_please_try_again")); + } finally { + setIsDeleting(false); + } + }; + + const handleEdit = () => { + toast(t("environments.analysis.charts.action_coming_soon")); + }; + + return ( +
+ + + + + + + } onClick={handleEdit}> + {t("common.edit")} + + + } + onClick={() => { + setIsDropDownOpen(false); + handleDuplicate(); + }} + disabled={isDuplicating}> + {t("common.duplicate")} + + + } + onClick={() => { + setIsDropDownOpen(false); + setDeleteDialogOpen(true); + }} + disabled={isDeleting}> + {t("common.delete")} + + + + + + +
+ ); +} diff --git a/apps/web/modules/ee/analysis/charts/components/chart-row.tsx b/apps/web/modules/ee/analysis/charts/components/chart-row.tsx new file mode 100644 index 0000000000..e6a29a3ff4 --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/components/chart-row.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { BarChart3Icon } from "lucide-react"; +import { convertDateString, timeSinceDate } from "@/lib/time"; +import { ChartDropdownMenu } from "@/modules/ee/analysis/charts/components/chart-dropdown-menu"; +import { CHART_TYPE_ICONS } from "@/modules/ee/analysis/charts/lib/chart-types"; +import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis"; + +interface ChartRowProps { + chart: TChartWithCreator; + environmentId: string; + isReadOnly: boolean; +} + +export function ChartRow({ chart, environmentId, isReadOnly }: Readonly) { + const IconComponent = CHART_TYPE_ICONS[chart.type as keyof typeof CHART_TYPE_ICONS] ?? BarChart3Icon; + + return ( +
+
+
+
+
+ +
+
+
{chart.name}
+
+
+
+
+
{chart.creator?.name ?? "-"}
+
+
+
+ {convertDateString(chart.createdAt.toISOString())} +
+
+
+
{timeSinceDate(new Date(chart.updatedAt))}
+
+
+
+ {!isReadOnly && } +
+
+ ); +} diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx new file mode 100644 index 0000000000..7d5be19338 --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/components/charts-list-page.tsx @@ -0,0 +1,63 @@ +import { Delay } from "@suspensive/react"; +import { Suspense, use } from "react"; +import { getTranslate } from "@/lingodotdev/server"; +import { ChartsList } from "@/modules/ee/analysis/charts/components/charts-list"; +import { ChartsListSkeleton } from "@/modules/ee/analysis/charts/components/charts-list-skeleton"; +import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button"; +import { getChartsWithCreator } from "@/modules/ee/analysis/charts/lib/charts"; +import { AnalysisPageLayout } from "@/modules/ee/analysis/components/analysis-page-layout"; +import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis"; +import { getEnvironmentAuth } from "@/modules/environments/lib/utils"; + +interface ChartsListContentProps { + chartsPromise: Promise; + environmentId: string; + isReadOnly: boolean; +} + +const ChartsListContent = ({ + chartsPromise, + environmentId, + isReadOnly, +}: Readonly) => { + const charts = use(chartsPromise); + + return ; +}; + +interface ChartsListPageProps { + environmentId: string; +} + +export async function ChartsListPage({ environmentId }: Readonly) { + const t = await getTranslate(); + const { project, isReadOnly } = await getEnvironmentAuth(environmentId); + const chartsPromise = getChartsWithCreator(project.id); + + return ( + }> + + + + }> + + + + ); +} diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list-skeleton.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list-skeleton.tsx new file mode 100644 index 0000000000..8fb2dfa25f --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/components/charts-list-skeleton.tsx @@ -0,0 +1,43 @@ +const SKELETON_ROWS = 3; + +const SkeletonRow = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}; + +interface ChartsListSkeletonProps { + columnHeaders: [string, string, string, string]; +} + +export const ChartsListSkeleton = ({ columnHeaders }: Readonly) => { + return ( +
+
+
{columnHeaders[0]}
+
{columnHeaders[1]}
+
{columnHeaders[2]}
+
{columnHeaders[3]}
+
+
+ {Array.from({ length: SKELETON_ROWS }).map((_, i) => ( + + ))} +
+ ); +}; diff --git a/apps/web/modules/ee/analysis/charts/components/charts-list.tsx b/apps/web/modules/ee/analysis/charts/components/charts-list.tsx new file mode 100644 index 0000000000..a142defff6 --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/components/charts-list.tsx @@ -0,0 +1,34 @@ +import { getTranslate } from "@/lingodotdev/server"; +import { ChartRow } from "@/modules/ee/analysis/charts/components/chart-row"; +import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis"; + +interface ChartsListProps { + charts: TChartWithCreator[]; + environmentId: string; + isReadOnly: boolean; +} + +export const ChartsList = async ({ charts, environmentId, isReadOnly }: Readonly) => { + const t = await getTranslate(); + + return ( +
+
+
{t("common.title")}
+
{t("common.created_by")}
+
{t("common.created_at")}
+
{t("common.updated_at")}
+
+
+ {charts.length === 0 ? ( +

+ {t("environments.analysis.charts.no_charts_found")} +

+ ) : ( + charts.map((chart) => ( + + )) + )} +
+ ); +}; diff --git a/apps/web/modules/ee/analysis/charts/components/create-chart-button.tsx b/apps/web/modules/ee/analysis/charts/components/create-chart-button.tsx new file mode 100644 index 0000000000..1235210e44 --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/components/create-chart-button.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { PlusIcon } from "lucide-react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/modules/ui/components/button"; + +export function CreateChartButton() { + const { t } = useTranslation(); + + const handleClick = () => { + toast(t("environments.analysis.charts.action_coming_soon")); + }; + + return ( + + ); +} diff --git a/apps/web/modules/ee/analysis/charts/lib/chart-types.test.ts b/apps/web/modules/ee/analysis/charts/lib/chart-types.test.ts new file mode 100644 index 0000000000..0eca1f99d9 --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/lib/chart-types.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test, vi } from "vitest"; +import { CHART_TYPE_ICONS, getChartTypes } from "./chart-types"; + +describe("chart-types", () => { + test("CHART_TYPE_ICONS has all chart types", () => { + expect(Object.keys(CHART_TYPE_ICONS)).toEqual(["area", "bar", "line", "pie", "big_number"]); + }); + + test("getChartTypes returns chart types with translated labels", () => { + const t = vi.fn((key: string) => key); + const result = getChartTypes(t); + + expect(result).toHaveLength(5); + expect(result.map((r) => r.id)).toEqual(["area", "bar", "line", "pie", "big_number"]); + expect(t).toHaveBeenCalledWith("environments.analysis.charts.chart_type_area"); + expect(result[0].label).toBe("environments.analysis.charts.chart_type_area"); + }); +}); diff --git a/apps/web/modules/ee/analysis/charts/lib/chart-types.ts b/apps/web/modules/ee/analysis/charts/lib/chart-types.ts new file mode 100644 index 0000000000..ea5300560e --- /dev/null +++ b/apps/web/modules/ee/analysis/charts/lib/chart-types.ts @@ -0,0 +1,35 @@ +import type { TFunction } from "i18next"; +import { ActivityIcon, AreaChartIcon, BarChart3Icon, LineChartIcon, PieChartIcon } from "lucide-react"; +import type React from "react"; +import type { TChartType } from "@/modules/ee/analysis/types/analysis"; + +export const CHART_TYPE_ICONS: Record< + TChartType, + React.ComponentType<{ className?: string; strokeWidth?: number }> +> = { + area: AreaChartIcon, + bar: BarChart3Icon, + line: LineChartIcon, + pie: PieChartIcon, + big_number: ActivityIcon, +}; + +export function getChartTypes( + t: TFunction +): readonly { + id: TChartType; + icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; + label: string; +}[] { + return [ + { id: "area", icon: CHART_TYPE_ICONS.area, label: t("environments.analysis.charts.chart_type_area") }, + { id: "bar", icon: CHART_TYPE_ICONS.bar, label: t("environments.analysis.charts.chart_type_bar") }, + { id: "line", icon: CHART_TYPE_ICONS.line, label: t("environments.analysis.charts.chart_type_line") }, + { id: "pie", icon: CHART_TYPE_ICONS.pie, label: t("environments.analysis.charts.chart_type_pie") }, + { + id: "big_number", + icon: CHART_TYPE_ICONS.big_number, + label: t("environments.analysis.charts.chart_type_big_number"), + }, + ]; +} diff --git a/apps/web/modules/ee/analysis/charts/lib/charts.test.ts b/apps/web/modules/ee/analysis/charts/lib/charts.test.ts index 3ce9d748f9..90df49e381 100644 --- a/apps/web/modules/ee/analysis/charts/lib/charts.test.ts +++ b/apps/web/modules/ee/analysis/charts/lib/charts.test.ts @@ -380,4 +380,35 @@ describe("Chart Service", () => { }); }); }); + + describe("getChartsWithCreator", () => { + test("returns charts with creator info", async () => { + const chartsWithCreator = [ + { ...mockChart, creator: { name: "Alice" } }, + { ...mockChart, id: "chart-2", name: "Chart 2", creator: null }, + ]; + vi.mocked(prisma.chart.findMany).mockResolvedValue(chartsWithCreator as any); + const { getChartsWithCreator } = await import("./charts"); + + const result = await getChartsWithCreator(mockProjectId); + + expect(result).toEqual(chartsWithCreator); + expect(prisma.chart.findMany).toHaveBeenCalledWith({ + where: { projectId: mockProjectId }, + orderBy: { createdAt: "desc" }, + select: expect.objectContaining({ + creator: { select: { name: true } }, + }), + }); + }); + + test("throws DatabaseError on Prisma errors", async () => { + vi.mocked(prisma.chart.findMany).mockRejectedValue(makePrismaError("P9999")); + const { getChartsWithCreator } = await import("./charts"); + + await expect(getChartsWithCreator(mockProjectId)).rejects.toMatchObject({ + name: "DatabaseError", + }); + }); + }); }); diff --git a/apps/web/modules/ee/analysis/charts/lib/charts.ts b/apps/web/modules/ee/analysis/charts/lib/charts.ts index 2f77318c3b..ef1d3f88c0 100644 --- a/apps/web/modules/ee/analysis/charts/lib/charts.ts +++ b/apps/web/modules/ee/analysis/charts/lib/charts.ts @@ -10,6 +10,7 @@ import { TChart, TChartCreateInput, TChartUpdateInput, + TChartWithCreator, TChartWithWidgets, ZChartCreateInput, ZChartType, @@ -244,3 +245,25 @@ export const getCharts = async (projectId: string): Promise throw error; } }; + +export const getChartsWithCreator = async (projectId: string): Promise => { + validateInputs([projectId, ZId]); + + try { + return await prisma.chart.findMany({ + where: { projectId }, + orderBy: { createdAt: "desc" }, + select: { + ...selectChart, + creator: { + select: { name: true }, + }, + }, + }); + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } +}; diff --git a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx index 93157f3922..0dc71353ed 100644 --- a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx +++ b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx @@ -25,8 +25,11 @@ const DashboardsListContent = ({ return ; }; -export const DashboardsListPage = async (props: { params: Promise<{ environmentId: string }> }) => { - const { environmentId } = await props.params; +interface DashboardsListPageProps { + environmentId: string; +} + +export const DashboardsListPage = async ({ environmentId }: Readonly) => { const t = await getTranslate(); const { project, isReadOnly } = await getEnvironmentAuth(environmentId); diff --git a/apps/web/modules/ee/analysis/types/analysis.ts b/apps/web/modules/ee/analysis/types/analysis.ts index c9af94688f..bb3759adcc 100644 --- a/apps/web/modules/ee/analysis/types/analysis.ts +++ b/apps/web/modules/ee/analysis/types/analysis.ts @@ -41,6 +41,10 @@ export type TChartWithWidgets = TChart & { widgets: { dashboardId: string }[]; }; +export type TChartWithCreator = TChart & { + creator: { name: string } | null; +}; + // ── Dashboard input schemas ───────────────────────────────────────────────── export const ZDashboardCreateInput = z.object({