// eslint-disable-next-line no-restricted-imports -- this is the wrapper itself import { useToast as useBaseToast } from "@tailor-platform/app-shell"; import type { ReactNode } from "react"; import type { CombinedError } from "urql"; // Extract a human-readable error message from a urql CombinedError. // Backend resolver errors are wrapped as "rpc error: code = ... desc = Error: " // inside extensions.attr.error. This function extracts when available. function getErrorMessage(error: CombinedError): string { const gqlError = error.graphQLErrors[0]; const attrError = (gqlError?.extensions as Record> | undefined) ?.attr?.error; if (attrError) { const match = /desc = (?:\w+: )?(.+?)(?:\n|$)/.exec(attrError); if (match) return match[1]; } return error.message; } // Wraps useToast so that toast.error() accepts a CombinedError directly // and automatically extracts the human-readable message. export function useToast() { const toast = useBaseToast(); return { success: toast.success, error: (error: string | CombinedError) => { const message = typeof error === "string" ? error : getErrorMessage(error); toast.error(message); }, /** Error toast with rich description (supports React nodes for links etc.) */ errorRich: ( message: string, options: { description?: ReactNode; duration?: number; closeButton?: boolean }, ) => { toast.error(message, options); }, /** * Error toast that, when a `detail` is present, pins itself open (no * auto-dismiss) and shows the detail as a closable rich description; with * no detail it's a plain auto-dismissing error. Pinning matters because * `detail` carries the actual rejection reason the operator may need to * read or copy (#823) — a default ~4s timeout would hide it. */ errorMaybeRich: (message: string, detail?: string | null) => { if (detail) { toast.error(message, { description: detail, duration: Infinity, closeButton: true }); } else { toast.error(message); } }, }; }