feat(v5): add user friendly error toasts
This commit is contained in:
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"react-dom": "latest",
|
||||
"react-quick-pinch-zoom": "latest",
|
||||
"recharts": "latest",
|
||||
"sonner": "^2.0.7",
|
||||
"vite": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3027,6 +3028,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/sonner/-/sonner-2.0.7.tgz",
|
||||
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://package-mirror.liara.ir/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"react-quick-pinch-zoom": "latest",
|
||||
"recharts": "latest",
|
||||
"lucide-react": "latest",
|
||||
"prop-types": "latest"
|
||||
"prop-types": "latest",
|
||||
"sonner": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "3.4.17",
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import CanvasPane, { CanvasThumbnail } from "./components/CanvasPane.jsx";
|
||||
import Controls from "./components/Controls.jsx";
|
||||
import HistogramPanel from "./components/HistogramPanel.jsx";
|
||||
import { applyStateOperation, combineStates, deleteState, getOperations, listStates, uploadImage } from "./lib/api.js";
|
||||
|
||||
function showErrorToast(error, fallback = "Something went wrong.") {
|
||||
const message = error?.message || fallback;
|
||||
toast.error(message, {
|
||||
description: "Please check the selected image, operation, and parameters."
|
||||
});
|
||||
return message;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState(null);
|
||||
const [states, setStates] = useState([]);
|
||||
@@ -26,7 +35,7 @@ export default function App() {
|
||||
setParams({ _repeat: 1, ...Object.fromEntries(Object.entries(first.params || {}).map(([key, schema]) => [key, schema.default])) });
|
||||
}
|
||||
})
|
||||
.catch((error) => setStatus(error.message));
|
||||
.catch((error) => setStatus(showErrorToast(error, "Unable to load operations.")));
|
||||
}, []);
|
||||
|
||||
async function refreshStates(sessionId, nextActiveId = null) {
|
||||
@@ -51,7 +60,7 @@ export default function App() {
|
||||
setTransform({ x: 0, y: 0, scale: 1 });
|
||||
setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded as S0.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
setStatus(showErrorToast(error, "Unable to upload image."));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -76,7 +85,7 @@ export default function App() {
|
||||
setSelectedStateIds([state.state_id]);
|
||||
setStatus(`${state.label} created.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
setStatus(showErrorToast(error, "Unable to apply operation."));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -92,7 +101,7 @@ export default function App() {
|
||||
setSelectedStateIds([state.state_id]);
|
||||
setStatus(`${state.label} created.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
setStatus(showErrorToast(error, "Unable to combine states."));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -117,7 +126,7 @@ export default function App() {
|
||||
setSelectedStateIds((current) => current.filter((id) => id !== state.state_id));
|
||||
setStatus(`${state.label} deleted.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
setStatus(showErrorToast(error, "Unable to delete state."));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -131,6 +140,7 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-zinc-950 text-zinc-100">
|
||||
<Toaster richColors theme="dark" position="top-right" closeButton />
|
||||
<Controls
|
||||
operations={operations}
|
||||
selectedOperation={selectedOperation}
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
|
||||
function humanizeErrorDetail(detail) {
|
||||
if (!detail) return "Request failed";
|
||||
if (typeof detail === "string") return detail;
|
||||
if (Array.isArray(detail)) return detail.map(humanizeErrorDetail).join(" ");
|
||||
if (typeof detail === "object") {
|
||||
return Object.entries(detail)
|
||||
.map(([key, value]) => {
|
||||
const message = humanizeErrorDetail(value);
|
||||
return key === "non_field_errors" || key === "detail" ? message : `${key}: ${message}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
return String(detail);
|
||||
}
|
||||
|
||||
async function parseResponse(response) {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.detail || "Request failed");
|
||||
throw new Error(humanizeErrorDetail(payload.detail || payload));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user