fix(ui): route bare read-failure branches through QueryErrorCard

Eight data surfaces still rendered a read failure as a bare
text-destructive paragraph with no retry: the biomarker manager, the lab
values list, the three record managers (allergies, family history,
conditions), the illness episode detail, and the intake history table;
the lab biomarker detail carried an inline near-duplicate of the
primitive from before it existed. Replace all of them with
QueryErrorCard wired to the query's refetch, keeping each surface's own
error string. Inline role="alert" text remains the pattern for mutation
errors inside forms.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 16:33:10 +02:00
1 parent 21b65788ae
commit 9a35cfb3e9
8 files changed
+54 -41

No files matched your search

@@ -13,6 +13,7 @@ import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { Skeleton } from "@/components/ui/skeleton";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
@@ -35,7 +36,12 @@ function todayLocal(): string {
export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
const { t } = useTranslations();
const fmt = useFormatters();
const { data: episode, isLoading, isError } = useIllnessEpisode(episodeId);
const {
data: episode,
isLoading,
isError,
refetch,
} = useIllnessEpisode(episodeId);
const resolve = useResolveEpisode();
const [logOpen, setLogOpen] = useState(false);
@@ -44,9 +50,10 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
if (isError) {
return (
<p className="text-destructive py-8 text-center text-sm">
{t("illness.loadError")}
</p>
<QueryErrorCard
title={t("illness.loadError")}
onRetry={() => void refetch()}
/>
);
}
+6 -4
View File
@@ -8,6 +8,7 @@ import { toast } from "sonner";
import { DeleteButton } from "@/components/data-list";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch";
@@ -40,7 +41,7 @@ export function BiomarkerManager() {
const [editing, setEditing] = useState<BiomarkerDto | null>(null);
const [formFooterEl, setFormFooterEl] = useState<HTMLDivElement | null>(null);
const { data, isLoading, isError } = useQuery({
const { data, isLoading, isError, refetch } = useQuery({
queryKey: queryKeys.biomarkers(),
queryFn: () => apiGet<BiomarkerListResponse>("/api/biomarkers"),
});
@@ -174,9 +175,10 @@ export function BiomarkerManager() {
))}
</div>
) : isError ? (
<p className="text-destructive py-6 text-center text-sm">
{t("labs.biomarker.loadError")}
</p>
<QueryErrorCard
title={t("labs.biomarker.loadError")}
onRetry={() => void refetch()}
/>
) : markers.length === 0 ? (
<EmptyState
icon={<FlaskConical className="size-6" />}
+8 -15
View File
@@ -16,6 +16,7 @@ import { DeleteButton } from "@/components/data-list";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
@@ -248,21 +249,13 @@ export function LabBiomarkerDetail({ biomarkerId }: { biomarkerId: string }) {
{description}
</p>
</header>
<Card>
<CardContent className="flex flex-col items-center gap-3 py-8 text-center">
<p className="text-destructive text-sm">{t("labs.loadError")}</p>
<Button
variant="outline"
size="sm"
onClick={() => {
if (markerError) void refetchMarker();
if (listError) void refetchList();
}}
>
{t("common.retry")}
</Button>
</CardContent>
</Card>
<QueryErrorCard
title={t("labs.loadError")}
onRetry={() => {
if (markerError) void refetchMarker();
if (listError) void refetchList();
}}
/>
</div>
);
}
+6 -3
View File
@@ -5,6 +5,7 @@ import { useInfiniteQuery } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { Skeleton } from "@/components/ui/skeleton";
import { apiGet } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
@@ -33,6 +34,7 @@ export function LabValuesList({ biomarkerId }: { biomarkerId: string }) {
data: list,
isLoading,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
@@ -60,9 +62,10 @@ export function LabValuesList({ biomarkerId }: { biomarkerId: string }) {
if (isError) {
return (
<p className="text-destructive py-8 text-center text-sm">
{t("labs.loadError")}
</p>
<QueryErrorCard
title={t("labs.loadError")}
onRetry={() => void refetch()}
/>
);
}
@@ -28,6 +28,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { EmptyState } from "@/components/ui/empty-state";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import {
Table,
TableBody,
@@ -172,7 +173,7 @@ export function IntakeHistoryListV2({
const offset = page * pageSize;
const { data, isLoading, isError } = useQuery<IntakeListResponse>({
const { data, isLoading, isError, refetch } = useQuery<IntakeListResponse>({
queryKey: queryKeys.medicationIntakeList(medicationId, {
sortBy,
sortDir,
@@ -243,9 +244,10 @@ export function IntakeHistoryListV2({
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin motion-reduce:animate-none" />
</div>
) : isError ? (
<p className="text-destructive text-sm">
{t("medications.loadFailed")}
</p>
<QueryErrorCard
title={t("medications.loadFailed")}
onRetry={() => void refetch()}
/>
) : events.length === 0 ? (
<EmptyState
variant="plain"
+6 -4
View File
@@ -9,6 +9,7 @@ import { DeleteButton } from "@/components/data-list";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { apiDelete, apiGet } from "@/lib/api/api-fetch";
@@ -30,7 +31,7 @@ export function AllergyManager() {
const [editing, setEditing] = useState<AllergyDTO | null>(null);
const [formFooterEl, setFormFooterEl] = useState<HTMLDivElement | null>(null);
const { data, isLoading, isError } = useQuery({
const { data, isLoading, isError, refetch } = useQuery({
queryKey: queryKeys.allergyList(true),
queryFn: () => apiGet<AllergyDTO[]>("/api/allergies"),
});
@@ -135,9 +136,10 @@ export function AllergyManager() {
))}
</div>
) : isError ? (
<p className="text-destructive py-6 text-center text-sm">
{t("records.allergies.loadError")}
</p>
<QueryErrorCard
title={t("records.allergies.loadError")}
onRetry={() => void refetch()}
/>
) : rows.length === 0 ? (
<EmptyState
icon={<ShieldAlert className="size-6" />}
@@ -28,6 +28,7 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { cn } from "@/lib/utils";
@@ -126,9 +127,10 @@ export function ConditionsManager() {
return (
<div className="space-y-4" data-slot="conditions-manager">
{query.isError ? (
<p role="alert" className="text-destructive text-sm">
{t("records.conditions.loadError")}
</p>
<QueryErrorCard
title={t("records.conditions.loadError")}
onRetry={() => void query.refetch()}
/>
) : (
<>
{field(
@@ -8,6 +8,7 @@ import { toast } from "sonner";
import { DeleteButton } from "@/components/data-list";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { QueryErrorCard } from "@/components/ui/query-error-card";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { Skeleton } from "@/components/ui/skeleton";
import { apiDelete, apiGet } from "@/lib/api/api-fetch";
@@ -30,7 +31,7 @@ export function FamilyHistoryManager() {
const [editing, setEditing] = useState<FamilyHistoryEntryDTO | null>(null);
const [formFooterEl, setFormFooterEl] = useState<HTMLDivElement | null>(null);
const { data, isLoading, isError } = useQuery({
const { data, isLoading, isError, refetch } = useQuery({
queryKey: queryKeys.familyHistoryList(),
queryFn: () => apiGet<FamilyHistoryEntryDTO[]>("/api/family-history"),
});
@@ -120,9 +121,10 @@ export function FamilyHistoryManager() {
))}
</div>
) : isError ? (
<p className="text-destructive py-6 text-center text-sm">
{t("records.family.loadError")}
</p>
<QueryErrorCard
title={t("records.family.loadError")}
onRetry={() => void refetch()}
/>
) : rows.length === 0 ? (
<EmptyState
icon={<Users className="size-6" />}