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:
@@ -19,10 +19,10 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
||||||
import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import { InlineErrorCard } from "#/components/errors/InlineErrorCard";
|
||||||
import { HomeSection } from "#/components/HomeSection/HomeSection";
|
import { HomeSection } from "#/components/HomeSection/HomeSection";
|
||||||
|
|
||||||
import type { ComplianceFrameworksSection_trustCenter$key } from "./__generated__/ComplianceFrameworksSection_trustCenter.graphql";
|
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
|
// The data comes from the preloaded HomePageQuery, so there is no local
|
||||||
// refetch to clear a field error — reload the page to recover.
|
// refetch to clear a field error — reload the page to recover.
|
||||||
<HomeSection title={t("home.sections.compliance")}>
|
<HomeSection title={t("home.sections.compliance")}>
|
||||||
<InlineError
|
<InlineErrorCard onRetry={() => window.location.reload()} />
|
||||||
message={t("errors.inline.message")}
|
|
||||||
retryLabel={t("errors.inline.retry")}
|
|
||||||
onRetry={() => window.location.reload()}
|
|
||||||
/>
|
|
||||||
</HomeSection>
|
</HomeSection>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
|
|
||||||
import { Link } from "@probo/ui/src/v2/Button/Link";
|
import { Link } from "@probo/ui/src/v2/Button/Link";
|
||||||
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
||||||
import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import { InlineErrorCard } from "#/components/errors/InlineErrorCard";
|
||||||
import { HomeSection } from "#/components/HomeSection/HomeSection";
|
import { HomeSection } from "#/components/HomeSection/HomeSection";
|
||||||
import { MailingListUpdateListItem } from "#/components/MailingListUpdateListItem/MailingListUpdateListItem";
|
import { MailingListUpdateListItem } from "#/components/MailingListUpdateListItem/MailingListUpdateListItem";
|
||||||
import { dotPatternStyle } from "#/components/MediaTile/variants";
|
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
|
// The data comes from the preloaded HomePageQuery, so there is no local
|
||||||
// refetch to clear a field error — reload the page to recover.
|
// refetch to clear a field error — reload the page to recover.
|
||||||
<HomeSection title={t("home.sections.recentUpdates")}>
|
<HomeSection title={t("home.sections.recentUpdates")}>
|
||||||
<InlineError
|
<InlineErrorCard onRetry={() => window.location.reload()} />
|
||||||
message={t("errors.inline.message")}
|
|
||||||
retryLabel={t("errors.inline.retry")}
|
|
||||||
onRetry={() => window.location.reload()}
|
|
||||||
/>
|
|
||||||
</HomeSection>
|
</HomeSection>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
// SOFTWARE.
|
// SOFTWARE.
|
||||||
|
|
||||||
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary";
|
||||||
import { InlineError } from "@probo/ui/src/v2/InlineError/InlineError";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { graphql, useFragment } from "react-relay";
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import { InlineErrorCard } from "#/components/errors/InlineErrorCard";
|
||||||
import { HomeSection } from "#/components/HomeSection/HomeSection";
|
import { HomeSection } from "#/components/HomeSection/HomeSection";
|
||||||
|
|
||||||
import type { TrustedBySection_trustCenter$key } from "./__generated__/TrustedBySection_trustCenter.graphql";
|
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
|
// The data comes from the preloaded HomePageQuery, so there is no local
|
||||||
// refetch to clear a field error — reload the page to recover.
|
// refetch to clear a field error — reload the page to recover.
|
||||||
<HomeSection title={t("home.sections.trustedBy")}>
|
<HomeSection title={t("home.sections.trustedBy")}>
|
||||||
<InlineError
|
<InlineErrorCard onRetry={() => window.location.reload()} />
|
||||||
message={t("errors.inline.message")}
|
|
||||||
retryLabel={t("errors.inline.retry")}
|
|
||||||
onRetry={() => window.location.reload()}
|
|
||||||
/>
|
|
||||||
</HomeSection>
|
</HomeSection>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,11 +25,12 @@ interface ListErrorBoundaryProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains a list/section field error to an inline fallback with a working
|
// Contains a list field error to an inline fallback with a working retry. A
|
||||||
// retry. The boundary only resets *after* the caller's refetch settles (via the
|
// standalone list page has no section framing, so the fallback is a bare
|
||||||
// `done` callback bumping its key), so remounting reads the refreshed store
|
// InlineError (no card). The boundary only resets *after* the caller's refetch
|
||||||
// instead of racing the in-flight request back into the same error. See
|
// settles (via the `done` callback bumping its key), so remounting reads the
|
||||||
// contrib/claude/error-handling.md.
|
// 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) {
|
export function ListErrorBoundary({ onRetry, children }: ListErrorBoundaryProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [resetToken, setResetToken] = useState(0);
|
const [resetToken, setResetToken] = useState(0);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { type FetchFunction, type GraphQLResponse } from "relay-runtime";
|
|||||||
const isRequestLevel = (error: GraphQLError) =>
|
const isRequestLevel = (error: GraphQLError) =>
|
||||||
error.path === undefined || error.path === null || error.path.length === 0;
|
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
|
// The portal fetch only throws for request-level failures. Everything else
|
||||||
// (field-level errors) flows through to Relay untouched.
|
// (field-level errors) flows through to Relay untouched.
|
||||||
export const makeFetchQuery = (endpoint: string): FetchFunction => {
|
export const makeFetchQuery = (endpoint: string): FetchFunction => {
|
||||||
@@ -82,9 +84,7 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => {
|
|||||||
throw new InternalServerError();
|
throw new InternalServerError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = (await response.json()) as GraphQLResponse & {
|
const json = (await response.json()) as ErrorResponse;
|
||||||
errors?: GraphQLError[];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
// An unauthenticated session is always a global concern: the backend
|
// An unauthenticated session is always a global concern: the backend
|
||||||
|
|||||||
Reference in New Issue
Block a user