feat: ask for confirmation before bulk delete

This commit is contained in:
zhom
2025-07-26 19:03:42 +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 { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog";
import { ChangeVersionDialog } from "@/components/change-version-dialog"; import { ChangeVersionDialog } from "@/components/change-version-dialog";
import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { GroupAssignmentDialog } from "@/components/group-assignment-dialog"; import { GroupAssignmentDialog } from "@/components/group-assignment-dialog";
import { GroupBadges } from "@/components/group-badges"; import { GroupBadges } from "@/components/group-badges";
import { GroupManagementDialog } from "@/components/group-management-dialog"; import { GroupManagementDialog } from "@/components/group-management-dialog";
@@ -77,6 +78,9 @@ export default function Home() {
const [areGroupsLoading, setGroupsLoading] = useState(true); const [areGroupsLoading, setGroupsLoading] = useState(true);
const [currentPermissionType, setCurrentPermissionType] = const [currentPermissionType, setCurrentPermissionType] =
useState<PermissionType>("microphone"); useState<PermissionType>("microphone");
const [showBulkDeleteConfirmation, setShowBulkDeleteConfirmation] =
useState(false);
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } = const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
usePermissions(); usePermissions();
@@ -595,17 +599,27 @@ export default function Home() {
setGroupAssignmentDialogOpen(true); 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; if (selectedProfiles.length === 0) return;
setIsBulkDeleting(true);
try { try {
await invoke("delete_selected_profiles", { await invoke("delete_selected_profiles", {
profileNames: selectedProfiles, profileNames: selectedProfiles,
}); });
await loadProfiles(); await loadProfiles();
setSelectedProfiles([]); setSelectedProfiles([]);
setShowBulkDeleteConfirmation(false);
} catch (error) { } catch (error) {
console.error("Failed to delete selected profiles:", error); console.error("Failed to delete selected profiles:", error);
setError(`Failed to delete selected profiles: ${JSON.stringify(error)}`);
} finally {
setIsBulkDeleting(false);
} }
}, [selectedProfiles, loadProfiles]); }, [selectedProfiles, loadProfiles]);
@@ -827,6 +841,16 @@ export default function Home() {
selectedProfiles={selectedProfilesForGroup} selectedProfiles={selectedProfilesForGroup}
onAssignmentComplete={handleGroupAssignmentComplete} 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> </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 = { type Props = {
selectedProfiles: string[]; selectedProfiles: string[];
onBulkGroupAssignment: () => void; onBulkGroupAssignment: () => void;
onBulkDelete: () => Promise<void>; onBulkDelete: () => void;
onSettingsDialogOpen: (open: boolean) => void; onSettingsDialogOpen: (open: boolean) => void;
onProxyManagementDialogOpen: (open: boolean) => void; onProxyManagementDialogOpen: (open: boolean) => void;
onGroupManagementDialogOpen: (open: boolean) => void; onGroupManagementDialogOpen: (open: boolean) => void;
@@ -57,7 +57,7 @@ const HomeHeader = ({
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
onClick={() => void onBulkDelete()} onClick={onBulkDelete}
className="flex gap-2 items-center" className="flex gap-2 items-center"
> >
<LuTrash2 className="w-4 h-4" /> <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 { CiCircleCheck } from "react-icons/ci";
import { IoEllipsisHorizontal } from "react-icons/io5"; import { IoEllipsisHorizontal } from "react-icons/io5";
import { LuChevronDown, LuChevronUp } from "react-icons/lu"; import { LuChevronDown, LuChevronUp } from "react-icons/lu";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
@@ -101,9 +101,7 @@ export function ProfilesDataTable({
const [renameError, setRenameError] = React.useState<string | null>(null); const [renameError, setRenameError] = React.useState<string | null>(null);
const [profileToDelete, setProfileToDelete] = const [profileToDelete, setProfileToDelete] =
React.useState<BrowserProfile | null>(null); React.useState<BrowserProfile | null>(null);
const [deleteConfirmationName, setDeleteConfirmationName] = const [isDeleting, setIsDeleting] = React.useState(false);
React.useState("");
const [deleteError, setDeleteError] = React.useState<string | null>(null);
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]); const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]);
const [selectedProfiles, setSelectedProfiles] = React.useState<Set<string>>( const [selectedProfiles, setSelectedProfiles] = React.useState<Set<string>>(
@@ -214,20 +212,16 @@ export function ProfilesDataTable({
}; };
const handleDelete = async () => { const handleDelete = async () => {
if (!profileToDelete || deleteConfirmationName !== profileToDelete.name) { if (!profileToDelete) return;
setDeleteError("Profile name confirmation does not match");
return;
}
setIsDeleting(true);
try { try {
await onDeleteProfile(profileToDelete); await onDeleteProfile(profileToDelete);
setProfileToDelete(null); setProfileToDelete(null);
setDeleteConfirmationName("");
setDeleteError(null);
} catch (error) { } catch (error) {
setDeleteError( console.error("Failed to delete profile:", error);
error instanceof Error ? error.message : "Failed to delete profile", } finally {
); setIsDeleting(false);
} }
}; };
@@ -621,7 +615,6 @@ export function ProfilesDataTable({
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setProfileToDelete(profile); setProfileToDelete(profile);
setDeleteConfirmationName("");
}} }}
disabled={ disabled={
!browserState.isClient || isRunning || isBrowserUpdating !browserState.isClient || isRunning || isBrowserUpdating
@@ -776,73 +769,15 @@ export function ProfilesDataTable({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog <DeleteConfirmationDialog
open={profileToDelete !== null} isOpen={profileToDelete !== null}
onOpenChange={(open) => { onClose={() => setProfileToDelete(null)}
if (!open) { onConfirm={handleDelete}
setProfileToDelete(null); title="Delete Profile"
setDeleteConfirmationName(""); description={`This action cannot be undone. This will permanently delete the profile "${profileToDelete?.name}" and all its associated data.`}
setDeleteError(null); confirmButtonText="Delete Profile"
} isLoading={isDeleting}
}} />
>
<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>
</> </>
); );
} }