mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -28,7 +28,7 @@ import {
|
||||
useNavigation,
|
||||
useProjectCreation,
|
||||
useSetupDialog,
|
||||
useTrashDialog,
|
||||
useTrashOperations,
|
||||
useProjectTheme,
|
||||
useUnviewedValidations,
|
||||
} from './sidebar/hooks';
|
||||
@@ -68,6 +68,9 @@ export function Sidebar() {
|
||||
// State for delete project confirmation dialog
|
||||
const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false);
|
||||
|
||||
// State for trash dialog
|
||||
const [showTrashDialog, setShowTrashDialog] = useState(false);
|
||||
|
||||
// Project theme management (must come before useProjectCreation which uses globalTheme)
|
||||
const { globalTheme } = useProjectTheme();
|
||||
|
||||
@@ -131,20 +134,17 @@ export function Sidebar() {
|
||||
// Unviewed validations count
|
||||
const { count: unviewedValidationsCount } = useUnviewedValidations(currentProject);
|
||||
|
||||
// Trash dialog and operations
|
||||
// Trash operations
|
||||
const {
|
||||
showTrashDialog,
|
||||
setShowTrashDialog,
|
||||
activeTrashId,
|
||||
isEmptyingTrash,
|
||||
handleRestoreProject,
|
||||
handleDeleteProjectFromDisk,
|
||||
handleEmptyTrash,
|
||||
} = useTrashDialog({
|
||||
} = useTrashOperations({
|
||||
restoreTrashedProject,
|
||||
deleteTrashedProject,
|
||||
emptyTrash,
|
||||
trashedProjects,
|
||||
});
|
||||
|
||||
// Spec regeneration events
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { X, Trash2, Undo2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -8,6 +9,8 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
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';
|
||||
|
||||
interface TrashDialogProps {
|
||||
@@ -33,84 +36,146 @@ export function TrashDialog({
|
||||
handleEmptyTrash,
|
||||
isEmptyingTrash,
|
||||
}: 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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Recycle Bin</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Restore projects to the sidebar or delete their folders using your system Trash.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Recycle Bin</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Restore projects to the sidebar or delete their folders using your system Trash.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{trashedProjects.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Recycle bin is empty.</p>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
|
||||
{trashedProjects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="flex items-start justify-between gap-3 rounded-lg border border-border bg-card/50 p-4"
|
||||
>
|
||||
<div className="space-y-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">{project.name}</p>
|
||||
<p className="text-xs text-muted-foreground break-all">{project.path}</p>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
Trashed {new Date(project.trashedAt).toLocaleString()}
|
||||
</p>
|
||||
{trashedProjects.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Recycle bin is empty.</p>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
|
||||
{trashedProjects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="flex items-start justify-between gap-3 rounded-lg border border-border bg-card/50 p-4"
|
||||
>
|
||||
<div className="space-y-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">{project.name}</p>
|
||||
<p className="text-xs text-muted-foreground break-all">{project.path}</p>
|
||||
<p className="text-[11px] text-muted-foreground/80">
|
||||
Trashed {new Date(project.trashedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => handleRestoreProject(project.id)}
|
||||
data-testid={`restore-project-${project.id}`}
|
||||
>
|
||||
<Undo2 className="h-3.5 w-3.5 mr-1.5" />
|
||||
Restore
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => onDeleteFromDiskClick(project)}
|
||||
disabled={activeTrashId === project.id}
|
||||
data-testid={`delete-project-disk-${project.id}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
||||
{activeTrashId === project.id ? 'Deleting...' : 'Delete from disk'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => deleteTrashedProject(project.id)}
|
||||
data-testid={`remove-project-${project.id}`}
|
||||
>
|
||||
<X className="h-3.5 w-3.5 mr-1.5" />
|
||||
Remove from list
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => handleRestoreProject(project.id)}
|
||||
data-testid={`restore-project-${project.id}`}
|
||||
>
|
||||
<Undo2 className="h-3.5 w-3.5 mr-1.5" />
|
||||
Restore
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => handleDeleteProjectFromDisk(project)}
|
||||
disabled={activeTrashId === project.id}
|
||||
data-testid={`delete-project-disk-${project.id}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1.5" />
|
||||
{activeTrashId === project.id ? 'Deleting...' : 'Delete from disk'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => deleteTrashedProject(project.id)}
|
||||
data-testid={`remove-project-${project.id}`}
|
||||
>
|
||||
<X className="h-3.5 w-3.5 mr-1.5" />
|
||||
Remove from list
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="flex justify-between">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
{trashedProjects.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleEmptyTrash}
|
||||
disabled={isEmptyingTrash}
|
||||
data-testid="empty-trash"
|
||||
>
|
||||
{isEmptyingTrash ? 'Clearing...' : 'Empty Recycle Bin'}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DialogFooter className="flex justify-between">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
{trashedProjects.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onEmptyTrashClick}
|
||||
disabled={isEmptyingTrash}
|
||||
data-testid="empty-trash"
|
||||
>
|
||||
{isEmptyingTrash ? 'Clearing...' : 'Empty Recycle Bin'}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,5 @@ export { useSpecRegeneration } from './use-spec-regeneration';
|
||||
export { useNavigation } from './use-navigation';
|
||||
export { useProjectCreation } from './use-project-creation';
|
||||
export { useSetupDialog } from './use-setup-dialog';
|
||||
export { useTrashDialog } from './use-trash-dialog';
|
||||
export { useProjectTheme } from './use-project-theme';
|
||||
export { useUnviewedValidations } from './use-unviewed-validations';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -6,35 +6,35 @@ interface UseTrashOperationsProps {
|
||||
restoreTrashedProject: (projectId: string) => void;
|
||||
deleteTrashedProject: (projectId: string) => void;
|
||||
emptyTrash: () => void;
|
||||
trashedProjects: TrashedProject[];
|
||||
}
|
||||
|
||||
export function useTrashOperations({
|
||||
restoreTrashedProject,
|
||||
deleteTrashedProject,
|
||||
emptyTrash,
|
||||
trashedProjects,
|
||||
}: UseTrashOperationsProps) {
|
||||
const [activeTrashId, setActiveTrashId] = useState<string | null>(null);
|
||||
const [isEmptyingTrash, setIsEmptyingTrash] = useState(false);
|
||||
|
||||
const handleRestoreProject = useCallback(
|
||||
(projectId: string) => {
|
||||
restoreTrashedProject(projectId);
|
||||
toast.success('Project restored', {
|
||||
description: 'Added back to your project list.',
|
||||
});
|
||||
try {
|
||||
restoreTrashedProject(projectId);
|
||||
toast.success('Project restored', {
|
||||
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]
|
||||
);
|
||||
|
||||
const handleDeleteProjectFromDisk = useCallback(
|
||||
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);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
@@ -64,23 +64,19 @@ export function useTrashOperations({
|
||||
);
|
||||
|
||||
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);
|
||||
try {
|
||||
emptyTrash();
|
||||
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 {
|
||||
setIsEmptyingTrash(false);
|
||||
}
|
||||
}, [emptyTrash, trashedProjects.length]);
|
||||
}, [emptyTrash]);
|
||||
|
||||
return {
|
||||
activeTrashId,
|
||||
|
||||
Reference in New Issue
Block a user