Make delete app better handled & revamp error toast (#422)

Fixes #395 
Fixes #270 
Fixes #268
This commit is contained in:
Will Chen
2025-06-16 23:36:43 -07:00
committed by GitHub
parent df38fb0f80
commit 2fc33d04c1
6 changed files with 147 additions and 34 deletions

View File

@@ -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 (
<div className="relative bg-red-50/95 backdrop-blur-sm border border-red-200 rounded-xl shadow-lg min-w-[400px] max-w-[500px] overflow-hidden">
{/* Content */}
<div className="p-4">
<div className="flex items-start">
<div className="flex-1">
<div className="flex items-center mb-3">
<div className="flex-shrink-0">
<div className="w-5 h-5 bg-gradient-to-br from-red-400 to-red-500 rounded-full flex items-center justify-center shadow-sm">
<X className="w-3 h-3 text-white" />
</div>
</div>
<h3 className="ml-3 text-sm font-medium text-red-900">Error</h3>
{/* Action buttons */}
<div className="flex items-center space-x-1.5 ml-auto">
<button
onClick={(e) => {
e.stopPropagation();
handleCopy();
}}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150"
title="Copy to clipboard"
>
{copied ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleClose();
}}
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-100/70 rounded-lg transition-all duration-150"
title="Close"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
<div>
<p className="text-sm text-red-800 leading-relaxed whitespace-pre-wrap bg-red-100/50 backdrop-blur-sm p-3 rounded-lg border border-red-200/50">
{message}
</p>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -23,7 +23,9 @@ export function useLoadApp(appId: number | null) {
return ipcClient.getApp(appId); return ipcClient.getApp(appId);
}, },
enabled: appId !== null, 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(() => { useEffect(() => {

View File

@@ -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 // Delete app files
const appPath = getDyadAppPath(app.path); const appPath = getDyadAppPath(app.path);
try { try {
await fsPromises.rm(appPath, { recursive: true, force: true }); await fsPromises.rm(appPath, { recursive: true, force: true });
} catch (error: any) { } catch (error: any) {
logger.error(`Error deleting app files for app ${appId}:`, error); 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( 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}`,
); );
} }
}); });

View File

@@ -22,7 +22,8 @@ export function registerVersionHandlers() {
}); });
if (!app) { 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); const appPath = getDyadAppPath(app.path);

View File

@@ -1,5 +1,7 @@
import { toast } from "sonner"; import { toast } from "sonner";
import { PostHog } from "posthog-js"; import { PostHog } from "posthog-js";
import React from "react";
import { CustomErrorToast } from "../components/CustomErrorToast";
/** /**
* Toast utility functions for consistent notifications across the app * 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 * @param message The error message to display
*/ */
export const showError = (message: any) => { export const showError = (message: any) => {
toast.error(message.toString()); const errorMessage = message.toString();
console.error(message); console.error(message);
const onCopy = (toastId: string | number) => {
navigator.clipboard.writeText(errorMessage);
// Update the toast to show the 'copied' state
toast.custom(
(t) => (
<CustomErrorToast
message={errorMessage}
toastId={t}
copied={true}
onCopy={() => onCopy(t)}
/>
),
{ id: toastId, duration: Infinity },
);
// After 2 seconds, revert the toast back to the original state
setTimeout(() => {
toast.custom(
(t) => (
<CustomErrorToast
message={errorMessage}
toastId={t}
copied={false}
onCopy={() => onCopy(t)}
/>
),
{ id: toastId, duration: Infinity },
);
}, 2000);
};
// Use custom error toast with enhanced features
const toastId = toast.custom(
(t) => (
<CustomErrorToast
message={errorMessage}
toastId={t}
onCopy={() => onCopy(t)}
/>
),
{ duration: 4000 },
);
return toastId;
}; };
/** /**
@@ -39,26 +87,6 @@ export const showInfo = (message: string) => {
toast.info(message); 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 = <T>(
loadingMessage: string,
promise: Promise<T>,
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 = ({ export const showExtraFilesToast = ({
files, files,
error, error,

View File

@@ -86,6 +86,7 @@ export default function AppDetailsPage() {
await refreshApps(); await refreshApps();
navigate({ to: "/", search: {} }); navigate({ to: "/", search: {} });
} catch (error) { } catch (error) {
setIsDeleteDialogOpen(false);
showError(error); showError(error);
} finally { } finally {
setIsDeleting(false); setIsDeleting(false);