@@ -21,6 +21,8 @@ export interface ImportWizardProps {
2121 onCancel ?: ( ) => void ;
2222 open ?: boolean ;
2323 onOpenChange ?: ( open : boolean ) => void ;
24+ /** Error handling strategy: 'skip' skips invalid rows, 'stop' aborts on first error. @default 'skip' */
25+ onErrorMode ?: 'skip' | 'stop' ;
2426}
2527
2628export interface ImportResult {
@@ -201,37 +203,61 @@ const StepMapping: React.FC<{
201203 ) ;
202204} ;
203205
204- // Step 3: Preview & Import
206+ // Step 3: Preview & Import (shows first 10 rows with per-row validation errors)
205207const StepPreview : React . FC < {
206208 headers : string [ ] ; rows : string [ ] [ ] ; mapping : Record < number , string > ; fields : ImportWizardProps [ 'fields' ] ;
207209} > = ( { headers, rows, mapping, fields } ) => {
208210 const mappedCols = useMemo ( ( ) =>
209211 Object . entries ( mapping ) . map ( ( [ idx , fieldName ] ) => ( {
210212 csvIdx : Number ( idx ) , header : headers [ Number ( idx ) ] , field : fields . find ( ( f ) => f . name === fieldName ) ! ,
211213 } ) ) , [ mapping , headers , fields ] ) ;
212- const previewRows = rows . slice ( 0 , 5 ) ;
214+ const previewRows = rows . slice ( 0 , 10 ) ;
215+
216+ const rowValidations = useMemo ( ( ) => previewRows . map ( ( row , rIdx ) => {
217+ const errs : Record < number , string > = { } ;
218+ for ( const col of mappedCols ) {
219+ const raw = row [ col . csvIdx ] ?? '' ;
220+ if ( col . field . required && ! raw ) errs [ col . csvIdx ] = 'Required' ;
221+ else if ( raw && ! validateValue ( raw , col . field . type ) ) errs [ col . csvIdx ] = `Invalid ${ col . field . type } ` ;
222+ }
223+ return errs ;
224+ } ) , [ previewRows , mappedCols ] ) ;
225+
226+ const errorCount = rowValidations . filter ( e => Object . keys ( e ) . length > 0 ) . length ;
213227
214228 return (
215229 < div className = "max-h-[360px] overflow-auto" >
230+ { errorCount > 0 && (
231+ < p className = "mb-2 flex items-center gap-1 text-xs text-destructive" >
232+ < AlertCircle className = "h-3.5 w-3.5" /> { errorCount } row(s) with errors in preview
233+ </ p >
234+ ) }
216235 < Table >
217236 < TableHeader >
218237 < TableRow >
238+ < TableHead className = "w-12" > #</ TableHead >
219239 { mappedCols . map ( ( col ) => < TableHead key = { col . csvIdx } > { col . field . label } </ TableHead > ) }
220240 </ TableRow >
221241 </ TableHeader >
222242 < TableBody >
223- { previewRows . map ( ( row , rIdx ) => (
224- < TableRow key = { rIdx } >
225- { mappedCols . map ( ( col ) => {
226- const value = row [ col . csvIdx ] ?? '' ;
227- return (
228- < TableCell key = { col . csvIdx } className = { cn ( ! validateValue ( value , col . field . type ) && 'text-destructive' ) } >
229- { value }
230- </ TableCell >
231- ) ;
232- } ) }
233- </ TableRow >
234- ) ) }
243+ { previewRows . map ( ( row , rIdx ) => {
244+ const errs = rowValidations [ rIdx ] ;
245+ const hasError = Object . keys ( errs ) . length > 0 ;
246+ return (
247+ < TableRow key = { rIdx } className = { cn ( hasError && 'bg-destructive/5' ) } >
248+ < TableCell className = "text-xs text-muted-foreground" > { rIdx + 1 } </ TableCell >
249+ { mappedCols . map ( ( col ) => {
250+ const value = row [ col . csvIdx ] ?? '' ;
251+ const cellErr = errs [ col . csvIdx ] ;
252+ return (
253+ < TableCell key = { col . csvIdx } className = { cn ( cellErr && 'text-destructive' ) } title = { cellErr } >
254+ { value || < span className = "text-muted-foreground/50" > —</ span > }
255+ </ TableCell >
256+ ) ;
257+ } ) }
258+ </ TableRow >
259+ ) ;
260+ } ) }
235261 </ TableBody >
236262 </ Table >
237263 < p className = "mt-2 text-xs text-muted-foreground" > Showing { previewRows . length } of { rows . length } rows</ p >
@@ -241,7 +267,7 @@ const StepPreview: React.FC<{
241267
242268// Main wizard component
243269export const ImportWizard : React . FC < ImportWizardProps > = ( {
244- objectName, objectLabel, fields, dataSource, onComplete, onCancel, open, onOpenChange,
270+ objectName, objectLabel, fields, dataSource, onComplete, onCancel, open, onOpenChange, onErrorMode = 'skip' ,
245271} ) => {
246272 const [ step , setStep ] = useState < WizardStep > ( 'upload' ) ;
247273 const [ headers , setHeaders ] = useState < string [ ] > ( [ ] ) ;
@@ -274,19 +300,21 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
274300 if ( rowErrors . length > 0 ) {
275301 skippedRows ++ ;
276302 errors . push ( ...rowErrors ) ;
303+ if ( onErrorMode === 'stop' ) break ;
277304 } else {
278305 try { if ( dataSource ?. create ) await dataSource . create ( objectName , record ) ; importedRows ++ ; }
279306 catch ( err ) {
280307 skippedRows ++ ;
281308 const msg = err instanceof Error ? err . message : 'Failed to create record' ;
282309 errors . push ( { row : i + 1 , field : '' , message : msg } ) ;
310+ if ( onErrorMode === 'stop' ) break ;
283311 }
284312 }
285313 setProgress ( Math . round ( ( ( i + 1 ) / rows . length ) * 100 ) ) ;
286314 }
287315 const importResult : ImportResult = { totalRows : rows . length , importedRows, skippedRows, errors } ;
288316 setResult ( importResult ) ; setImporting ( false ) ; onComplete ?.( importResult ) ;
289- } , [ rows , mapping , fields , dataSource , objectName , onComplete ] ) ;
317+ } , [ rows , mapping , fields , dataSource , objectName , onComplete , onErrorMode ] ) ;
290318
291319 const reset = useCallback ( ( ) => {
292320 setStep ( 'upload' ) ; setHeaders ( [ ] ) ; setRows ( [ ] ) ; setMapping ( { } ) ; setProgress ( 0 ) ; setResult ( null ) ;
0 commit comments