Make delete app better handled & revamp error toast (#422)
Fixes #395 Fixes #270 Fixes #268
This commit is contained in:
80
src/components/CustomErrorToast.tsx
Normal file
80
src/components/CustomErrorToast.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user