feat: ask for confirmation before bulk delete

This commit is contained in:
zhom
2025-07-26 18:24:22 +04:00
parent 40ad32af6d
commit 328e6f16ee
4 changed files with 101 additions and 84 deletions
+25 -1
View File
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { GroupAssignmentDialog } from "@/components/group-assignment-dialog";
import { GroupBadges } from "@/components/group-badges";
import { GroupManagementDialog } from "@/components/group-management-dialog";
@@ -77,6 +78,9 @@ export default function Home() {
const [areGroupsLoading, setGroupsLoading] = useState(true);
const [currentPermissionType, setCurrentPermissionType] =
useState<PermissionType>("microphone");
const [showBulkDeleteConfirmation, setShowBulkDeleteConfirmation] =
useState(false);
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
usePermissions();
@@ -595,17 +599,27 @@ export default function Home() {
setGroupAssignmentDialogOpen(true);
}, []);
const handleBulkDelete = useCallback(async () => {
const handleBulkDelete = useCallback(() => {
if (selectedProfiles.length === 0) return;
setShowBulkDeleteConfirmation(true);
}, [selectedProfiles]);
const confirmBulkDelete = useCallback(async () => {
if (selectedProfiles.length === 0) return;
setIsBulkDeleting(true);
try {
await invoke("delete_selected_profiles", {
profileNames: selectedProfiles,
});
await loadProfiles();
setSelectedProfiles([]);
setShowBulkDeleteConfirmation(false);
} catch (error) {
console.error("Failed to delete selected profiles:", error);
setError(`Failed to delete selected profiles: ${JSON.stringify(error)}`);
} finally {
setIsBulkDeleting(false);
}
}, [selectedProfiles, loadProfiles]);
@@ -827,6 +841,16 @@ export default function Home() {
selectedProfiles={selectedProfilesForGroup}
onAssignmentComplete={handleGroupAssignmentComplete}
/>
<DeleteConfirmationDialog
isOpen={showBulkDeleteConfirmation}
onClose={() => setShowBulkDeleteConfirmation(false)}
onConfirm={confirmBulkDelete}
title="Delete Selected Profiles"
description={`This action cannot be undone. This will permanently delete ${selectedProfiles.length} profile${selectedProfiles.length !== 1 ? "s" : ""} and all associated data.`}
confirmButtonText={`Delete ${selectedProfiles.length} Profile${selectedProfiles.length !== 1 ? "s" : ""}`}
isLoading={isBulkDeleting}
/>
</div>
);
}
@@ -0,0 +1,58 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
interface DeleteConfirmationDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void | Promise<void>;
title: string;
description: string;
confirmButtonText?: string;
isLoading?: boolean;
}
export function DeleteConfirmationDialog({
isOpen,
onClose,
onConfirm,
title,
description,
confirmButtonText = "Delete",
isLoading = false,
}: DeleteConfirmationDialogProps) {
const handleConfirm = async () => {
await onConfirm();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isLoading}>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => void handleConfirm()}
disabled={isLoading}
>
{isLoading ? "Deleting..." : confirmButtonText}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+2 -2
View File
@@ -16,7 +16,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
type Props = {
selectedProfiles: string[];
onBulkGroupAssignment: () => void;
onBulkDelete: () => Promise<void>;
onBulkDelete: () => void;
onSettingsDialogOpen: (open: boolean) => void;
onProxyManagementDialogOpen: (open: boolean) => void;
onGroupManagementDialogOpen: (open: boolean) => void;
@@ -57,7 +57,7 @@ const HomeHeader = ({
<Button
variant="destructive"
size="sm"
onClick={() => void onBulkDelete()}
onClick={onBulkDelete}
className="flex gap-2 items-center"
>
<LuTrash2 className="w-4 h-4" />
+16 -81
View File
@@ -13,12 +13,12 @@ import * as React from "react";
import { CiCircleCheck } from "react-icons/ci";
import { IoEllipsisHorizontal } from "react-icons/io5";
import { LuChevronDown, LuChevronUp } from "react-icons/lu";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
@@ -101,9 +101,7 @@ export function ProfilesDataTable({
const [renameError, setRenameError] = React.useState<string | null>(null);
const [profileToDelete, setProfileToDelete] =
React.useState<BrowserProfile | null>(null);
const [deleteConfirmationName, setDeleteConfirmationName] =
React.useState("");
const [deleteError, setDeleteError] = React.useState<string | null>(null);
const [isDeleting, setIsDeleting] = React.useState(false);
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]);
const [selectedProfiles, setSelectedProfiles] = React.useState<Set<string>>(
@@ -214,20 +212,16 @@ export function ProfilesDataTable({
};
const handleDelete = async () => {
if (!profileToDelete || deleteConfirmationName !== profileToDelete.name) {
setDeleteError("Profile name confirmation does not match");
return;
}
if (!profileToDelete) return;
setIsDeleting(true);
try {
await onDeleteProfile(profileToDelete);
setProfileToDelete(null);
setDeleteConfirmationName("");
setDeleteError(null);
} catch (error) {
setDeleteError(
error instanceof Error ? error.message : "Failed to delete profile",
);
console.error("Failed to delete profile:", error);
} finally {
setIsDeleting(false);
}
};
@@ -621,7 +615,6 @@ export function ProfilesDataTable({
<DropdownMenuItem
onClick={() => {
setProfileToDelete(profile);
setDeleteConfirmationName("");
}}
disabled={
!browserState.isClient || isRunning || isBrowserUpdating
@@ -776,73 +769,15 @@ export function ProfilesDataTable({
</DialogContent>
</Dialog>
<Dialog
open={profileToDelete !== null}
onOpenChange={(open) => {
if (!open) {
setProfileToDelete(null);
setDeleteConfirmationName("");
setDeleteError(null);
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Profile</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
profile &quot;{profileToDelete?.name}&quot; and all its associated
data.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="delete-confirmation">
Please type <strong>{profileToDelete?.name}</strong> to confirm:
</Label>
<Input
id="delete-confirmation"
value={deleteConfirmationName}
onChange={(e) => {
setDeleteConfirmationName(e.target.value);
setDeleteError(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleDelete();
}
}}
placeholder="Type the profile name here"
/>
</div>
{deleteError && (
<p className="text-sm text-red-600">{deleteError}</p>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setProfileToDelete(null);
setDeleteConfirmationName("");
setDeleteError(null);
}}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => void handleDelete()}
disabled={
!deleteConfirmationName.trim() ||
deleteConfirmationName !== profileToDelete?.name
}
>
Delete Profile
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DeleteConfirmationDialog
isOpen={profileToDelete !== null}
onClose={() => setProfileToDelete(null)}
onConfirm={handleDelete}
title="Delete Profile"
description={`This action cannot be undone. This will permanently delete the profile "${profileToDelete?.name}" and all its associated data.`}
confirmButtonText="Delete Profile"
isLoading={isDeleting}
/>
</>
);
}