mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-12 03:20:43 -05:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1810,6 +1810,7 @@
|
||||
},
|
||||
"dashboards": {
|
||||
"add_count_charts": "{count}個のグラフを追加",
|
||||
"chart_removed": "チャートがダッシュボードから削除されました",
|
||||
"charts_add_failed": "ダッシュボードへのグラフの追加に失敗しました",
|
||||
"charts_add_partial_failure": "{count}個のグラフの追加に失敗しました",
|
||||
"charts_added_to_dashboard": "グラフをダッシュボードに追加しました",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1810,6 +1810,7 @@
|
||||
},
|
||||
"dashboards": {
|
||||
"add_count_charts": "Добавить {count} график(ов)",
|
||||
"chart_removed": "График удалён с панели",
|
||||
"charts_add_failed": "Не удалось добавить графики на дашборд",
|
||||
"charts_add_partial_failure": "Не удалось добавить {count} график(ов)",
|
||||
"charts_added_to_dashboard": "Графики добавлены на дашборд",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1810,6 +1810,7 @@
|
||||
},
|
||||
"dashboards": {
|
||||
"add_count_charts": "添加 {count} 个图表",
|
||||
"chart_removed": "图表已从仪表板中移除",
|
||||
"charts_add_failed": "添加图表到仪表板失败",
|
||||
"charts_add_partial_failure": "添加 {count} 个图表失败",
|
||||
"charts_added_to_dashboard": "图表已添加到仪表板",
|
||||
|
||||
@@ -1810,6 +1810,7 @@
|
||||
},
|
||||
"dashboards": {
|
||||
"add_count_charts": "新增 {count} 個圖表",
|
||||
"chart_removed": "圖表已從儀表板移除",
|
||||
"charts_add_failed": "無法將圖表新增至儀表板",
|
||||
"charts_add_partial_failure": "無法新增 {count} 個圖表",
|
||||
"charts_added_to_dashboard": "圖表已新增至儀表板",
|
||||
|
||||
@@ -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<typeof ZRemoveWidgetFromDashboardAction>;
|
||||
}) => {
|
||||
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 };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -134,7 +134,7 @@ export function AddExistingChartsDialog({
|
||||
<DialogTitle>{t("common.add_charts")}</DialogTitle>
|
||||
<DialogDescription>{t("common.add_existing_chart_description")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<DialogBody className="p-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center rounded-md border px-3 py-2">
|
||||
<Loader2Icon className="h-5 w-5 animate-spin text-slate-400" />
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [widgetIdToRemove, setWidgetIdToRemove] = useState<string | null>(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 && (
|
||||
<DeleteDialog
|
||||
open={widgetIdToRemove !== null}
|
||||
setOpen={(open) => {
|
||||
if (!open) setWidgetIdToRemove(null);
|
||||
}}
|
||||
deleteWhat={t("common.chart")}
|
||||
onDelete={handleConfirmRemoveWidget}
|
||||
isDeleting={isRemovingWidget}
|
||||
/>
|
||||
)}
|
||||
</PageContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ var mockTxChart: { findFirst: ReturnType<typeof vi.fn> }; // NOSONAR / test code
|
||||
var mockTxWidget: {
|
||||
// NOSONAR / test code
|
||||
aggregate: ReturnType<typeof vi.fn>;
|
||||
findFirst: ReturnType<typeof vi.fn>;
|
||||
findMany: ReturnType<typeof vi.fn>;
|
||||
create: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
delete: ReturnType<typeof vi.fn>;
|
||||
deleteMany: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user