From cd6a2caf5609f0553ff5f34500c51b2df6807bd8 Mon Sep 17 00:00:00 2001 From: siddarth Date: Thu, 4 Jun 2026 14:27:14 +0530 Subject: [PATCH 1/2] Merge upstream/main into main --- frontend/app/dataset/[id]/page.tsx | 94 +++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/frontend/app/dataset/[id]/page.tsx b/frontend/app/dataset/[id]/page.tsx index d28abda..36e9592 100644 --- a/frontend/app/dataset/[id]/page.tsx +++ b/frontend/app/dataset/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useConvexAuth } from "convex/react"; @@ -25,6 +25,7 @@ import type { ProfileUser } from "@/lib/profile-user"; export default function DatasetPage() { const params = useParams(); + const router = useRouter(); const { isLoading: authLoading, isAuthenticated } = useConvexAuth(); const { userId, getToken } = useAuth(); const { user } = useUser(); @@ -36,6 +37,7 @@ export default function DatasetPage() { const [exportOpen, setExportOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [confirmPopulate, setConfirmPopulate] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); const [savingRefreshCadence, setSavingRefreshCadence] = useState(false); const [savingMaxRowCount, setSavingMaxRowCount] = useState(false); const [maxRowCountSaveError, setMaxRowCountSaveError] = useState(null); @@ -55,6 +57,7 @@ export default function DatasetPage() { authLoading ? "skip" : { datasetId }, ); const updateRefreshSettings = useMutation(api.datasets.updateRefreshSettings); + const removeDataset = useMutation(api.datasets.remove); const updateMaxRowCount = useMutation(api.datasets.updateMaxRowCount); const usage = useQuery( api.quota.getMy, @@ -288,6 +291,17 @@ export default function DatasetPage() { } } + async function handleDelete() { + if (!dataset) return; + try { + await removeDataset({ id: dataset._id }); + router.push("/dashboard"); + } catch (err) { + console.error("[delete] failed", err); + captureException(err, { operation: "dataset_delete", datasetId: dataset._id }); + } + } + // Computed before the loading guard so the useEffect below can depend on it // without hitting the TDZ. Optional chaining handles the pre-load undefined state. const isDatasetBusy = dataset?.status === "building" || dataset?.status === "updating"; @@ -302,7 +316,6 @@ export default function DatasetPage() { return () => clearTimeout(id); } }, [isDatasetBusy]); - if (authLoading || dataset === undefined || rows === undefined) { return (
@@ -414,6 +427,7 @@ export default function DatasetPage() { handlePopulate(); } }} + onDelete={isOwner ? () => { setSettingsOpen(false); setConfirmDelete(true); } : undefined} />
@@ -476,6 +490,17 @@ export default function DatasetPage() { onCancel={() => setConfirmPopulate(false)} /> )} + + {confirmDelete && ( + { + setConfirmDelete(false); + handleDelete(); + }} + onCancel={() => setConfirmDelete(false)} + /> + )}
); } @@ -581,6 +606,7 @@ function SettingsDropdown({ onMaxRowCountChange, onUpdate, onPopulate, + onDelete, }: { open: boolean; onToggle: () => void; @@ -599,6 +625,7 @@ function SettingsDropdown({ onMaxRowCountChange: (maxRowCount: number) => void; onUpdate: () => void; onPopulate: () => void; + onDelete?: () => void; }) { const ref = useRef(null); const [maxRowCountInput, setMaxRowCountInput] = useState(String(maxRowCount)); @@ -710,6 +737,7 @@ function SettingsDropdown({
+ {onDelete && ( +
+ +
+ )} )} @@ -856,3 +895,54 @@ function ConfirmPopulateModal({ ); } + +/* ------------------------------------------------------------------ */ +/* Confirm delete modal */ +/* ------------------------------------------------------------------ */ + +function ConfirmDeleteModal({ + datasetName, + onConfirm, + onCancel, +}: { + datasetName: string; + onConfirm: () => void; + onCancel: () => void; +}) { + useEffect(() => { + function handleKey(e: KeyboardEvent) { + if (e.key === "Escape") onCancel(); + } + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + role="presentation" + > +
+

+ Delete “{datasetName}”? +

+

All rows will be permanently deleted. This can't be undone.

+
+ + +
+
+
+ ); +} From acc2fa9ff855a0a59b281849c8dea1e9998ddef6 Mon Sep 17 00:00:00 2001 From: siddarth Date: Sat, 6 Jun 2026 22:32:09 +0530 Subject: [PATCH 2/2] fix: guard delete against duplicate submits and disable during active runs - Add deleting state to prevent repeated clicks while mutation is in flight - Keep modal open until deletion resolves, then navigate with router.replace - Disable Cancel and confirm button while deleting is in progress - Hide delete option when dataset is building or updating to avoid orphaned backend runs --- frontend/app/dataset/[id]/page.tsx | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/frontend/app/dataset/[id]/page.tsx b/frontend/app/dataset/[id]/page.tsx index 36e9592..75bb48b 100644 --- a/frontend/app/dataset/[id]/page.tsx +++ b/frontend/app/dataset/[id]/page.tsx @@ -38,6 +38,7 @@ export default function DatasetPage() { const [settingsOpen, setSettingsOpen] = useState(false); const [confirmPopulate, setConfirmPopulate] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); + const [deleting, setDeleting] = useState(false); const [savingRefreshCadence, setSavingRefreshCadence] = useState(false); const [savingMaxRowCount, setSavingMaxRowCount] = useState(false); const [maxRowCountSaveError, setMaxRowCountSaveError] = useState(null); @@ -292,13 +293,17 @@ export default function DatasetPage() { } async function handleDelete() { - if (!dataset) return; + if (!dataset || deleting || dataset.status === "building" || dataset.status === "updating") return; + setDeleting(true); try { await removeDataset({ id: dataset._id }); - router.push("/dashboard"); + setConfirmDelete(false); + router.replace("/dashboard"); } catch (err) { console.error("[delete] failed", err); captureException(err, { operation: "dataset_delete", datasetId: dataset._id }); + } finally { + setDeleting(false); } } @@ -427,7 +432,7 @@ export default function DatasetPage() { handlePopulate(); } }} - onDelete={isOwner ? () => { setSettingsOpen(false); setConfirmDelete(true); } : undefined} + onDelete={isOwner && !isDatasetBusy ? () => { setSettingsOpen(false); setConfirmDelete(true); } : undefined} />
@@ -494,11 +499,9 @@ export default function DatasetPage() { {confirmDelete && ( { - setConfirmDelete(false); - handleDelete(); - }} - onCancel={() => setConfirmDelete(false)} + deleting={deleting} + onConfirm={handleDelete} + onCancel={() => { if (!deleting) setConfirmDelete(false); }} /> )}
@@ -902,10 +905,12 @@ function ConfirmPopulateModal({ function ConfirmDeleteModal({ datasetName, + deleting, onConfirm, onCancel, }: { datasetName: string; + deleting: boolean; onConfirm: () => void; onCancel: () => void; }) { @@ -931,15 +936,17 @@ function ConfirmDeleteModal({