From 04a5ae48e2a11232228047ad67f05a8c06890980 Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Fri, 26 Dec 2025 12:36:57 +0100 Subject: [PATCH 1/2] refactor: replace window.confirm with React dialogs in trash operations --- apps/ui/src/components/layout/sidebar.tsx | 12 +- .../layout/sidebar/dialogs/trash-dialog.tsx | 211 ++++++++++++------ .../components/layout/sidebar/hooks/index.ts | 1 - .../layout/sidebar/hooks/use-trash-dialog.ts | 40 ---- .../sidebar/hooks/use-trash-operations.ts | 38 ++-- 5 files changed, 160 insertions(+), 142 deletions(-) delete mode 100644 apps/ui/src/components/layout/sidebar/hooks/use-trash-dialog.ts diff --git a/apps/ui/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar.tsx index 12a20113b..0ad8804d9 100644 --- a/apps/ui/src/components/layout/sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar.tsx @@ -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 diff --git a/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx b/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx index bb2314367..74c6b3e16 100644 --- a/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx +++ b/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx @@ -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,144 @@ export function TrashDialog({ handleEmptyTrash, isEmptyingTrash, }: TrashDialogProps) { + // Confirmation dialog state (managed internally to avoid prop drilling) + const [deleteFromDiskProject, setDeleteFromDiskProject] = useState(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 ( - - - - Recycle Bin - - Restore projects to the sidebar or delete their folders using your system Trash. - - + <> + + + + Recycle Bin + + Restore projects to the sidebar or delete their folders using your system Trash. + + - {trashedProjects.length === 0 ? ( -

Recycle bin is empty.

- ) : ( -
- {trashedProjects.map((project) => ( -
-
-

{project.name}

-

{project.path}

-

- Trashed {new Date(project.trashedAt).toLocaleString()} -

+ {trashedProjects.length === 0 ? ( +

Recycle bin is empty.

+ ) : ( +
+ {trashedProjects.map((project) => ( +
+
+

{project.name}

+

{project.path}

+

+ Trashed {new Date(project.trashedAt).toLocaleString()} +

+
+
+ + + +
-
- - - -
-
- ))} -
- )} + ))} +
+ )} - - - {trashedProjects.length > 0 && ( - - )} - - -
+ {trashedProjects.length > 0 && ( + + )} + +
+
+ + {/* Delete from disk confirmation dialog */} + !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 */} + + ); } diff --git a/apps/ui/src/components/layout/sidebar/hooks/index.ts b/apps/ui/src/components/layout/sidebar/hooks/index.ts index caa900a77..ca6ce898e 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/index.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/index.ts @@ -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'; diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-trash-dialog.ts b/apps/ui/src/components/layout/sidebar/hooks/use-trash-dialog.ts deleted file mode 100644 index 74c1ee9b5..000000000 --- a/apps/ui/src/components/layout/sidebar/hooks/use-trash-dialog.ts +++ /dev/null @@ -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, - }; -} diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts b/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts index bb0dc5714..2112bc376 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts @@ -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(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, From 3bb9d27dc64f1ef350db0f2bab6a62f5929d1ef7 Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Fri, 26 Dec 2025 12:51:53 +0100 Subject: [PATCH 2/2] refactor: simplify DeleteConfirmDialog rendering in TrashDialog component --- .../layout/sidebar/dialogs/trash-dialog.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx b/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx index 74c6b3e16..a26837da8 100644 --- a/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx +++ b/apps/ui/src/components/layout/sidebar/dialogs/trash-dialog.tsx @@ -151,16 +151,18 @@ export function TrashDialog({ {/* Delete from disk confirmation dialog */} - !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" - /> + {deleteFromDiskProject && ( + !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 */}