mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +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,
|
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
|
||||||
|
|||||||
@@ -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,84 +36,146 @@ 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}>
|
<>
|
||||||
<DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl">
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogHeader>
|
<DialogContent className="bg-popover/95 backdrop-blur-xl border-border max-w-2xl">
|
||||||
<DialogTitle>Recycle Bin</DialogTitle>
|
<DialogHeader>
|
||||||
<DialogDescription className="text-muted-foreground">
|
<DialogTitle>Recycle Bin</DialogTitle>
|
||||||
Restore projects to the sidebar or delete their folders using your system Trash.
|
<DialogDescription className="text-muted-foreground">
|
||||||
</DialogDescription>
|
Restore projects to the sidebar or delete their folders using your system Trash.
|
||||||
</DialogHeader>
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
{trashedProjects.length === 0 ? (
|
{trashedProjects.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">Recycle bin is empty.</p>
|
<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">
|
<div className="space-y-3 max-h-[360px] overflow-y-auto pr-1">
|
||||||
{trashedProjects.map((project) => (
|
{trashedProjects.map((project) => (
|
||||||
<div
|
<div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
className="flex items-start justify-between gap-3 rounded-lg border border-border bg-card/50 p-4"
|
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">
|
<div className="space-y-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-foreground truncate">{project.name}</p>
|
<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-xs text-muted-foreground break-all">{project.path}</p>
|
||||||
<p className="text-[11px] text-muted-foreground/80">
|
<p className="text-[11px] text-muted-foreground/80">
|
||||||
Trashed {new Date(project.trashedAt).toLocaleString()}
|
Trashed {new Date(project.trashedAt).toLocaleString()}
|
||||||
</p>
|
</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>
|
||||||
<div className="flex flex-col gap-2 shrink-0">
|
))}
|
||||||
<Button
|
</div>
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
<DialogFooter className="flex justify-between">
|
||||||
</Dialog>
|
<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 { 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';
|
||||||
|
|||||||
@@ -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;
|
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) => {
|
||||||
restoreTrashedProject(projectId);
|
try {
|
||||||
toast.success('Project restored', {
|
restoreTrashedProject(projectId);
|
||||||
description: 'Added back to your project list.',
|
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]
|
[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,
|
||||||
|
|||||||
Reference in New Issue
Block a user