diff --git a/src/components/CustomErrorToast.tsx b/src/components/CustomErrorToast.tsx new file mode 100644 index 0000000..6674667 --- /dev/null +++ b/src/components/CustomErrorToast.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { toast } from "sonner"; +import { X, Copy, Check } from "lucide-react"; + +interface CustomErrorToastProps { + message: string; + toastId: string | number; + copied?: boolean; + onCopy?: () => void; +} + +export function CustomErrorToast({ + message, + toastId, + copied = false, + onCopy, +}: CustomErrorToastProps) { + const handleClose = () => { + toast.dismiss(toastId); + }; + + const handleCopy = () => { + if (onCopy) { + onCopy(); + } + }; + + return ( +
+ {/* Content */} +
+
+
+
+
+
+ +
+
+

Error

+ + {/* Action buttons */} +
+ + +
+
+
+

+ {message} +

+
+
+
+
+
+ ); +} diff --git a/src/hooks/useLoadApp.ts b/src/hooks/useLoadApp.ts index a0f70be..2b3e4c3 100644 --- a/src/hooks/useLoadApp.ts +++ b/src/hooks/useLoadApp.ts @@ -23,7 +23,9 @@ export function useLoadApp(appId: number | null) { return ipcClient.getApp(appId); }, enabled: appId !== null, - meta: { showErrorToast: true }, + // Deliberately not showing error toast here because + // this will pop up when app is deleted. + // meta: { showErrorToast: true }, }); useEffect(() => { diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index 3ac7b8d..e6097d4 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -669,24 +669,25 @@ export function registerAppHandlers() { } } + // Delete app from database + try { + await db.delete(apps).where(eq(apps.id, appId)); + // Note: Associated chats will cascade delete + } catch (error: any) { + logger.error(`Error deleting app ${appId} from database:`, error); + throw new Error( + `Failed to delete app from database: ${error.message}`, + ); + } + // Delete app files const appPath = getDyadAppPath(app.path); try { await fsPromises.rm(appPath, { recursive: true, force: true }); } catch (error: any) { logger.error(`Error deleting app files for app ${appId}:`, error); - throw new Error(`Failed to delete app files: ${error.message}`); - } - - // Delete app from database - try { - await db.delete(apps).where(eq(apps.id, appId)); - // Note: Associated chats will cascade delete if that's set up in the schema - return; - } catch (error: any) { - logger.error(`Error deleting app ${appId} from database:`, error); throw new Error( - `Failed to delete app from database: ${error.message}`, + `App deleted from database, but failed to delete app files. Please delete app files from ${appPath} manually.\n\nError: ${error.message}`, ); } }); diff --git a/src/ipc/handlers/version_handlers.ts b/src/ipc/handlers/version_handlers.ts index f00b742..827a53d 100644 --- a/src/ipc/handlers/version_handlers.ts +++ b/src/ipc/handlers/version_handlers.ts @@ -22,7 +22,8 @@ export function registerVersionHandlers() { }); if (!app) { - throw new Error("App not found"); + // The app might have just been deleted, so we return an empty array. + return []; } const appPath = getDyadAppPath(app.path); diff --git a/src/lib/toast.ts b/src/lib/toast.tsx similarity index 54% rename from src/lib/toast.ts rename to src/lib/toast.tsx index 8ed344f..8f8856b 100644 --- a/src/lib/toast.ts +++ b/src/lib/toast.tsx @@ -1,5 +1,7 @@ import { toast } from "sonner"; import { PostHog } from "posthog-js"; +import React from "react"; +import { CustomErrorToast } from "../components/CustomErrorToast"; /** * Toast utility functions for consistent notifications across the app @@ -18,8 +20,54 @@ export const showSuccess = (message: string) => { * @param message The error message to display */ export const showError = (message: any) => { - toast.error(message.toString()); + const errorMessage = message.toString(); console.error(message); + + const onCopy = (toastId: string | number) => { + navigator.clipboard.writeText(errorMessage); + + // Update the toast to show the 'copied' state + toast.custom( + (t) => ( + onCopy(t)} + /> + ), + { id: toastId, duration: Infinity }, + ); + + // After 2 seconds, revert the toast back to the original state + setTimeout(() => { + toast.custom( + (t) => ( + onCopy(t)} + /> + ), + { id: toastId, duration: Infinity }, + ); + }, 2000); + }; + + // Use custom error toast with enhanced features + const toastId = toast.custom( + (t) => ( + onCopy(t)} + /> + ), + { duration: 4000 }, + ); + + return toastId; }; /** @@ -39,26 +87,6 @@ export const showInfo = (message: string) => { toast.info(message); }; -/** - * Show a loading toast that can be updated with success/error - * @param loadingMessage The message to show while loading - * @param promise The promise to track - * @param successMessage Optional success message - * @param errorMessage Optional error message - */ -export const showLoading = ( - loadingMessage: string, - promise: Promise, - successMessage?: string, - errorMessage?: string, -) => { - return toast.promise(promise, { - loading: loadingMessage, - success: () => successMessage || "Operation completed successfully", - error: (err) => errorMessage || `Error: ${err.message || "Unknown error"}`, - }); -}; - export const showExtraFilesToast = ({ files, error, diff --git a/src/pages/app-details.tsx b/src/pages/app-details.tsx index c4fce3d..64a021e 100644 --- a/src/pages/app-details.tsx +++ b/src/pages/app-details.tsx @@ -86,6 +86,7 @@ export default function AppDetailsPage() { await refreshApps(); navigate({ to: "/", search: {} }); } catch (error) { + setIsDeleteDialogOpen(false); showError(error); } finally { setIsDeleting(false);