|
1 | 1 | "use client" |
2 | 2 |
|
3 | | -import { AlertTriangle, CheckCircle, Info, AlertCircle as AlertIcon } from "lucide-react" |
| 3 | +import React, { memo, useCallback } from 'react'; |
| 4 | +import { AlertTriangle, CheckCircle, Info, AlertCircle as AlertIcon, XCircle, ChevronDown } from "lucide-react" |
4 | 5 | import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" |
5 | 6 | import { Badge } from "@/components/ui/badge" |
6 | 7 | import { useState } from "react" |
7 | 8 | import { ErrorObject } from "ajv" |
8 | 9 | import * as jsonc from 'jsonc-parser'; |
| 10 | +import { cn } from "@/lib/utils"; |
9 | 11 |
|
10 | 12 | // Add _range to ErrorObject type |
11 | 13 | interface ErrorObjectWithRange extends ErrorObject { |
@@ -59,95 +61,100 @@ const SchemaErrorItem: React.FC<SchemaErrorItemProps> = ({ error, index }) => { |
59 | 61 | // --- ValidationResults Component (Updated) --- |
60 | 62 | interface ValidationResultsProps { |
61 | 63 | results: { |
62 | | - schemaIsValid: boolean; |
63 | | - schemaErrors?: ErrorObjectWithRange[]; // Use extended type |
64 | | - parseError?: string | null; |
65 | | - }; |
66 | | - schemaData?: string; // schemaData no longer strictly needed by SchemaErrorItem |
| 64 | + row: number; |
| 65 | + errors: { property?: string; message: string }[]; |
| 66 | + warnings: { property?: string; message: string }[]; |
| 67 | + }[]; |
| 68 | + openAccordionValue: string | undefined; |
| 69 | + setOpenAccordionValue: (value: string | undefined) => void; |
| 70 | + setHighlightedCsvLine: (line: number | undefined) => void; |
| 71 | + setScrollToLine: (line: number | undefined) => void; |
67 | 72 | } |
68 | 73 |
|
69 | | -export default function ValidationResults({ results }: ValidationResultsProps) { // Remove schemaData prop if unused |
70 | | - const { schemaIsValid, schemaErrors = [], parseError } = results; |
71 | | - |
72 | | - // Create a unified error array |
73 | | - let displayableErrors: { message: string; instancePath?: string; isParseError?: boolean; rawError?: ErrorObjectWithRange }[] = []; // Use extended type |
74 | | - |
75 | | - if (parseError) { |
76 | | - displayableErrors.push({ message: parseError, isParseError: true }); |
77 | | - } else { |
78 | | - displayableErrors = schemaErrors.map(err => ({ |
79 | | - message: err.message || 'Unknown error', |
80 | | - instancePath: err.instancePath || err.schemaPath || '', |
81 | | - isParseError: false, |
82 | | - rawError: err // Pass the raw error object for SchemaErrorItem |
83 | | - })); |
84 | | - } |
85 | | - |
86 | | - const totalErrors = displayableErrors.length; |
87 | | - const overallValid = !parseError && schemaIsValid; |
88 | | - |
89 | | - // No need to render anything if valid and no errors |
90 | | - // if (overallValid && totalErrors === 0) { |
91 | | - // return ( |
92 | | - // <div className="flex items-center p-3 rounded-md bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200"> |
93 | | - // <CheckCircle className="h-5 w-5 mr-2" /> |
94 | | - // <span className="font-medium">Schema is VALID according to the selected official draft</span> |
95 | | - // </div> |
96 | | - // ); |
97 | | - // } |
| 74 | +const ValidationResults = memo(function ValidationResults({ |
| 75 | + results, |
| 76 | + openAccordionValue, |
| 77 | + setOpenAccordionValue, |
| 78 | + setHighlightedCsvLine, |
| 79 | + setScrollToLine, |
| 80 | +}: ValidationResultsProps) { |
| 81 | + const renderRow = useCallback((result, idx) => { |
| 82 | + const rowSeverity = result.errors.length > 0 ? 'error' : 'warning'; |
| 83 | + const displayRowNumber = result.row; |
| 84 | + return ( |
| 85 | + <Accordion |
| 86 | + key={result.row} |
| 87 | + type="single" |
| 88 | + collapsible |
| 89 | + className="w-full border-b border-muted/20 px-4" |
| 90 | + value={openAccordionValue} |
| 91 | + onValueChange={(val) => { |
| 92 | + setOpenAccordionValue(val); |
| 93 | + if (val && val.startsWith('item-')) { |
| 94 | + const rowIdx = parseInt(val.replace('item-', ''), 10); |
| 95 | + // Highlight the same row as reported (no +2 offset) |
| 96 | + setHighlightedCsvLine(rowIdx); |
| 97 | + setScrollToLine(rowIdx); |
| 98 | + } else { |
| 99 | + setHighlightedCsvLine(undefined); |
| 100 | + setScrollToLine(undefined); |
| 101 | + } |
| 102 | + }} |
| 103 | + > |
| 104 | + <AccordionItem |
| 105 | + value={`item-${result.row}`} |
| 106 | + className="border-b-0" |
| 107 | + > |
| 108 | + <AccordionTrigger className={cn( |
| 109 | + "text-sm text-left hover:no-underline py-2 group flex items-center", |
| 110 | + rowSeverity === 'error' |
| 111 | + ? 'data-[state=open]:text-red-700 dark:data-[state=open]:text-red-300' |
| 112 | + : 'data-[state=open]:text-yellow-700 dark:data-[state=open]:text-yellow-300' |
| 113 | + )}> |
| 114 | + <div className="flex items-center space-x-2 flex-grow truncate"> |
| 115 | + {rowSeverity === 'error' ? |
| 116 | + <XCircle className="h-4 w-4 text-red-500 flex-shrink-0" /> : |
| 117 | + <AlertTriangle className="h-4 w-4 text-yellow-500 flex-shrink-0" />} |
| 118 | + <span className="font-semibold">Row {displayRowNumber}:</span> |
| 119 | + <span className="truncate flex-grow text-muted-foreground"> |
| 120 | + {result.errors[0]?.message || result.warnings[0]?.message || 'Unknown issue'} |
| 121 | + {(result.errors.length + result.warnings.length) > 1 ? ` (+${result.errors.length + result.warnings.length - 1} more)` : ''} |
| 122 | + </span> |
| 123 | + </div> |
| 124 | + <ChevronDown className={cn( |
| 125 | + "h-4 w-4 ml-2 transition-transform duration-200", |
| 126 | + openAccordionValue === `item-${result.row}` ? 'rotate-180' : '' |
| 127 | + )} /> |
| 128 | + </AccordionTrigger> |
| 129 | + <AccordionContent className="text-xs px-4 pt-2 pb-3 space-y-1 bg-muted/30 rounded-b"> |
| 130 | + {result.errors.map((err, index) => ( |
| 131 | + <div key={`err-${index}`} className="flex items-start text-red-600 dark:text-red-400"> |
| 132 | + <XCircle className="h-3 w-3 mr-1.5 mt-0.5 flex-shrink-0" /> |
| 133 | + <div> |
| 134 | + <span className="font-semibold">Error:</span> <span className="font-medium">{err.property || 'N/A'}</span> - {err.message} |
| 135 | + </div> |
| 136 | + </div> |
| 137 | + ))} |
| 138 | + {result.warnings.map((warn, index) => ( |
| 139 | + <div key={`warn-${index}`} className="flex items-start text-yellow-600 dark:text-yellow-400"> |
| 140 | + <AlertTriangle className="h-3 w-3 mr-1.5 mt-0.5 flex-shrink-0" /> |
| 141 | + <div> |
| 142 | + <span className="font-semibold">Warning:</span> <span className="font-medium">{warn.property || 'N/A'}</span> - {warn.message} |
| 143 | + </div> |
| 144 | + </div> |
| 145 | + ))} |
| 146 | + </AccordionContent> |
| 147 | + </AccordionItem> |
| 148 | + </Accordion> |
| 149 | + ); |
| 150 | + }, [openAccordionValue, setOpenAccordionValue, setHighlightedCsvLine, setScrollToLine]); |
98 | 151 |
|
99 | 152 | return ( |
100 | | - <div className="space-y-4"> |
101 | | - {/* Always show Overall Status */} |
102 | | - <div className={`flex items-center p-3 rounded-md ${overallValid ? 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200' : 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200'}`}> |
103 | | - {overallValid ? <CheckCircle className="h-5 w-5 mr-2" /> : <AlertTriangle className="h-5 w-5 mr-2" />} |
104 | | - <span className="font-medium">{ |
105 | | - parseError |
106 | | - ? 'Schema has Syntax Errors' |
107 | | - : (schemaIsValid ? 'Schema is VALID according to the selected official draft' : 'Schema is INVALID according to the selected official draft') |
108 | | - }</span> |
109 | | - </div> |
110 | | - |
111 | | - {/* Always render Accordion if there are *any* errors (parse or schema) */} |
112 | | - {totalErrors > 0 && ( |
113 | | - <Accordion type="single" collapsible defaultValue="schema-errors" className="w-full"> |
114 | | - <AccordionItem value="schema-errors"> |
115 | | - <AccordionTrigger className="text-base font-medium px-3 py-2 hover:bg-muted/50 dark:hover:bg-zinc-700/30 rounded-md"> |
116 | | - <div className="flex items-center justify-between w-full"> |
117 | | - {/* Adjust title slightly based on error type? Or keep generic? */} |
118 | | - <span className="flex items-center"> |
119 | | - <AlertIcon className={`h-4 w-4 mr-2 ${parseError ? 'text-red-500' : 'text-orange-500'}`} /> |
120 | | - {parseError ? 'Syntax Errors' : 'Schema Validation Errors'} |
121 | | - </span> |
122 | | - <Badge variant="destructive" className="ml-auto">{totalErrors}</Badge> |
123 | | - </div> |
124 | | - </AccordionTrigger> |
125 | | - <AccordionContent className="pt-2 px-1"> |
126 | | - <div className="space-y-2"> |
127 | | - {/* Map over unified displayableErrors */} |
128 | | - {displayableErrors.map((error, index) => { |
129 | | - // Render slightly differently for parse error vs schema error |
130 | | - if (error.isParseError) { |
131 | | - return ( |
132 | | - <div key={`parse-${index}`} className="py-2 px-3 border-l-4 border-red-500 dark:border-red-400 bg-red-50 dark:bg-red-900/20 rounded-r-md text-sm"> |
133 | | - <pre className="whitespace-pre-wrap break-words font-sans font-semibold">{error.message}</pre> |
134 | | - </div> |
135 | | - ); |
136 | | - } else if (error.rawError) { |
137 | | - return ( |
138 | | - // Pass only error and index, schemaData is removed |
139 | | - <SchemaErrorItem key={`schema-${index}`} error={error.rawError} index={index} /> |
140 | | - ); |
141 | | - } else { |
142 | | - return null; // Should not happen |
143 | | - } |
144 | | - })} |
145 | | - </div> |
146 | | - </AccordionContent> |
147 | | - </AccordionItem> |
148 | | - </Accordion> |
149 | | - )} |
150 | | - </div> |
| 153 | + <> |
| 154 | + {results.map(renderRow)} |
| 155 | + </> |
151 | 156 | ); |
152 | | -} |
| 157 | +}); |
| 158 | + |
| 159 | +export default ValidationResults; |
153 | 160 |
|
0 commit comments