From ddd2d5e983cfa5ef2c71ee287ea8dd0cc4e9081c Mon Sep 17 00:00:00 2001 From: Dhruwang Date: Tue, 12 May 2026 12:51:46 +0530 Subject: [PATCH] fix(dashboards): chart removal flow + add-charts dialog focus ring (ENG-901, ENG-903) ENG-901: clicking Remove on a chart widget no longer enters dashboard edit mode (which surfaced the rename input) and saving with the last widget removed no longer surfaces a raw "widgets: Too small" zod error. Out of edit mode, Remove now goes through a DeleteDialog -> dedicated removeWidgetFromDashboardAction. The batched update path also allows an empty widgets array now that the lib already supports deleting all widgets correctly. ENG-903: add p-1 to AddExistingChartsDialog DialogBody so the focus ring on the inner search input is no longer clipped by the body's overflow boundary. --- apps/web/i18n.lock | 1 + apps/web/locales/de-DE.json | 1 + apps/web/locales/en-US.json | 1 + apps/web/locales/es-ES.json | 1 + apps/web/locales/fr-FR.json | 1 + apps/web/locales/hu-HU.json | 1 + apps/web/locales/ja-JP.json | 1 + apps/web/locales/nl-NL.json | 1 + apps/web/locales/pt-BR.json | 1 + apps/web/locales/pt-PT.json | 1 + apps/web/locales/ro-RO.json | 1 + apps/web/locales/ru-RU.json | 1 + apps/web/locales/sv-SE.json | 1 + apps/web/locales/tr-TR.json | 1 + apps/web/locales/zh-Hans-CN.json | 1 + apps/web/locales/zh-Hant-TW.json | 1 + .../modules/ee/analysis/dashboards/actions.ts | 58 ++++++++++++++++--- .../components/add-existing-charts-dialog.tsx | 2 +- .../components/dashboard-detail-client.tsx | 51 +++++++++++++--- .../dashboards/lib/dashboards.test.ts | 33 +++++++++++ .../ee/analysis/dashboards/lib/dashboards.ts | 30 ++++++++++ 21 files changed, 173 insertions(+), 17 deletions(-) diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index cdce0dc404..b54b760ad5 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -1743,6 +1743,7 @@ checksums: workspace/analysis/charts/time_dimension_toggle_description: 77251d8b3b564390bad8b76f56905190 workspace/analysis/charts/update_chart: 7d9223335d9f0c5938ec30356d7034a9 workspace/analysis/dashboards/add_count_charts: b4ee1f29efce0bb380a060e0bc5d64fa + workspace/analysis/dashboards/chart_removed: 1ce20b8ee0b56bcd7d6fea2b5c1ae9fd workspace/analysis/dashboards/charts_add_failed: c4fda79ede798ab6747a989f083a0947 workspace/analysis/dashboards/charts_add_partial_failure: b1a9fc6fe18ab20fe16c16e91a05c195 workspace/analysis/dashboards/charts_added_to_dashboard: 917abab80231adf5af51e812352fc77b diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 0259a994f4..ed9899ac7a 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "{count} Diagramm(e) hinzufügen", + "chart_removed": "Diagramm vom Dashboard entfernt", "charts_add_failed": "Diagramme konnten nicht zum Dashboard hinzugefügt werden", "charts_add_partial_failure": "{count} Diagramm(e) konnten nicht hinzugefügt werden", "charts_added_to_dashboard": "Diagramme zum Dashboard hinzugefügt", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 04a0b73c77..bca8f42103 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Add {count} chart(s)", + "chart_removed": "Chart removed from dashboard", "charts_add_failed": "Failed to add charts to dashboard", "charts_add_partial_failure": "Failed to add {count} chart(s)", "charts_added_to_dashboard": "Charts added to dashboard", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index d9a3b9c719..5a708a6a5d 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Añadir {count} gráfico(s)", + "chart_removed": "Gráfico eliminado del panel", "charts_add_failed": "Error al añadir gráficos al panel", "charts_add_partial_failure": "Error al añadir {count} gráfico(s)", "charts_added_to_dashboard": "Gráficos añadidos al panel", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 4dc1ac7f82..5c630d959a 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Ajouter {count} graphique(s)", + "chart_removed": "Graphique retiré du tableau de bord", "charts_add_failed": "Échec de l'ajout des graphiques au tableau de bord", "charts_add_partial_failure": "Échec de l'ajout de {count} graphique(s)", "charts_added_to_dashboard": "Graphiques ajoutés au tableau de bord", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 498669f029..6257cc490b 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "{count} diagram hozzáadása", + "chart_removed": "A diagram eltávolítva a műszerfalról", "charts_add_failed": "A diagramok műszerfalhoz való hozzáadása sikertelen", "charts_add_partial_failure": "{count} diagram hozzáadása sikertelen", "charts_added_to_dashboard": "Diagramok hozzáadva a műszerfalhoz", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index a329357b3c..3823fa5456 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "{count}個のグラフを追加", + "chart_removed": "チャートがダッシュボードから削除されました", "charts_add_failed": "ダッシュボードへのグラフの追加に失敗しました", "charts_add_partial_failure": "{count}個のグラフの追加に失敗しました", "charts_added_to_dashboard": "グラフをダッシュボードに追加しました", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index ff1d41ed38..812ad6e5c9 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "{count} grafiek(en) toevoegen", + "chart_removed": "Grafiek verwijderd van dashboard", "charts_add_failed": "Grafieken toevoegen aan dashboard mislukt", "charts_add_partial_failure": "{count} grafiek(en) toevoegen mislukt", "charts_added_to_dashboard": "Grafieken toegevoegd aan dashboard", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 0df72c00e4..a17b4d97d9 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Adicionar {count} gráfico(s)", + "chart_removed": "Gráfico removido do painel", "charts_add_failed": "Falha ao adicionar gráficos ao painel", "charts_add_partial_failure": "Falha ao adicionar {count} gráfico(s)", "charts_added_to_dashboard": "Gráficos adicionados ao painel", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 85fbbc2edb..f032d8d5a6 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Adicionar {count} gráfico(s)", + "chart_removed": "Gráfico removido do painel", "charts_add_failed": "Falha ao adicionar gráficos ao painel", "charts_add_partial_failure": "Falha ao adicionar {count} gráfico(s)", "charts_added_to_dashboard": "Gráficos adicionados ao painel", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 4c0349f7f2..50d61971bd 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Adaugă {count} grafic(e)", + "chart_removed": "Graficul a fost eliminat din tabloul de bord", "charts_add_failed": "Nu s-au putut adăuga graficele la panoul de control", "charts_add_partial_failure": "Nu s-au putut adăuga {count} grafic(e)", "charts_added_to_dashboard": "Grafice adăugate la panoul de control", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index 9142b38109..481bed0693 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Добавить {count} график(ов)", + "chart_removed": "График удалён с панели", "charts_add_failed": "Не удалось добавить графики на дашборд", "charts_add_partial_failure": "Не удалось добавить {count} график(ов)", "charts_added_to_dashboard": "Графики добавлены на дашборд", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index c66c310e3d..4b9baa6043 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "Lägg till {count} diagram", + "chart_removed": "Diagram borttaget från instrumentpanelen", "charts_add_failed": "Misslyckades med att lägga till diagram i instrumentpanelen", "charts_add_partial_failure": "Misslyckades med att lägga till {count} diagram", "charts_added_to_dashboard": "Diagram tillagda i instrumentpanelen", diff --git a/apps/web/locales/tr-TR.json b/apps/web/locales/tr-TR.json index 43e33db336..becc8054b9 100644 --- a/apps/web/locales/tr-TR.json +++ b/apps/web/locales/tr-TR.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "{count} grafik ekle", + "chart_removed": "Grafik gösterge panosundan kaldırıldı", "charts_add_failed": "Grafikler panoya eklenemedi", "charts_add_partial_failure": "{count} grafik eklenemedi", "charts_added_to_dashboard": "Grafikler panoya eklendi", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 7f9aa2bc22..527ccd5f14 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "添加 {count} 个图表", + "chart_removed": "图表已从仪表板中移除", "charts_add_failed": "添加图表到仪表板失败", "charts_add_partial_failure": "添加 {count} 个图表失败", "charts_added_to_dashboard": "图表已添加到仪表板", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 8adf781b35..0d9347145a 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -1810,6 +1810,7 @@ }, "dashboards": { "add_count_charts": "新增 {count} 個圖表", + "chart_removed": "圖表已從儀表板移除", "charts_add_failed": "無法將圖表新增至儀表板", "charts_add_partial_failure": "無法新增 {count} 個圖表", "charts_added_to_dashboard": "圖表已新增至儀表板", diff --git a/apps/web/modules/ee/analysis/dashboards/actions.ts b/apps/web/modules/ee/analysis/dashboards/actions.ts index fd9864a34b..00185c71ef 100644 --- a/apps/web/modules/ee/analysis/dashboards/actions.ts +++ b/apps/web/modules/ee/analysis/dashboards/actions.ts @@ -18,6 +18,7 @@ import { duplicateDashboard, getDashboard, getDashboards, + removeWidgetFromDashboard, updateDashboard, updateWidgetLayouts, } from "./lib/dashboards"; @@ -111,15 +112,13 @@ export const updateDashboardAction = authenticatedActionClient.inputSchema(ZUpda const ZUpdateWidgetLayoutsAction = z.object({ workspaceId: ZId, dashboardId: ZId, - widgets: z - .array( - z.object({ - id: ZId, - layout: ZWidgetLayout, - order: z.number().int().nonnegative(), - }) - ) - .min(1), + widgets: z.array( + z.object({ + id: ZId, + layout: ZWidgetLayout, + order: z.number().int().nonnegative(), + }) + ), }); export const updateWidgetLayoutsAction = authenticatedActionClient @@ -325,3 +324,44 @@ export const addChartToDashboardAction = authenticatedActionClient } ) ); + +const ZRemoveWidgetFromDashboardAction = z.object({ + workspaceId: ZId, + dashboardId: ZId, + widgetId: ZId, +}); + +export const removeWidgetFromDashboardAction = authenticatedActionClient + .inputSchema(ZRemoveWidgetFromDashboardAction) + .action( + withAuditLogging( + "deleted", + "dashboardWidget", + async ({ + ctx, + parsedInput, + }: { + ctx: AuthenticatedActionClientCtx; + parsedInput: z.infer; + }) => { + const { organizationId, workspaceId } = await checkWorkspaceAccess( + ctx.user.id, + parsedInput.workspaceId, + "readWrite" + ); + await checkDashboardsEnabled(organizationId); + + const widget = await removeWidgetFromDashboard( + parsedInput.dashboardId, + workspaceId, + parsedInput.widgetId + ); + + ctx.auditLoggingCtx.organizationId = organizationId; + ctx.auditLoggingCtx.workspaceId = workspaceId; + ctx.auditLoggingCtx.dashboardWidgetId = widget.id; + ctx.auditLoggingCtx.oldObject = widget; + return { success: true }; + } + ) + ); 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 ad141c3ed9..bf31af56b9 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 @@ -134,7 +134,7 @@ export function AddExistingChartsDialog({ {t("common.add_charts")} {t("common.add_existing_chart_description")} - + {isLoading ? (
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 904eec49dd..cadd319cb9 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 @@ -17,10 +17,15 @@ import { DashboardWidget } from "@/modules/ee/analysis/dashboards/components/das import { DashboardWidgetData } from "@/modules/ee/analysis/dashboards/components/dashboard-widget-data"; import { DashboardWidgetSkeleton } from "@/modules/ee/analysis/dashboards/components/dashboard-widget-skeleton"; import type { TChartDataRow, TDashboardDetail, TDashboardWidget } from "@/modules/ee/analysis/types/analysis"; +import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { EmptyState } from "@/modules/ui/components/empty-state"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; -import { updateDashboardAction, updateWidgetLayoutsAction } from "../actions"; +import { + removeWidgetFromDashboardAction, + updateDashboardAction, + updateWidgetLayoutsAction, +} from "../actions"; import type { TDashboardWidgetError } from "../lib/widget-errors"; const ROW_HEIGHT = 80; @@ -163,6 +168,8 @@ export function DashboardDetailClient({ const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); const [editingChartId, setEditingChartId] = useState(null); + const [widgetIdToRemove, setWidgetIdToRemove] = useState(null); + const [isRemovingWidget, setIsRemovingWidget] = useState(false); const [, startTransition] = useTransition(); const [name, setName] = useState(dashboard.name); @@ -207,17 +214,36 @@ export function DashboardDetailClient({ const handleRemoveWidgetFromMenu = useCallback( (widgetId: string) => { - if (!isEditing) { - setDraftWidgets((current) => (current ?? dashboard.widgets).filter((w) => w.id !== widgetId)); - setIsEditing(true); + if (isEditing) { + handleRemoveWidget(widgetId); return; } - - handleRemoveWidget(widgetId); + setWidgetIdToRemove(widgetId); }, - [dashboard.widgets, handleRemoveWidget, isEditing] + [handleRemoveWidget, isEditing] ); + const handleConfirmRemoveWidget = useCallback(async () => { + if (!widgetIdToRemove) return; + setIsRemovingWidget(true); + try { + const result = await removeWidgetFromDashboardAction({ + workspaceId, + dashboardId: dashboard.id, + widgetId: widgetIdToRemove, + }); + if (!result?.data) { + toast.error(getFormattedErrorMessage(result)); + return; + } + toast.success(t("workspace.analysis.dashboards.chart_removed")); + setWidgetIdToRemove(null); + startTransition(() => router.refresh()); + } finally { + setIsRemovingWidget(false); + } + }, [widgetIdToRemove, workspaceId, dashboard.id, router, t, startTransition]); + const handleCancel = useCallback(() => { setName(dashboard.name); setDraftWidgets(null); @@ -373,6 +399,17 @@ export function DashboardDetailClient({ aiUnavailableReason={aiUnavailableReason} /> )} + {!isReadOnly && ( + { + if (!open) setWidgetIdToRemove(null); + }} + deleteWhat={t("common.chart")} + onDelete={handleConfirmRemoveWidget} + isDeleting={isRemovingWidget} + /> + )} ); } diff --git a/apps/web/modules/ee/analysis/dashboards/lib/dashboards.test.ts b/apps/web/modules/ee/analysis/dashboards/lib/dashboards.test.ts index dd19c1886f..be1382088e 100644 --- a/apps/web/modules/ee/analysis/dashboards/lib/dashboards.test.ts +++ b/apps/web/modules/ee/analysis/dashboards/lib/dashboards.test.ts @@ -18,9 +18,11 @@ var mockTxChart: { findFirst: ReturnType }; // NOSONAR / test code var mockTxWidget: { // NOSONAR / test code aggregate: ReturnType; + findFirst: ReturnType; findMany: ReturnType; create: ReturnType; update: ReturnType; + delete: ReturnType; deleteMany: ReturnType; }; @@ -29,9 +31,11 @@ vi.mock("@formbricks/database", () => { const txChart = { findFirst: vi.fn() }; const txWidget = { aggregate: vi.fn(), + findFirst: vi.fn(), findMany: vi.fn(), create: vi.fn(), update: vi.fn(), + delete: vi.fn(), deleteMany: vi.fn(), }; mockTxDashboard = txDash; @@ -672,4 +676,33 @@ describe("Dashboard Service", () => { }); }); }); + + describe("removeWidgetFromDashboard", () => { + const mockWidgetId = "widget-abc-123"; + + test("deletes a widget that belongs to the dashboard", async () => { + const mockWidget = { id: mockWidgetId, dashboardId: mockDashboardId, chartId: mockChartId }; + mockTxWidget.findFirst.mockResolvedValue(mockWidget); + mockTxWidget.delete.mockResolvedValue(mockWidget); + const { removeWidgetFromDashboard } = await import("./dashboards"); + + const result = await removeWidgetFromDashboard(mockDashboardId, mockWorkspaceId, mockWidgetId); + + expect(result).toEqual(mockWidget); + expect(mockTxWidget.findFirst).toHaveBeenCalledWith({ + where: { id: mockWidgetId, dashboard: { id: mockDashboardId, workspaceId: mockWorkspaceId } }, + }); + expect(mockTxWidget.delete).toHaveBeenCalledWith({ where: { id: mockWidgetId } }); + }); + + test("throws ResourceNotFoundError when the widget is not on the dashboard", async () => { + mockTxWidget.findFirst.mockResolvedValue(null); + const { removeWidgetFromDashboard } = await import("./dashboards"); + + await expect( + removeWidgetFromDashboard(mockDashboardId, mockWorkspaceId, mockWidgetId) + ).rejects.toMatchObject({ name: "ResourceNotFoundError", resourceType: "DashboardWidget" }); + expect(mockTxWidget.delete).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/web/modules/ee/analysis/dashboards/lib/dashboards.ts b/apps/web/modules/ee/analysis/dashboards/lib/dashboards.ts index 43082a1c44..246788c265 100644 --- a/apps/web/modules/ee/analysis/dashboards/lib/dashboards.ts +++ b/apps/web/modules/ee/analysis/dashboards/lib/dashboards.ts @@ -301,6 +301,36 @@ export const updateWidgetLayouts = async ( } }; +export const removeWidgetFromDashboard = async ( + dashboardId: string, + workspaceId: string, + widgetId: string +) => { + validateInputs([dashboardId, ZId], [workspaceId, ZId], [widgetId, ZId]); + + try { + return await prisma.$transaction(async (tx) => { + const widget = await tx.dashboardWidget.findFirst({ + where: { id: widgetId, dashboard: { id: dashboardId, workspaceId } }, + }); + + if (!widget) { + throw new ResourceNotFoundError("DashboardWidget", widgetId); + } + + return tx.dashboardWidget.delete({ where: { id: widgetId } }); + }); + } catch (error) { + if (error instanceof ResourceNotFoundError) { + throw error; + } + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + throw error; + } +}; + export const addChartToDashboard = async (data: TAddWidgetInput) => { validateInputs([data, ZAddWidgetInput]);