Merge pull request #274 from illia1f/fix/ui-trash-operations

fix: replace window.confirm with React dialogs in trash operations
This commit is contained in:
Web Dev Cody
2025-12-26 18:15:28 -05:00
committed by GitHub
5 changed files with 163 additions and 143 deletions

View File

@@ -28,7 +28,7 @@ import {
useNavigation, useNavigation,
useProjectCreation, useProjectCreation,
useSetupDialog, useSetupDialog,
useTrashDialog, useTrashOperations,
useProjectTheme, useProjectTheme,
useUnviewedValidations, useUnviewedValidations,
} from './sidebar/hooks'; } from './sidebar/hooks';
@@ -68,6 +68,9 @@ export function Sidebar() {
// State for delete project confirmation dialog // State for delete project confirmation dialog
const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false); const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false);
// State for trash dialog
const [showTrashDialog, setShowTrashDialog] = useState(false);
// Project theme management (must come before useProjectCreation which uses globalTheme) // Project theme management (must come before useProjectCreation which uses globalTheme)
const { globalTheme } = useProjectTheme(); const { globalTheme } = useProjectTheme();
@@ -131,20 +134,17 @@ export function Sidebar() {
// Unviewed validations count // Unviewed validations count
const { count: unviewedValidationsCount } = useUnviewedValidations(currentProject); const { count: unviewedValidationsCount } = useUnviewedValidations(currentProject);
// Trash dialog and operations // Trash operations
const { const {
showTrashDialog,
setShowTrashDialog,
activeTrashId, activeTrashId,
isEmptyingTrash, isEmptyingTrash,
handleRestoreProject, handleRestoreProject,
handleDeleteProjectFromDisk, handleDeleteProjectFromDisk,
handleEmptyTrash, handleEmptyTrash,
} = useTrashDialog({ } = useTrashOperations({
restoreTrashedProject, restoreTrashedProject,
deleteTrashedProject, deleteTrashedProject,
emptyTrash, emptyTrash,
trashedProjects,
}); });
// Spec regeneration events // Spec regeneration events

View File

@@ -1,3 +1,4 @@
import { useState } from 'react';
import { X, Trash2, Undo2 } from 'lucide-react'; import { X, Trash2, Undo2 } from 'lucide-react';
import { import {
Dialog, Dialog,
@@ -8,6 +9,8 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import type { TrashedProject } from '@/lib/electron'; import type { TrashedProject } from '@/lib/electron';
interface TrashDialogProps { interface TrashDialogProps {
@@ -33,8 +36,42 @@ export function TrashDialog({
handleEmptyTrash, handleEmptyTrash,
isEmptyingTrash, isEmptyingTrash,
}: TrashDialogProps) { }: TrashDialogProps) {
// Confirmation dialog state (managed internally to avoid prop drilling)
const [deleteFromDiskProject, setDeleteFromDiskProject] = useState<TrashedProject | null>(null);
const [showEmptyTrashConfirm, setShowEmptyTrashConfirm] = useState(false);
// Reset confirmation dialog state when main dialog closes
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
setDeleteFromDiskProject(null);
setShowEmptyTrashConfirm(false);
}
onOpenChange(isOpen);
};
const onDeleteFromDiskClick = (project: TrashedProject) => {
setDeleteFromDiskProject(project);
};
const onConfirmDeleteFromDisk = () => {
if (deleteFromDiskProject) {
handleDeleteProjectFromDisk(deleteFromDiskProject);
setDeleteFromDiskProject(null);
}
};
const onEmptyTrashClick = () => {
setShowEmptyTrashConfirm(true);
};
const onConfirmEmptyTrash = () => {
handleEmptyTrash();
setShowEmptyTrashConfirm(false);
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl"> <DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Recycle Bin</DialogTitle> <DialogTitle>Recycle Bin</DialogTitle>
@@ -72,7 +109,7 @@ export function TrashDialog({
<Button <Button
size="sm" size="sm"
variant="destructive" variant="destructive"
onClick={() => handleDeleteProjectFromDisk(project)} onClick={() => onDeleteFromDiskClick(project)}
disabled={activeTrashId === project.id} disabled={activeTrashId === project.id}
data-testid={`delete-project-disk-${project.id}`} data-testid={`delete-project-disk-${project.id}`}
> >
@@ -102,7 +139,7 @@ export function TrashDialog({
{trashedProjects.length > 0 && ( {trashedProjects.length > 0 && (
<Button <Button
variant="outline" variant="outline"
onClick={handleEmptyTrash} onClick={onEmptyTrashClick}
disabled={isEmptyingTrash} disabled={isEmptyingTrash}
data-testid="empty-trash" data-testid="empty-trash"
> >
@@ -112,5 +149,33 @@ export function TrashDialog({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete from disk confirmation dialog */}
{deleteFromDiskProject && (
<DeleteConfirmDialog
open
onOpenChange={(isOpen) => !isOpen && setDeleteFromDiskProject(null)}
onConfirm={onConfirmDeleteFromDisk}
title={`Delete "${deleteFromDiskProject.name}" from disk?`}
description="This sends the folder to your system Trash."
confirmText="Delete from disk"
testId="delete-from-disk-confirm-dialog"
confirmTestId="confirm-delete-from-disk-button"
/>
)}
{/* Empty trash confirmation dialog */}
<ConfirmDialog
open={showEmptyTrashConfirm}
onOpenChange={setShowEmptyTrashConfirm}
onConfirm={onConfirmEmptyTrash}
title="Empty Recycle Bin"
description="Clear all projects from recycle bin? This does not delete folders from disk."
confirmText="Empty"
confirmVariant="destructive"
icon={Trash2}
iconClassName="text-destructive"
/>
</>
); );
} }

View File

@@ -8,6 +8,5 @@ export { useSpecRegeneration } from './use-spec-regeneration';
export { useNavigation } from './use-navigation'; export { useNavigation } from './use-navigation';
export { useProjectCreation } from './use-project-creation'; export { useProjectCreation } from './use-project-creation';
export { useSetupDialog } from './use-setup-dialog'; export { useSetupDialog } from './use-setup-dialog';
export { useTrashDialog } from './use-trash-dialog';
export { useProjectTheme } from './use-project-theme'; export { useProjectTheme } from './use-project-theme';
export { useUnviewedValidations } from './use-unviewed-validations'; export { useUnviewedValidations } from './use-unviewed-validations';

View File

@@ -1,40 +0,0 @@
import { useState } from 'react';
import { useTrashOperations } from './use-trash-operations';
import type { TrashedProject } from '@/lib/electron';
interface UseTrashDialogProps {
restoreTrashedProject: (projectId: string) => void;
deleteTrashedProject: (projectId: string) => void;
emptyTrash: () => void;
trashedProjects: TrashedProject[];
}
/**
* Hook that combines trash operations with dialog state management
*/
export function useTrashDialog({
restoreTrashedProject,
deleteTrashedProject,
emptyTrash,
trashedProjects,
}: UseTrashDialogProps) {
// Dialog state
const [showTrashDialog, setShowTrashDialog] = useState(false);
// Reuse existing trash operations logic
const trashOperations = useTrashOperations({
restoreTrashedProject,
deleteTrashedProject,
emptyTrash,
trashedProjects,
});
return {
// Dialog state
showTrashDialog,
setShowTrashDialog,
// Trash operations (spread from existing hook)
...trashOperations,
};
}

View File

@@ -6,35 +6,35 @@ interface UseTrashOperationsProps {
restoreTrashedProject: (projectId: string) => void; restoreTrashedProject: (projectId: string) => void;
deleteTrashedProject: (projectId: string) => void; deleteTrashedProject: (projectId: string) => void;
emptyTrash: () => void; emptyTrash: () => void;
trashedProjects: TrashedProject[];
} }
export function useTrashOperations({ export function useTrashOperations({
restoreTrashedProject, restoreTrashedProject,
deleteTrashedProject, deleteTrashedProject,
emptyTrash, emptyTrash,
trashedProjects,
}: UseTrashOperationsProps) { }: UseTrashOperationsProps) {
const [activeTrashId, setActiveTrashId] = useState<string | null>(null); const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false); const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
const handleRestoreProject = useCallback( const handleRestoreProject = useCallback(
(projectId: string) => { (projectId: string) => {
try {
restoreTrashedProject(projectId); restoreTrashedProject(projectId);
toast.success('Project restored', { toast.success('Project restored', {
description: 'Added back to your project list.', description: 'Added back to your project list.',
}); });
} catch (error) {
console.error('[Sidebar] Failed to restore project:', error);
toast.error('Failed to restore project', {
description: error instanceof Error ? error.message : 'Unknown error',
});
}
}, },
[restoreTrashedProject] [restoreTrashedProject]
); );
const handleDeleteProjectFromDisk = useCallback( const handleDeleteProjectFromDisk = useCallback(
async (trashedProject: TrashedProject) => { async (trashedProject: TrashedProject) => {
const confirmed = window.confirm(
`Delete "${trashedProject.name}" from disk?\nThis sends the folder to your system Trash.`
);
if (!confirmed) return;
setActiveTrashId(trashedProject.id); setActiveTrashId(trashedProject.id);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
@@ -64,23 +64,19 @@ export function useTrashOperations({
); );
const handleEmptyTrash = useCallback(() => { const handleEmptyTrash = useCallback(() => {
if (trashedProjects.length === 0) {
return;
}
const confirmed = window.confirm(
'Clear all projects from recycle bin? This does not delete folders from disk.'
);
if (!confirmed) return;
setIsEmptyingTrash(true); setIsEmptyingTrash(true);
try { try {
emptyTrash(); emptyTrash();
toast.success('Recycle bin cleared'); toast.success('Recycle bin cleared');
} catch (error) {
console.error('[Sidebar] Failed to empty trash:', error);
toast.error('Failed to clear recycle bin', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally { } finally {
setIsEmptyingTrash(false); setIsEmptyingTrash(false);
} }
}, [emptyTrash, trashedProjects.length]); }, [emptyTrash]);
return { return {
activeTrashId, activeTrashId,