mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-27 19:51:59 +02:00
feat: ask for confirmation before bulk delete
This commit is contained in:
+25
-1
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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 "{profileToDelete?.name}" 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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user