139 lines
5.7 KiB
TypeScript
139 lines
5.7 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { CheckCircle2, XCircle } from "lucide-react"
|
|
import type { ImportFailureRow } from "@garage/api"
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/shared/components/ui/dialog"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/shared/components/ui/table"
|
|
|
|
export type { ImportFailureRow }
|
|
|
|
export type ImportResultsDialogProps = {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
importedCount: number
|
|
failedCount: number
|
|
failedRows: ImportFailureRow[]
|
|
entityLabel?: string
|
|
}
|
|
|
|
function formatValue(value: unknown): string {
|
|
if (value === null || value === undefined || value === "") return "—"
|
|
if (typeof value === "string") return value
|
|
if (typeof value === "number" || typeof value === "boolean") return String(value)
|
|
try {
|
|
return JSON.stringify(value)
|
|
} catch {
|
|
return String(value)
|
|
}
|
|
}
|
|
|
|
export function ImportResultsDialog({
|
|
open,
|
|
onOpenChange,
|
|
importedCount,
|
|
failedCount,
|
|
failedRows,
|
|
entityLabel = "Records",
|
|
}: ImportResultsDialogProps) {
|
|
const hasFailures = failedCount > 0
|
|
const flatFailures = failedRows.flatMap((r) =>
|
|
r.errors.map((e) => ({ row: r.row, ...e })),
|
|
)
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="w-[min(96vw,64rem)] sm:max-w-4xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Import results</DialogTitle>
|
|
<DialogDescription>
|
|
{hasFailures
|
|
? "Some rows could not be imported. Fix the issues below and re-upload only the failing rows."
|
|
: `All ${entityLabel.toLowerCase()} imported successfully.`}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex gap-3">
|
|
<div className="flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">
|
|
<CheckCircle2 className="size-4" />
|
|
<span>
|
|
<strong>{importedCount}</strong> imported
|
|
</span>
|
|
</div>
|
|
{hasFailures && (
|
|
<div className="flex items-center gap-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
|
|
<XCircle className="size-4" />
|
|
<span>
|
|
<strong>{failedCount}</strong> failed
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{hasFailures && (
|
|
<ScrollArea className="max-h-96 rounded-md border">
|
|
<Table className="table-fixed w-full">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-16">Row</TableHead>
|
|
<TableHead className="w-40">Field</TableHead>
|
|
<TableHead className="w-56">Value</TableHead>
|
|
<TableHead>Reason</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{flatFailures.map((f, idx) => (
|
|
<TableRow key={`${f.row}-${f.field}-${idx}`}>
|
|
<TableCell className="font-mono text-xs">{f.row}</TableCell>
|
|
<TableCell className="font-medium">{f.label}</TableCell>
|
|
<TableCell className="text-muted-foreground break-all whitespace-pre-wrap">
|
|
{formatValue(f.value)}
|
|
</TableCell>
|
|
<TableCell className="break-words">
|
|
<div>{f.message}</div>
|
|
{f.valid_examples && f.valid_examples.length > 0 && (
|
|
<details className="mt-1 text-xs text-muted-foreground">
|
|
<summary className="cursor-pointer">
|
|
Try one of: {f.valid_examples.slice(0, 3).join(", ")}
|
|
{f.valid_examples.length > 3 ? "…" : ""}
|
|
</summary>
|
|
<div className="mt-1">
|
|
{f.valid_examples.join(", ")}
|
|
</div>
|
|
</details>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</ScrollArea>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
Close
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|