Put home section errors on a card surface

The home page frames its content in labelled sections, so a bare inline
error floated as loose text. Wrap the home section error fallbacks
(Compliance, Trusted by, Recent updates) in a shared InlineErrorCard so a
failed section reads as intentional content on a soft card surface.

Standalone list pages (subprocessors, updates) have no such framing, so
ListErrorBoundary keeps a bare inline error, which looks cleaner there.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-15 11:02:06 +02:00
parent ac695d4516
commit 943b0ea226
6 changed files with 54 additions and 26 deletions

View File

@@ -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.
<HomeSection title={t("home.sections.compliance")}>
<InlineError
message={t("errors.inline.message")}
retryLabel={t("errors.inline.retry")}
onRetry={() => window.location.reload()}
/>
<InlineErrorCard onRetry={() => window.location.reload()} />
</HomeSection>
)}
>

View File

@@ -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.
<HomeSection title={t("home.sections.recentUpdates")}>
<InlineError
message={t("errors.inline.message")}
retryLabel={t("errors.inline.retry")}
onRetry={() => window.location.reload()}
/>
<InlineErrorCard onRetry={() => window.location.reload()} />
</HomeSection>
)}
>

View File

@@ -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.
<HomeSection title={t("home.sections.trustedBy")}>
<InlineError
message={t("errors.inline.message")}
retryLabel={t("errors.inline.retry")}
onRetry={() => window.location.reload()}
/>
<InlineErrorCard onRetry={() => window.location.reload()} />
</HomeSection>
)}
>

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// 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 (
<Card variant="soft" padding={4}>
<InlineError
message={t("errors.inline.message")}
retryLabel={t("errors.inline.retry")}
onRetry={onRetry}
/>
</Card>
);
}

View File

@@ -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);

View File

@@ -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