Files
accounted/components/ui/toaster.tsx
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

63 lines
2.1 KiB
TypeScript

"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
import { isValidElement, type ReactNode } from "react"
import { getErrorMessage } from "@/lib/errors/get-error-message"
/**
* The Toaster lives in the ROOT layout, as a sibling of {children}. A throw in
* here escapes every segment error boundary and lands on global-error, which
* blanks the entire app. Callers that hand over an unchecked `await
* res.json()` field pass the canonical `{ code, message, … }` envelope object
* rather than a string, and React throws "Objects are not valid as a React
* child" on it.
*
* Individual call sites are still expected to run their errors through
* getErrorMessage. This is the choke point that keeps the one that forgets
* from taking the whole app down with it.
*/
export function coerceToastNode(value: ReactNode): ReactNode {
if (value == null || typeof value === "string" || typeof value === "number") return value
if (typeof value === "boolean") return null
if (isValidElement(value)) return value
// Arrays are legitimate React children, but an unchecked JSON array can hold
// objects, and one of those still throws. Coerce members too; elements pass
// through by identity so their keys survive.
if (Array.isArray(value)) return value.map(coerceToastNode)
return getErrorMessage(value)
}
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
const safeTitle = coerceToastNode(title)
const safeDescription = coerceToastNode(description)
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{safeTitle && <ToastTitle>{safeTitle}</ToastTitle>}
{safeDescription && (
<ToastDescription>{safeDescription}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}