mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
An admin page stacks independent cards, and a card that threw while rendering replaced the whole page, so a field missing from the off-host card's payload hid the snapshot table beside it. Every card in an admin section now sits in its own boundary and fails as an error card with a retry that remounts it; the rest of the page keeps working.
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
import { Component, Fragment, type ReactNode } from "react";
|
|
|
|
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
|
|
/**
|
|
* One admin card's error boundary.
|
|
*
|
|
* An admin page stacks independent cards, each reading its own endpoint. A
|
|
* card that threw while rendering took the whole page with it: a field the
|
|
* off-host card read and a stale payload did not carry left the backups page
|
|
* without its snapshot table, the one card the operator had come for. Each
|
|
* card now fails alone, as a `QueryErrorCard` in its place, and the others
|
|
* keep working. "Try again" remounts the card, which refetches what it reads;
|
|
* the error still reaches the client error tracker.
|
|
*/
|
|
export function AdminCardBoundary({ children }: { children: ReactNode }) {
|
|
return (
|
|
<Boundary renderFallback={(retry) => <AdminCardError onRetry={retry} />}>
|
|
{children}
|
|
</Boundary>
|
|
);
|
|
}
|
|
|
|
/** What a failed card paints in its place. */
|
|
export function AdminCardError({ onRetry }: { onRetry: () => void }) {
|
|
const { t } = useTranslations();
|
|
return (
|
|
<QueryErrorCard
|
|
title={t("admin.cardError.title")}
|
|
description={t("admin.cardError.description")}
|
|
onRetry={onRetry}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export interface BoundaryProps {
|
|
renderFallback: (retry: () => void) => ReactNode;
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface BoundaryState {
|
|
failed: boolean;
|
|
attempt: number;
|
|
}
|
|
|
|
/** The boundary itself, exported for its unit test. */
|
|
export class Boundary extends Component<BoundaryProps, BoundaryState> {
|
|
state: BoundaryState = { failed: false, attempt: 0 };
|
|
|
|
static getDerivedStateFromError(): Pick<BoundaryState, "failed"> {
|
|
return { failed: true };
|
|
}
|
|
|
|
componentDidCatch(error: Error): void {
|
|
if (typeof window !== "undefined") {
|
|
const g = window as typeof window & {
|
|
__healthlog_onError?: (err: Error) => void;
|
|
};
|
|
g.__healthlog_onError?.(error);
|
|
}
|
|
}
|
|
|
|
retry = (): void => {
|
|
this.setState((s) => ({ failed: false, attempt: s.attempt + 1 }));
|
|
};
|
|
|
|
render(): ReactNode {
|
|
if (this.state.failed) return this.props.renderFallback(this.retry);
|
|
// A new key on retry remounts the card, so it starts from a clean state
|
|
// rather than re-rendering the tree that threw.
|
|
return <Fragment key={this.state.attempt}>{this.props.children}</Fragment>;
|
|
}
|
|
}
|