fix(web): stop leaking /vehicles/undefined and the raw uuid parse error
When the landing decodes a VIN for a logged-in visitor and the API returns a multi-candidate response (no single id), the code was navigating to /dashboard/vehicles/$id with id=undefined. The catalog page then asked the API for /vehicles/undefined and the backend "invalid input syntax for type uuid: 'undefined'" message landed in front of the user. Two-sided fix: - On the landing, if the decode reply has no id, hand the VIN off to /dashboard/search?vin=… instead — that page already renders the candidate-picker and the unrecognised-VIN flow. - On the vehicle detail page, gate both queries on a UUID check and render a friendly "Geçersiz araç bağlantısı / Aramaya git" panel for any malformed id. Also stop showing raw backend error messages in the generic error state — surface the localised hint instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -605,6 +605,9 @@
|
||||
"showAll": "Show all ({count})",
|
||||
"showLess": "Show less",
|
||||
"categoryDetail": "Category detail",
|
||||
"invalidLinkTitle": "Invalid vehicle link",
|
||||
"invalidLinkHint": "This link doesn't point to a known vehicle. Go back to search and try the VIN again.",
|
||||
"goToSearch": "Go to search",
|
||||
"categoryNotLoadedTitle": "Couldn't load category",
|
||||
"catalogUnavailableTitle": "Catalog isn't available right now",
|
||||
"catalogUnavailableHint": "Couldn't fetch this category from the supplier catalog. Please try again shortly."
|
||||
|
||||
@@ -605,6 +605,9 @@
|
||||
"showAll": "Tümünü göster ({count})",
|
||||
"showLess": "Daha az göster",
|
||||
"categoryDetail": "Kategori Detayı",
|
||||
"invalidLinkTitle": "Geçersiz araç bağlantısı",
|
||||
"invalidLinkHint": "Bu bağlantı tanımlı bir araca işaret etmiyor. Aramaya dönüp şase numarasını tekrar girebilirsiniz.",
|
||||
"goToSearch": "Aramaya git",
|
||||
"categoryNotLoadedTitle": "Kategori yüklenemedi",
|
||||
"catalogUnavailableTitle": "Katalog şu an yüklenemedi",
|
||||
"catalogUnavailableHint": "Bu kategori tedarikçi katalogundan alınamadı. Lütfen birazdan tekrar deneyin."
|
||||
|
||||
@@ -34,9 +34,12 @@ export const Route = createFileRoute("/dashboard/vehicles_/$id/")({
|
||||
component: VehicleDetailPage,
|
||||
});
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function VehicleDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = Route.useParams();
|
||||
const idValid = UUID_RE.test(id);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
|
||||
() => getUserSettings().categoryViewMode ?? "grid",
|
||||
);
|
||||
@@ -71,7 +74,7 @@ function VehicleDetailPage() {
|
||||
} = useQuery({
|
||||
queryKey: ["vehicle", id],
|
||||
queryFn: () => api.get<Vehicle>(`/vehicles/${id}`),
|
||||
enabled: !!id,
|
||||
enabled: !!id && idValid,
|
||||
// A missing vehicle (404) is final — don't burn retries before showing the
|
||||
// not-found state. Transient errors still retry.
|
||||
retry: (count, err) => !(err instanceof ApiError && err.status === 404) && count < 2,
|
||||
@@ -80,7 +83,7 @@ function VehicleDetailPage() {
|
||||
const { data: categoryTree, isLoading: categoriesLoading } = useQuery({
|
||||
queryKey: ["category-tree", id],
|
||||
queryFn: () => api.get<CategoryNode[]>(`/categories/tree/${id}`),
|
||||
enabled: !!id,
|
||||
enabled: !!id && idValid,
|
||||
});
|
||||
|
||||
const vehicleLabel: string = vehicle?.brandName
|
||||
@@ -102,6 +105,43 @@ function VehicleDetailPage() {
|
||||
return () => clearChatwootVehicle();
|
||||
}, [vehicle, id]);
|
||||
|
||||
// The vehicle id comes from the URL — guard the obvious "undefined" /
|
||||
// "null" / malformed cases (e.g. landing handed us a decode response with no
|
||||
// id) before they hit the backend and surface a raw uuid parse error.
|
||||
if (!idValid) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleBack}
|
||||
title={t("vehicle.back")}
|
||||
aria-label={t("vehicle.back")}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<h2 className="text-xl font-bold">{t("vehicle.invalidLinkTitle")}</h2>
|
||||
</div>
|
||||
<div
|
||||
role="alert"
|
||||
className="flex flex-col items-start gap-3 rounded-lg border border-border bg-muted/30 p-5 text-sm"
|
||||
>
|
||||
<p className="text-muted-foreground">{t("vehicle.invalidLinkHint")}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" size="sm" onClick={() => navigate({ to: "/dashboard/search" })}>
|
||||
{t("vehicle.goToSearch")}
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="ghost" onClick={handleBack}>
|
||||
{t("vehicle.back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (vehicleLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -160,11 +200,7 @@ function VehicleDetailPage() {
|
||||
{is404 ? t("vehicle.notFoundMsg") : t("vehicle.errorMsg")}
|
||||
</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{is404
|
||||
? t("vehicle.notFoundHint")
|
||||
: vehicleErrorObj instanceof Error
|
||||
? vehicleErrorObj.message
|
||||
: t("vehicle.errorHint")}
|
||||
{is404 ? t("vehicle.notFoundHint") : t("vehicle.errorHint")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -570,9 +570,17 @@ export function HomePage() {
|
||||
|
||||
setDecodeLoading(true);
|
||||
try {
|
||||
const vehicle = await api.post<{ id: string }>("/vehicles/decode", { vin: trimmed });
|
||||
capture("vin_decode_success", { vin: trimmed, source: "landing", vehicle_id: vehicle.id });
|
||||
navigate({ to: "/dashboard/vehicles/$id", params: { id: vehicle.id } });
|
||||
const vehicle = await api.post<{ id?: string }>("/vehicles/decode", { vin: trimmed });
|
||||
capture("vin_decode_success", { vin: trimmed, source: "landing", vehicle_id: vehicle?.id });
|
||||
// Decode may return a single vehicle or a multi-candidate response; only
|
||||
// jump straight to the catalog when we got a concrete id, otherwise hand
|
||||
// off to the search page (which knows how to render the candidate picker
|
||||
// and the unrecognised-VIN flow).
|
||||
if (vehicle?.id) {
|
||||
navigate({ to: "/dashboard/vehicles/$id", params: { id: vehicle.id } });
|
||||
} else {
|
||||
navigate({ to: "/dashboard/search", search: { vin: trimmed } });
|
||||
}
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ApiError ? err.message : "Bir hata oluştu. Lütfen tekrar deneyin.";
|
||||
|
||||
Reference in New Issue
Block a user