diff --git a/apps/compliance-portal/src/components/ComplianceFrameworks/ComplianceFrameworksSection.tsx b/apps/compliance-portal/src/components/ComplianceFrameworks/ComplianceFrameworksSection.tsx index 412876367..0e1310a47 100644 --- a/apps/compliance-portal/src/components/ComplianceFrameworks/ComplianceFrameworksSection.tsx +++ b/apps/compliance-portal/src/components/ComplianceFrameworks/ComplianceFrameworksSection.tsx @@ -19,10 +19,10 @@ // SOFTWARE. import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary"; -import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError"; import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; +import { InlineErrorCard } from "#/components/errors/InlineErrorCard"; import { HomeSection } from "#/components/HomeSection/HomeSection"; import type { ComplianceFrameworksSection_trustCenter$key } from "./__generated__/ComplianceFrameworksSection_trustCenter.graphql"; @@ -60,11 +60,7 @@ export function ComplianceFrameworksSection({ trustCenterKey }: ComplianceFramew // The data comes from the preloaded HomePageQuery, so there is no local // refetch to clear a field error — reload the page to recover. - window.location.reload()} - /> + window.location.reload()} /> )} > diff --git a/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx b/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx index 77bfa3807..8b5b6b701 100644 --- a/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx +++ b/apps/compliance-portal/src/components/RecentUpdates/RecentUpdatesSection.tsx @@ -20,10 +20,10 @@ import { Link } from "@probo/ui/src/v2/Button/Link"; import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary"; -import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError"; import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; +import { InlineErrorCard } from "#/components/errors/InlineErrorCard"; import { HomeSection } from "#/components/HomeSection/HomeSection"; import { MailingListUpdateListItem } from "#/components/MailingListUpdateListItem/MailingListUpdateListItem"; import { dotPatternStyle } from "#/components/MediaTile/variants"; @@ -60,11 +60,7 @@ export function RecentUpdatesSection({ trustCenterKey }: RecentUpdatesSectionPro // The data comes from the preloaded HomePageQuery, so there is no local // refetch to clear a field error — reload the page to recover. - window.location.reload()} - /> + window.location.reload()} /> )} > diff --git a/apps/compliance-portal/src/components/TrustedBy/TrustedBySection.tsx b/apps/compliance-portal/src/components/TrustedBy/TrustedBySection.tsx index 323520dd5..6d718d4c4 100644 --- a/apps/compliance-portal/src/components/TrustedBy/TrustedBySection.tsx +++ b/apps/compliance-portal/src/components/TrustedBy/TrustedBySection.tsx @@ -19,10 +19,10 @@ // SOFTWARE. import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary"; -import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError"; import { useTranslation } from "react-i18next"; import { graphql, useFragment } from "react-relay"; +import { InlineErrorCard } from "#/components/errors/InlineErrorCard"; import { HomeSection } from "#/components/HomeSection/HomeSection"; import type { TrustedBySection_trustCenter$key } from "./__generated__/TrustedBySection_trustCenter.graphql"; @@ -58,11 +58,7 @@ export function TrustedBySection({ trustCenterKey }: TrustedBySectionProps) { // The data comes from the preloaded HomePageQuery, so there is no local // refetch to clear a field error — reload the page to recover. - window.location.reload()} - /> + window.location.reload()} /> )} > diff --git a/apps/compliance-portal/src/components/errors/InlineErrorCard.tsx b/apps/compliance-portal/src/components/errors/InlineErrorCard.tsx new file mode 100644 index 000000000..0f3cfe791 --- /dev/null +++ b/apps/compliance-portal/src/components/errors/InlineErrorCard.tsx @@ -0,0 +1,39 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { Card } from "@probo/ui/src/v2/Card/Card"; +import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError"; +import { useTranslation } from "react-i18next"; + +interface InlineErrorCardProps { + // Retry handler. When omitted, the retry action is hidden. + onRetry?: () => void; +} + +// The standard section/list error fallback: the vertical InlineError on a soft +// card surface, so a failed region reads as intentional content rather than +// floating text. Row-level failures use the bare horizontal InlineError. +export function InlineErrorCard({ onRetry }: InlineErrorCardProps) { + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/apps/compliance-portal/src/components/errors/ListErrorBoundary.tsx b/apps/compliance-portal/src/components/errors/ListErrorBoundary.tsx index 60cbde989..50096cbfc 100644 --- a/apps/compliance-portal/src/components/errors/ListErrorBoundary.tsx +++ b/apps/compliance-portal/src/components/errors/ListErrorBoundary.tsx @@ -25,11 +25,12 @@ interface ListErrorBoundaryProps { children: ReactNode; } -// Contains a list/section field error to an inline fallback with a working -// retry. The boundary only resets *after* the caller's refetch settles (via the -// `done` callback bumping its key), so remounting reads the refreshed store -// instead of racing the in-flight request back into the same error. See -// contrib/claude/error-handling.md. +// Contains a list field error to an inline fallback with a working retry. A +// standalone list page has no section framing, so the fallback is a bare +// InlineError (no card). The boundary only resets *after* the caller's refetch +// settles (via the `done` callback bumping its key), so remounting reads the +// refreshed store instead of racing the in-flight request back into the same +// error. See contrib/claude/error-handling.md. export function ListErrorBoundary({ onRetry, children }: ListErrorBoundaryProps) { const { t } = useTranslation(); const [resetToken, setResetToken] = useState(0); diff --git a/apps/compliance-portal/src/lib/relay/fetch.ts b/apps/compliance-portal/src/lib/relay/fetch.ts index 302c45ef2..35010fef4 100644 --- a/apps/compliance-portal/src/lib/relay/fetch.ts +++ b/apps/compliance-portal/src/lib/relay/fetch.ts @@ -26,6 +26,8 @@ import { type FetchFunction, type GraphQLResponse } from "relay-runtime"; const isRequestLevel = (error: GraphQLError) => error.path === undefined || error.path === null || error.path.length === 0; +type ErrorResponse = GraphQLResponse & { errors?: GraphQLError[] }; + // The portal fetch only throws for request-level failures. Everything else // (field-level errors) flows through to Relay untouched. export const makeFetchQuery = (endpoint: string): FetchFunction => { @@ -82,9 +84,7 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => { throw new InternalServerError(); } - const json = (await response.json()) as GraphQLResponse & { - errors?: GraphQLError[]; - }; + const json = (await response.json()) as ErrorResponse; if (json.errors) { // An unauthenticated session is always a global concern: the backend