diff --git a/apps/web/modules/ee/analysis/charts/components/add-to-dashboard-dialog.tsx b/apps/web/modules/ee/analysis/charts/components/add-to-dashboard-dialog.tsx index 53a373a319..265178eb9e 100644 --- a/apps/web/modules/ee/analysis/charts/components/add-to-dashboard-dialog.tsx +++ b/apps/web/modules/ee/analysis/charts/components/add-to-dashboard-dialog.tsx @@ -31,6 +31,7 @@ interface AddToDashboardDialogProps { onDashboardSelect: (id: string) => void; onConfirm: () => void; isSaving: boolean; + showChartNameField?: boolean; } export function AddToDashboardDialog({ @@ -43,6 +44,7 @@ export function AddToDashboardDialog({ onDashboardSelect, onConfirm, isSaving, + showChartNameField = true, }: Readonly) { const { t } = useTranslation(); @@ -57,17 +59,19 @@ export function AddToDashboardDialog({
-
- - onChartNameChange(e.target.value)} - maxLength={255} - /> -
+ {showChartNameField && ( +
+ + onChartNameChange(e.target.value)} + maxLength={255} + /> +
+ )}
setChartName(event.target.value)} + placeholder={t("workspace.analysis.charts.chart_name_placeholder")} + maxLength={255} + required + />
- + {!isEditing && ( + <> + - {selectedChartType && ( +
+ + + )} + + + + {chartType && ( )} - {chartData && ( + {(isEditing || chartData) && (
- +
)} + ) : ( + +
+

{t("workspace.analysis.charts.no_data_source_available")}

+ + {t("workspace.analysis.charts.go_to_feedback_record_directories")} + +
+
)}
{chartData && ( - <> - setIsSaveDialogOpen(true)} - onAddToDashboardClick={() => setIsAddToDashboardDialogOpen(true)} - isSaving={isSaving} - /> - - - + )} diff --git a/apps/web/modules/ee/analysis/charts/components/edit-chart-view.tsx b/apps/web/modules/ee/analysis/charts/components/edit-chart-view.tsx deleted file mode 100644 index 7fc5a98589..0000000000 --- a/apps/web/modules/ee/analysis/charts/components/edit-chart-view.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { useTranslation } from "react-i18next"; -import { AddToDashboardDialog } from "@/modules/ee/analysis/charts/components/add-to-dashboard-dialog"; -import { AdvancedChartBuilder } from "@/modules/ee/analysis/charts/components/advanced-chart-builder"; -import { ChartDialogFooter } from "@/modules/ee/analysis/charts/components/chart-dialog-footer"; -import { ChartDialogLoadingView } from "@/modules/ee/analysis/charts/components/chart-dialog-loading-view"; -import { ChartPreview } from "@/modules/ee/analysis/charts/components/chart-preview"; -import { ManualChartBuilder } from "@/modules/ee/analysis/charts/components/manual-chart-builder"; -import { useChartDialog } from "@/modules/ee/analysis/charts/hooks/use-chart-dialog"; -import { DEFAULT_CHART_TYPE } from "@/modules/ee/analysis/charts/lib/chart-types"; -import type { TChartWithCreator } from "@/modules/ee/analysis/types/analysis"; -import { Button } from "@/modules/ui/components/button"; -import { - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/modules/ui/components/dialog"; -import { Input } from "@/modules/ui/components/input"; -import { Label } from "@/modules/ui/components/label"; - -interface EditChartViewProps { - open: boolean; - onOpenChange: (open: boolean) => void; - workspaceId: string; - chartId: string; - initialChart?: TChartWithCreator; - onSuccess?: () => void; - directories: { id: string; name: string }[]; -} - -export function EditChartView({ - open, - onOpenChange, - workspaceId, - chartId, - initialChart, - onSuccess, - directories, -}: Readonly) { - const { t } = useTranslation(); - - const { - chartData, - initialQuery, - isLoadingChart, - chartLoadError, - chartName, - setChartName, - selectedChartType, - handleChartTypeChange, - handleChartGenerated, - dashboards, - selectedDashboardId, - setSelectedDashboardId, - handleAddToDashboard, - handleSaveChart, - isSaving, - isAddToDashboardDialogOpen, - setIsAddToDashboardDialogOpen, - selectedDirectoryId, - handleClose, - } = useChartDialog({ open, onOpenChange, workspaceId, chartId, initialChart, onSuccess, directories }); - - if (isLoadingChart && !initialChart) { - return ; - } - - if (!isLoadingChart && !chartData && !initialChart && chartLoadError) { - return ( - !isOpen && handleClose()}> - - - {t("common.error")} - - - -
-

{chartLoadError}

- -
-
-
-
- ); - } - - const chartType = selectedChartType ?? DEFAULT_CHART_TYPE; - const directoryName = directories.find((d) => d.id === selectedDirectoryId)?.name; - - return ( - !isOpen && handleClose()}> - - - {t("workspace.analysis.charts.edit_chart_title")} - {t("workspace.analysis.charts.edit_chart_description")} - - -
-
- - setChartName(e.target.value)} - placeholder={t("workspace.analysis.charts.chart_name_placeholder")} - className="w-full" - /> -
- {directoryName && ( -
- -
- {directoryName} -
-
- )} -
- -
- - -
-
- setIsAddToDashboardDialogOpen(true)} - isSaving={isSaving} - /> - -
-
- ); -} diff --git a/apps/web/modules/ee/analysis/charts/hooks/use-chart-dialog.ts b/apps/web/modules/ee/analysis/charts/hooks/use-chart-dialog.ts index a247cd5597..9ee5ea42dc 100644 --- a/apps/web/modules/ee/analysis/charts/hooks/use-chart-dialog.ts +++ b/apps/web/modules/ee/analysis/charts/hooks/use-chart-dialog.ts @@ -26,6 +26,7 @@ export interface UseChartDialogProps { onOpenChange: (open: boolean) => void; workspaceId: string; chartId?: string; + autoAddToDashboardId?: string; /** Pre-loaded chart metadata; when provided for edit, skips getChartAction */ initialChart?: TChartWithCreator; onSuccess?: () => void; @@ -37,6 +38,7 @@ export function useChartDialog({ onOpenChange, workspaceId, chartId, + autoAddToDashboardId, initialChart, onSuccess, directories, @@ -45,7 +47,6 @@ export function useChartDialog({ const router = useRouter(); const [selectedChartType, setSelectedChartType] = useState(); const [chartData, setChartData] = useState(null); - const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false); const [isAddToDashboardDialogOpen, setIsAddToDashboardDialogOpen] = useState(false); const [chartName, setChartName] = useState(""); const [dashboards, setDashboards] = useState>([]); @@ -54,9 +55,7 @@ export function useChartDialog({ const [isLoadingChart, setIsLoadingChart] = useState(false); const [chartLoadError, setChartLoadError] = useState(null); const [currentChartId, setCurrentChartId] = useState(chartId); - const [selectedDirectoryId, setSelectedDirectoryId] = useState( - directories?.length === 1 ? directories[0].id : null - ); + const [selectedDirectoryId, setSelectedDirectoryId] = useState(directories?.[0]?.id ?? null); useEffect(() => { let cancelled = false; @@ -85,7 +84,7 @@ export function useChartDialog({ setChartName(""); setSelectedChartType(undefined); setCurrentChartId(undefined); - setSelectedDirectoryId(directories?.length === 1 ? directories[0].id : null); + setSelectedDirectoryId(directories?.[0]?.id ?? null); return; } @@ -159,11 +158,6 @@ export function useChartDialog({ const handleChartGenerated = (data: AnalyticsResponse) => { setChartData(data); - if (!currentChartId) { - setChartName( - data.chartType ? `${t("workspace.analysis.charts.chart")} ${new Date().toLocaleString()}` : "" - ); - } setSelectedChartType(data.chartType); }; @@ -180,6 +174,8 @@ export function useChartDialog({ setIsSaving(true); try { + let savedChartId = currentChartId; + if (currentChartId) { const result = await updateChartAction({ workspaceId, @@ -218,11 +214,32 @@ export function useChartDialog({ } setCurrentChartId(result.data.id); + savedChartId = result.data.id; toast.success(t("workspace.analysis.charts.chart_saved_successfully")); } - setIsSaveDialogOpen(false); + if (autoAddToDashboardId && savedChartId) { + const addResult = await addChartToDashboardAction({ + workspaceId, + chartId: savedChartId, + dashboardId: autoAddToDashboardId, + }); + + if (!addResult?.data) { + toast.error( + getFormattedErrorMessage(addResult) || + t("workspace.analysis.charts.failed_to_add_chart_to_dashboard") + ); + return; + } + + toast.success(t("workspace.analysis.charts.chart_added_to_dashboard")); + } + onOpenChange(false); + if (autoAddToDashboardId) { + router.push(`/workspaces/${workspaceId}/dashboards/${autoAddToDashboardId}`); + } router.refresh(); onSuccess?.(); } catch (error: unknown) { @@ -328,7 +345,7 @@ export function useChartDialog({ setSelectedChartType(undefined); setCurrentChartId(undefined); setChartLoadError(null); - setSelectedDirectoryId(directories?.length === 1 ? directories[0].id : null); + setSelectedDirectoryId(directories?.[0]?.id ?? null); onOpenChange(false); } }; @@ -349,8 +366,6 @@ export function useChartDialog({ setSelectedChartType, currentChartId, setCurrentChartId, - isSaveDialogOpen, - setIsSaveDialogOpen, isAddToDashboardDialogOpen, setIsAddToDashboardDialogOpen, dashboards, diff --git a/apps/web/modules/ee/analysis/components/no-feedback-records-state.tsx b/apps/web/modules/ee/analysis/components/no-feedback-records-state.tsx new file mode 100644 index 0000000000..30b0bf5fb9 --- /dev/null +++ b/apps/web/modules/ee/analysis/components/no-feedback-records-state.tsx @@ -0,0 +1,28 @@ +import { MessageSquareDashedIcon } from "lucide-react"; +import Link from "next/link"; +import { getTranslate } from "@/lingodotdev/server"; +import { Button } from "@/modules/ui/components/button"; + +interface NoFeedbackRecordsStateProps { + workspaceId: string; +} + +export const NoFeedbackRecordsState = async ({ workspaceId }: Readonly) => { + const t = await getTranslate(); + + return ( +
+
+ +

+ {t("workspace.analysis.no_feedback_records_message")} +

+ +
+
+ ); +}; diff --git a/apps/web/modules/ee/analysis/dashboards/actions.ts b/apps/web/modules/ee/analysis/dashboards/actions.ts index 06eea3c512..d153ecbc31 100644 --- a/apps/web/modules/ee/analysis/dashboards/actions.ts +++ b/apps/web/modules/ee/analysis/dashboards/actions.ts @@ -292,6 +292,9 @@ export const addChartToDashboardAction = authenticatedActionClient layout: parsedInput.layout, }); + revalidatePath(`/workspaces/${workspaceId}/dashboards`); + revalidatePath(`/workspaces/${workspaceId}/dashboards/${parsedInput.dashboardId}`); + ctx.auditLoggingCtx.organizationId = organizationId; ctx.auditLoggingCtx.workspaceId = workspaceId; ctx.auditLoggingCtx.dashboardWidgetId = widget.id; diff --git a/apps/web/modules/ee/analysis/dashboards/components/add-existing-charts-dialog.tsx b/apps/web/modules/ee/analysis/dashboards/components/add-existing-charts-dialog.tsx index 3e16554a57..f1278e4d9f 100644 --- a/apps/web/modules/ee/analysis/dashboards/components/add-existing-charts-dialog.tsx +++ b/apps/web/modules/ee/analysis/dashboards/components/add-existing-charts-dialog.tsx @@ -1,13 +1,14 @@ "use client"; import { Loader2Icon } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { getChartsAction } from "@/modules/ee/analysis/charts/actions"; +import { CreateChartButton } from "@/modules/ee/analysis/charts/components/create-chart-button"; import { addChartToDashboardAction } from "@/modules/ee/analysis/dashboards/actions"; -import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert"; import { Button } from "@/modules/ui/components/button"; import { Dialog, @@ -18,6 +19,7 @@ import { DialogHeader, DialogTitle, } from "@/modules/ui/components/dialog"; +import { Label } from "@/modules/ui/components/label"; import { MultiSelect } from "@/modules/ui/components/multi-select"; interface AddExistingChartsDialogProps { @@ -25,6 +27,7 @@ interface AddExistingChartsDialogProps { onOpenChange: (open: boolean) => void; workspaceId: string; dashboardId: string; + directories: { id: string; name: string }[]; existingChartIds: string[]; onSuccess: () => void; } @@ -39,39 +42,40 @@ export function AddExistingChartsDialog({ onOpenChange, workspaceId, dashboardId, + directories, existingChartIds, onSuccess, }: Readonly) { const { t } = useTranslation(); + const router = useRouter(); const [chartOptions, setChartOptions] = useState([]); const [selectedChartIds, setSelectedChartIds] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isAdding, setIsAdding] = useState(false); + const loadCharts = useCallback(async () => { + setIsLoading(true); + setSelectedChartIds([]); + try { + const result = await getChartsAction({ workspaceId }); + if (result?.data) { + const availableCharts = result.data.filter((chart) => !existingChartIds.includes(chart.id)); + setChartOptions(availableCharts.map((chart) => ({ value: chart.id, label: chart.name }))); + } else { + const errorMessage = getFormattedErrorMessage(result); + toast.error(errorMessage); + } + } catch { + toast.error(t("workspace.analysis.dashboards.charts_load_failed")); + } finally { + setIsLoading(false); + } + }, [workspaceId, existingChartIds, t]); + useEffect(() => { if (!open) return; - - const loadCharts = async () => { - setIsLoading(true); - setSelectedChartIds([]); - try { - const result = await getChartsAction({ workspaceId }); - if (result?.data) { - const availableCharts = result.data.filter((chart) => !existingChartIds.includes(chart.id)); - setChartOptions(availableCharts.map((chart) => ({ value: chart.id, label: chart.name }))); - } else { - const errorMessage = getFormattedErrorMessage(result); - toast.error(errorMessage); - } - } catch { - toast.error(t("workspace.analysis.dashboards.charts_load_failed")); - } finally { - setIsLoading(false); - } - }; - loadCharts(); - }, [open, workspaceId, existingChartIds, t]); + }, [open, loadCharts]); const handleAdd = async () => { if (selectedChartIds.length === 0) return; @@ -127,15 +131,8 @@ export function AddExistingChartsDialog({
) : ( - <> - {chartOptions.length === 0 && ( - - {t("workspace.analysis.dashboards.no_charts_to_add_message")} - - {t("workspace.analysis.dashboards.no_charts_available_description")} - - - )} +
+ - +
)}
- - - + + { + onOpenChange(false); + router.refresh(); + onSuccess(); + }} + buttonProps={{ variant: "secondary", size: "default", disabled: isAdding }} + /> +
+ + +
diff --git a/apps/web/modules/ee/analysis/dashboards/components/create-dashboard-button.tsx b/apps/web/modules/ee/analysis/dashboards/components/create-dashboard-button.tsx index 81a9ade7ae..432060ef0c 100644 --- a/apps/web/modules/ee/analysis/dashboards/components/create-dashboard-button.tsx +++ b/apps/web/modules/ee/analysis/dashboards/components/create-dashboard-button.tsx @@ -12,9 +12,13 @@ import { Button } from "@/modules/ui/components/button"; interface CreateDashboardButtonProps { workspaceId: string; + disabled?: boolean; } -export const CreateDashboardButton = ({ workspaceId }: Readonly) => { +export const CreateDashboardButton = ({ + workspaceId, + disabled = false, +}: Readonly) => { const { t } = useTranslation(); const router = useRouter(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); @@ -59,7 +63,7 @@ export const CreateDashboardButton = ({ workspaceId }: Readonly - diff --git a/apps/web/modules/ee/analysis/dashboards/components/dashboard-detail-client.tsx b/apps/web/modules/ee/analysis/dashboards/components/dashboard-detail-client.tsx index 6ab00af010..7958332601 100644 --- a/apps/web/modules/ee/analysis/dashboards/components/dashboard-detail-client.tsx +++ b/apps/web/modules/ee/analysis/dashboards/components/dashboard-detail-client.tsx @@ -10,6 +10,7 @@ import { useTranslation } from "react-i18next"; import "react-resizable/css/styles.css"; import type { TChartQuery } from "@formbricks/types/analysis"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { CreateChartDialog } from "@/modules/ee/analysis/charts/components/create-chart-dialog"; import { DashboardControlBar } from "@/modules/ee/analysis/dashboards/components/dashboard-control-bar"; import { DashboardPageHeader } from "@/modules/ee/analysis/dashboards/components/dashboard-page-header"; import { DashboardWidget } from "@/modules/ee/analysis/dashboards/components/dashboard-widget"; @@ -114,17 +115,26 @@ const MemoizedWidgetItem = memo(function WidgetItem({ widget, isEditing, dataPromise, + onEdit, + onResize, onRemove, }: Readonly<{ widget: TDashboardWidget; isEditing: boolean; dataPromise?: Promise<{ data: TChartDataRow[]; query: TChartQuery } | { error: string }>; + onEdit?: () => void; + onResize?: () => void; onRemove?: () => void; }>) { const title = widget.chart.name; return ( - + ); @@ -142,6 +152,7 @@ export function DashboardDetailClient({ const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); + const [editingChartId, setEditingChartId] = useState(null); const [, startTransition] = useTransition(); const [name, setName] = useState(dashboard.name); @@ -171,6 +182,32 @@ export function DashboardDetailClient({ [dashboard.widgets] ); + const handleEnterEditMode = useCallback(() => { + if (isEditing) { + return; + } + + setDraftWidgets((current) => current ?? dashboard.widgets); + setIsEditing(true); + }, [dashboard.widgets, isEditing]); + + const handleEditChart = useCallback((chartId: string) => { + setEditingChartId(chartId); + }, []); + + const handleRemoveWidgetFromMenu = useCallback( + (widgetId: string) => { + if (!isEditing) { + setDraftWidgets((current) => (current ?? dashboard.widgets).filter((w) => w.id !== widgetId)); + setIsEditing(true); + return; + } + + handleRemoveWidget(widgetId); + }, + [dashboard.widgets, handleRemoveWidget, isEditing] + ); + const handleCancel = useCallback(() => { setName(dashboard.name); setDraftWidgets(null); @@ -296,7 +333,9 @@ export function DashboardDetailClient({ widget={widget} isEditing={isEditing} dataPromise={widgetDataPromises.get(widget.id)} - onRemove={isEditing ? () => handleRemoveWidget(widget.id) : undefined} + onEdit={isReadOnly ? undefined : () => handleEditChart(widget.chartId)} + onResize={isReadOnly ? undefined : handleEnterEditMode} + onRemove={isReadOnly ? undefined : () => handleRemoveWidgetFromMenu(widget.id)} /> ))} @@ -305,6 +344,23 @@ export function DashboardDetailClient({ )} + {!isReadOnly && ( + { + if (!open) { + setEditingChartId(null); + } + }} + workspaceId={workspaceId} + chartId={editingChartId ?? undefined} + onSuccess={() => { + setEditingChartId(null); + router.refresh(); + }} + directories={directories} + /> + )} ); } diff --git a/apps/web/modules/ee/analysis/dashboards/components/dashboard-widget.tsx b/apps/web/modules/ee/analysis/dashboards/components/dashboard-widget.tsx index abc96f4ce1..cdee0a07f3 100644 --- a/apps/web/modules/ee/analysis/dashboards/components/dashboard-widget.tsx +++ b/apps/web/modules/ee/analysis/dashboards/components/dashboard-widget.tsx @@ -1,6 +1,6 @@ "use client"; -import { MoreVerticalIcon, TrashIcon } from "lucide-react"; +import { Maximize2Icon, MoreVerticalIcon, SquarePenIcon, TrashIcon } from "lucide-react"; import { ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; import { cn } from "@/lib/cn"; @@ -15,12 +15,22 @@ interface DashboardWidgetProps { title: string; children: ReactNode; isEditing?: boolean; + onEdit?: () => void; + onResize?: () => void; onRemove?: () => void; } -export function DashboardWidget({ title, children, isEditing, onRemove }: Readonly) { +export function DashboardWidget({ + title, + children, + isEditing, + onEdit, + onResize, + onRemove, +}: Readonly) { const { t } = useTranslation(); const [menuOpen, setMenuOpen] = useState(false); + const hasMenuActions = Boolean(onEdit || onResize || onRemove); return (

{title}

- {onRemove && ( + {hasMenuActions && (