fix(drill): ProviderDrawer quality pass — error branching, demo fallback, flex layout
Quality-fix iteration on 078c9ad. Resolves the 4 Important issues
flagged in the code review while preserving everything that's working.
Issue 1 — ProviderDrawer silently swallowed error states (infinite
skeleton on any failure). Now mirrors ClaimDrawer/RemitDrawer:
- Destructure { data, isLoading, isError, error, refetch }
- Compute errorKind = ApiError(404) → not_found, else → network
- New ProviderDrawerError.tsx renders the right copy + retry/close
- Body branches on errorKind → loading → data
Issue 2 — useProviderDetail did not match the useClaimDetail contract:
- 404 retry guard (no retries on 404, <=3 on everything else)
- Explicit typed return shape { data, isLoading, isError, error,
refetch }
- npi === null short-circuit after the useQuery call
Issue 3 — Provider drilldown was non-functional in demo mode. Added
the useSyncExternalStore fallback to useAppStore.providers (same
pattern as useProviders), gated by api.isConfigured. Unknown NPI in
demo mode surfaces via the error branch instead of an infinite
skeleton.
Issue 4 — Test was shape-only (1 case). Rewrote with vi.hoisted +
vi.fn() hook mock and 7 cases covering null/loading/404/network/
close/success/hook-call-shape.
Issue 5 — Replaced h-[calc(100%-64px)] with a flex layout
(flex h-full flex-col on DialogContent, flex h-full flex-col
overflow-y-auto on inner wrappers). Mirrors RemitDrawer's pattern;
no more coupling to DrillDrawerHeader's padding/font sizes.
Issue 7 — Removed the dead npi === null ? null : (...) wrapper
inside DialogContent; Dialog open={npi !== null} already gates it.
Issue 8 — Trailing newlines added across all touched + created files.
Self-review: tsc clean on changed files; drawer test suite 211/211
passing; full test suite 369/372 (3 pre-existing Inbox/Reconciliation
failures unchanged). ProviderDrawer now 7/7.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { DrillDrawerHeader } from "@/components/drill/DrillDrawerHeader";
|
||||
import { ProviderOverview } from "./ProviderOverview";
|
||||
import { useProviderDetail } from "@/hooks/useProviderDetail";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useProviderDetail } from "@/hooks/useProviderDetail";
|
||||
import { ProviderOverview } from "./ProviderOverview";
|
||||
import { ProviderDrawerError } from "./ProviderDrawerError";
|
||||
|
||||
interface Props {
|
||||
npi: string | null;
|
||||
@@ -20,36 +22,66 @@ interface Props {
|
||||
* Layout mirrors the ClaimDrawer / RemitDrawer pattern: Radix Dialog
|
||||
* repositioned to the right edge as a fixed-height side panel, with
|
||||
* the shared ``DrillDrawerHeader`` on top and a scrollable body below.
|
||||
* The DialogContent is the flex parent; the header and body stack
|
||||
* naturally inside it without an explicit `calc(100% - 64px)` height
|
||||
* that would couple to DrillDrawerHeader's padding/font sizes.
|
||||
*
|
||||
* Error branching mirrors the peer drawers:
|
||||
* - `ApiError(404)` → "not_found" (no retry, the provider is gone)
|
||||
* - anything else → "network" (retry available)
|
||||
* - demo mode + unknown NPI → ApiError(404), routes to the same
|
||||
* "not_found" branch (the hook raises `ApiError(404, "Provider not
|
||||
* found")` in this case, matching the real backend 404 path).
|
||||
*/
|
||||
export function ProviderDrawer({ npi, onClose }: Props) {
|
||||
const { data, isLoading } = useProviderDetail(npi);
|
||||
const { data, isLoading, isError, error, refetch } = useProviderDetail(npi);
|
||||
|
||||
const errorKind: "not_found" | "network" | null = isError
|
||||
? error instanceof ApiError && error.status === 404
|
||||
? "not_found"
|
||||
: "network"
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Dialog open={npi !== null} onOpenChange={(o) => { if (!o) onClose(); }}>
|
||||
<DialogContent
|
||||
className="fixed right-0 top-0 h-full w-full max-w-2xl translate-x-0 translate-y-0 rounded-none border-l border-border bg-card p-0"
|
||||
className="fixed right-0 top-0 flex h-full w-full max-w-2xl flex-col translate-x-0 translate-y-0 rounded-none border-l border-border bg-card p-0"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
{npi === null ? null : (
|
||||
<>
|
||||
{errorKind ? (
|
||||
<ProviderDrawerError
|
||||
kind={errorKind}
|
||||
onRetry={() => {
|
||||
void refetch();
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : isLoading || !data ? (
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<DrillDrawerHeader
|
||||
eyebrow="Provider"
|
||||
title={data?.name ?? "Loading…"}
|
||||
title="Loading…"
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className="p-6 overflow-y-auto h-[calc(100%-64px)]">
|
||||
{isLoading || !data ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton variant="row" />
|
||||
<Skeleton variant="row" />
|
||||
<Skeleton variant="row" />
|
||||
</div>
|
||||
) : (
|
||||
<ProviderOverview provider={data} />
|
||||
)}
|
||||
<div className="space-y-2 p-6">
|
||||
<Skeleton variant="row" />
|
||||
<Skeleton variant="row" />
|
||||
<Skeleton variant="row" />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<DrillDrawerHeader
|
||||
eyebrow="Provider"
|
||||
title={data.name}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<div className="p-6">
|
||||
<ProviderOverview provider={data} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user