mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 12:14:40 +00:00
[In Progress] fix(ui): show toast error when package install/uninstall fails (#440)
* fix(ui): show toast error when package install/uninstall fails HttpClient throws ApiError on HTTP 500 responses, but installPackage and uninstallPackage hooks did not catch the exception. This caused the error to propagate past the toast logic, so users saw no feedback when a system package failed to install. Wrap http.post calls in try/catch to always show toast with the server error message. Update PackageSection handlers to check res.ok instead of relying on try/catch for flow control. * fix(ui): truncate verbose package error in toast notification Extract last meaningful error line (e.g. "ERROR: ...") from package manager output instead of dumping the full multi-line response into the toast, which caused text overflow. * fix(ui): wrap long text in toast instead of truncating Add break-words and min-w-0 to toast content so long error messages (URLs, package manager output) wrap properly within the toast element. Remove shortError helper — show full error with proper wrapping. * fix(ui): constrain toast container width to prevent overflow Move max-w-sm from toast item to the fixed container so long text (URLs, multi-line errors) is properly bounded. Add overflow-hidden on toast items as a safety net. * fix(pkg-helper): re-add packages cascade-removed by apk del apk del removes orphaned dependencies, which may include other user-installed packages if they were pulled in as transitive deps. After uninstalling, check which persisted packages are still present and re-install any that were accidentally removed. * revert: remove cascade re-add logic from pkg-helper apk del does not cascade-remove other world packages (verified via Docker test). The re-add logic was based on incorrect assumption. Root cause of the all-packages-deleted issue needs further investigation.
This commit is contained in:
1 parent
00db30c2d1
commit
05c8b1f1dc
3 files changed
+42
-29
No files matched your search
@@ -23,16 +23,16 @@ function ToastItem({ toast }: { toast: Toast }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-lg border p-4 shadow-lg backdrop-blur-sm animate-in slide-in-from-bottom-2 fade-in duration-200",
|
||||
"pointer-events-auto flex w-full items-start gap-3 overflow-hidden rounded-lg border p-4 shadow-lg backdrop-blur-sm animate-in slide-in-from-bottom-2 fade-in duration-200",
|
||||
styles[toast.variant],
|
||||
)}
|
||||
role="alert"
|
||||
>
|
||||
<Icon className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<div className="flex-1 space-y-0.5">
|
||||
<p className="text-sm font-medium">{toast.title}</p>
|
||||
<div className="flex-1 min-w-0 space-y-0.5">
|
||||
<p className="text-sm font-medium break-words">{toast.title}</p>
|
||||
{toast.message && (
|
||||
<p className="text-xs opacity-80">{toast.message}</p>
|
||||
<p className="text-xs opacity-80 break-words">{toast.message}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
@@ -51,7 +51,7 @@ export function Toaster() {
|
||||
if (toasts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[100] flex flex-col-reverse gap-2 pointer-events-none sm:bottom-6 sm:right-6 safe-bottom safe-right">
|
||||
<div className="fixed bottom-4 right-4 z-[100] flex max-w-sm flex-col-reverse gap-2 pointer-events-none sm:bottom-6 sm:right-6 safe-bottom safe-right">
|
||||
{toasts.slice(-3).map((t) => (
|
||||
<ToastItem key={t.id} toast={t} />
|
||||
))}
|
||||
|
||||
@@ -21,6 +21,7 @@ interface InstallResult {
|
||||
error: string;
|
||||
}
|
||||
|
||||
|
||||
export function usePackages() {
|
||||
const http = useHttp();
|
||||
const qc = useQueryClient();
|
||||
@@ -36,27 +37,39 @@ export function usePackages() {
|
||||
const refresh = useCallback(() => { refetch(); }, [refetch]);
|
||||
|
||||
const installPackage = useCallback(async (pkg: string, t: (key: string, opts?: Record<string, string>) => string) => {
|
||||
const res = await http.post<InstallResult>("/v1/packages/install", { package: pkg });
|
||||
if (res.ok) {
|
||||
toast.success(t("messages.installSuccess", { name: pkg }));
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.all });
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.runtimes });
|
||||
} else {
|
||||
toast.error(t("messages.installError", { name: pkg }) + (res.error ? `: ${res.error}` : ""));
|
||||
try {
|
||||
const res = await http.post<InstallResult>("/v1/packages/install", { package: pkg });
|
||||
if (res.ok) {
|
||||
toast.success(t("messages.installSuccess", { name: pkg }));
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.all });
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.runtimes });
|
||||
} else {
|
||||
toast.error(t("messages.installError", { name: pkg }) + (res.error ? `: ${res.error}` : ""));
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
toast.error(t("messages.installError", { name: pkg }) + (msg ? `: ${msg}` : ""));
|
||||
return { ok: false, error: msg } as InstallResult;
|
||||
}
|
||||
return res;
|
||||
}, [http, qc]);
|
||||
|
||||
const uninstallPackage = useCallback(async (pkg: string, t: (key: string, opts?: Record<string, string>) => string) => {
|
||||
const res = await http.post<InstallResult>("/v1/packages/uninstall", { package: pkg });
|
||||
if (res.ok) {
|
||||
toast.success(t("messages.uninstallSuccess", { name: pkg }));
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.all });
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.runtimes });
|
||||
} else {
|
||||
toast.error(t("messages.uninstallError", { name: pkg }) + (res.error ? `: ${res.error}` : ""));
|
||||
try {
|
||||
const res = await http.post<InstallResult>("/v1/packages/uninstall", { package: pkg });
|
||||
if (res.ok) {
|
||||
toast.success(t("messages.uninstallSuccess", { name: pkg }));
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.all });
|
||||
qc.invalidateQueries({ queryKey: queryKeys.packages.runtimes });
|
||||
} else {
|
||||
toast.error(t("messages.uninstallError", { name: pkg }) + (res.error ? `: ${res.error}` : ""));
|
||||
}
|
||||
return res;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
toast.error(t("messages.uninstallError", { name: pkg }) + (msg ? `: ${msg}` : ""));
|
||||
return { ok: false, error: msg } as InstallResult;
|
||||
}
|
||||
return res;
|
||||
}, [http, qc]);
|
||||
|
||||
return { packages: data, loading, refresh, installPackage, uninstallPackage };
|
||||
|
||||
@@ -100,8 +100,8 @@ interface PackageSectionProps {
|
||||
placeholder: string;
|
||||
packages: PackageInfo[] | null | undefined;
|
||||
loading: boolean;
|
||||
onInstall: (pkg: string) => Promise<unknown>;
|
||||
onUninstall: (pkg: string) => Promise<unknown>;
|
||||
onInstall: (pkg: string) => Promise<{ ok: boolean }>;
|
||||
onUninstall: (pkg: string) => Promise<{ ok: boolean }>;
|
||||
}
|
||||
|
||||
function PackageSection({ title, placeholder, packages, loading, onInstall, onUninstall }: PackageSectionProps) {
|
||||
@@ -115,12 +115,12 @@ function PackageSection({ title, placeholder, packages, loading, onInstall, onUn
|
||||
const pkg = input.trim();
|
||||
if (!pkg) return;
|
||||
setInstallStatus("loading");
|
||||
try {
|
||||
await onInstall(pkg);
|
||||
const res = await onInstall(pkg);
|
||||
if (res.ok) {
|
||||
setInstallStatus("success");
|
||||
setInput("");
|
||||
setTimeout(() => setInstallStatus("idle"), 2000);
|
||||
} catch {
|
||||
} else {
|
||||
setInstallStatus("error");
|
||||
setTimeout(() => setInstallStatus("idle"), 3000);
|
||||
}
|
||||
@@ -128,10 +128,10 @@ function PackageSection({ title, placeholder, packages, loading, onInstall, onUn
|
||||
|
||||
async function handleUninstall(name: string) {
|
||||
setActionStatuses((s) => ({ ...s, [name]: "loading" }));
|
||||
try {
|
||||
await onUninstall(name);
|
||||
const res = await onUninstall(name);
|
||||
if (res.ok) {
|
||||
setActionStatuses((s) => ({ ...s, [name]: "success" }));
|
||||
} catch {
|
||||
} else {
|
||||
setActionStatuses((s) => ({ ...s, [name]: "error" }));
|
||||
setTimeout(() => setActionStatuses((s) => ({ ...s, [name]: "idle" })), 3000);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user