@@ -311,6 +311,117 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
311311 queueMicrotask ( ( ) => setIsLoading ( false ) ) ;
312312 } , [ objectName , recordId ] ) ;
313313
314+ // Build detail schema — must be before early returns to keep hook count
315+ // consistent across renders and avoid React error #310.
316+ const detailSchema : DetailViewSchema = useMemo ( ( ) => {
317+ if ( ! objectDef ) {
318+ return { type : 'detail-view' } as DetailViewSchema ;
319+ }
320+
321+ // Auto-detect primary field: prefer objectDef metadata, then 'name' or 'title' heuristic
322+ const primaryField = objectDef . primaryField
323+ || Object . keys ( objectDef . fields || { } ) . find (
324+ ( key ) => key === 'name' || key === 'title'
325+ ) ;
326+
327+ // Build sections: prefer form sections from objectDef, fallback to flat field list
328+ const formSections = objectDef . views ?. form ?. sections ;
329+ const sections = formSections && formSections . length > 0
330+ ? formSections . map ( ( sec : any ) => ( {
331+ title : sec . title ,
332+ collapsible : sec . collapsible ,
333+ defaultCollapsed : sec . defaultCollapsed ,
334+ fields : ( sec . fields || [ ] ) . map ( ( f : any ) => {
335+ const fieldName = typeof f === 'string' ? f : f . name ;
336+ const fieldDef = objectDef . fields [ fieldName ] ;
337+ if ( ! fieldDef ) {
338+ console . warn ( `[RecordDetailView] Field "${ fieldName } " not found in ${ objectDef . name } definition` ) ;
339+ return { name : fieldName , label : fieldName } ;
340+ }
341+ const refTarget = fieldDef . reference_to || fieldDef . reference ;
342+ return {
343+ name : fieldName ,
344+ label : fieldDef . label || fieldName ,
345+ type : fieldDef . type || 'text' ,
346+ ...( fieldDef . options && { options : fieldDef . options } ) ,
347+ ...( refTarget && { reference_to : refTarget } ) ,
348+ ...( fieldDef . reference_field && { reference_field : fieldDef . reference_field } ) ,
349+ ...( fieldDef . currency && { currency : fieldDef . currency } ) ,
350+ } ;
351+ } ) ,
352+ } ) )
353+ : [
354+ {
355+ title : t ( 'detail.details' , { defaultValue : 'Details' } ) ,
356+ fields : Object . keys ( objectDef . fields || { } ) . map ( key => {
357+ const fieldDef = objectDef . fields [ key ] ;
358+ const refTarget = fieldDef . reference_to || fieldDef . reference ;
359+ return {
360+ name : key ,
361+ label : fieldDef . label || key ,
362+ type : fieldDef . type || 'text' ,
363+ ...( fieldDef . options && { options : fieldDef . options } ) ,
364+ ...( refTarget && { reference_to : refTarget } ) ,
365+ ...( fieldDef . reference_field && { reference_field : fieldDef . reference_field } ) ,
366+ ...( fieldDef . currency && { currency : fieldDef . currency } ) ,
367+ } ;
368+ } ) ,
369+ } ,
370+ ] ;
371+
372+ // Filter actions for record_header location and deduplicate by name
373+ const recordHeaderActions = ( ( ) => {
374+ const seen = new Set < string > ( ) ;
375+ return ( objectDef . actions || [ ] ) . filter ( ( a : any ) => {
376+ if ( ! a . locations ?. includes ( 'record_header' ) ) return false ;
377+ if ( ! a . name ) return true ;
378+ if ( seen . has ( a . name ) ) return false ;
379+ seen . add ( a . name ) ;
380+ return true ;
381+ } ) ;
382+ } ) ( ) ;
383+
384+ // Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
385+ const highlightFields : HighlightField [ ] = objectDef . views ?. detail ?. highlightFields ?? [ ] ;
386+
387+ // Build sectionGroups from objectDef detail/form config if available
388+ const sectionGroups : SectionGroup [ ] | undefined =
389+ objectDef . views ?. detail ?. sectionGroups ?? objectDef . views ?. form ?. sectionGroups ;
390+
391+ // Build related entries from reverse-reference child objects
392+ const related = childRelations . map ( ( { childObject, childLabel } ) => ( {
393+ title : childLabel ,
394+ type : 'table' as const ,
395+ api : childObject ,
396+ data : childRelatedData [ childObject ] || [ ] ,
397+ } ) ) ;
398+
399+ return {
400+ type : 'detail-view' as const ,
401+ objectName : objectDef . name ,
402+ resourceId : pureRecordId ,
403+ showBack : true ,
404+ onBack : 'history' ,
405+ showEdit : true ,
406+ title : objectDef . label ,
407+ primaryField,
408+ sections,
409+ autoTabs : true ,
410+ autoDiscoverRelated : true ,
411+ ...( related . length > 0 && { related } ) ,
412+ ...( highlightFields . length > 0 && { highlightFields } ) ,
413+ ...( sectionGroups && sectionGroups . length > 0 && { sectionGroups } ) ,
414+ ...( recordHeaderActions . length > 0 && {
415+ actions : [ {
416+ type : 'action:bar' ,
417+ location : 'record_header' ,
418+ actions : recordHeaderActions ,
419+ } as any ] ,
420+ } ) ,
421+ } ;
422+ // eslint-disable-next-line react-hooks/exhaustive-deps
423+ } , [ objectDef ?. name , pureRecordId , childRelatedData , actionRefreshKey ] ) ;
424+
314425 if ( isLoading ) {
315426 return < SkeletonDetail /> ;
316427 }
@@ -332,109 +443,6 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
332443 ) ;
333444 }
334445
335- // Auto-detect primary field: prefer objectDef metadata, then 'name' or 'title' heuristic
336- const primaryField = objectDef . primaryField
337- || Object . keys ( objectDef . fields || { } ) . find (
338- ( key ) => key === 'name' || key === 'title'
339- ) ;
340-
341- // Build sections: prefer form sections from objectDef, fallback to flat field list
342- const formSections = objectDef . views ?. form ?. sections ;
343- const sections = formSections && formSections . length > 0
344- ? formSections . map ( ( sec : any ) => ( {
345- title : sec . title ,
346- collapsible : sec . collapsible ,
347- defaultCollapsed : sec . defaultCollapsed ,
348- fields : ( sec . fields || [ ] ) . map ( ( f : any ) => {
349- const fieldName = typeof f === 'string' ? f : f . name ;
350- const fieldDef = objectDef . fields [ fieldName ] ;
351- if ( ! fieldDef ) {
352- console . warn ( `[RecordDetailView] Field "${ fieldName } " not found in ${ objectDef . name } definition` ) ;
353- return { name : fieldName , label : fieldName } ;
354- }
355- const refTarget = fieldDef . reference_to || fieldDef . reference ;
356- return {
357- name : fieldName ,
358- label : fieldDef . label || fieldName ,
359- type : fieldDef . type || 'text' ,
360- ...( fieldDef . options && { options : fieldDef . options } ) ,
361- ...( refTarget && { reference_to : refTarget } ) ,
362- ...( fieldDef . reference_field && { reference_field : fieldDef . reference_field } ) ,
363- ...( fieldDef . currency && { currency : fieldDef . currency } ) ,
364- } ;
365- } ) ,
366- } ) )
367- : [
368- {
369- title : t ( 'detail.details' , { defaultValue : 'Details' } ) ,
370- fields : Object . keys ( objectDef . fields || { } ) . map ( key => {
371- const fieldDef = objectDef . fields [ key ] ;
372- const refTarget = fieldDef . reference_to || fieldDef . reference ;
373- return {
374- name : key ,
375- label : fieldDef . label || key ,
376- type : fieldDef . type || 'text' ,
377- ...( fieldDef . options && { options : fieldDef . options } ) ,
378- ...( refTarget && { reference_to : refTarget } ) ,
379- ...( fieldDef . reference_field && { reference_field : fieldDef . reference_field } ) ,
380- ...( fieldDef . currency && { currency : fieldDef . currency } ) ,
381- } ;
382- } ) ,
383- } ,
384- ] ;
385-
386- // Filter actions for record_header location and deduplicate by name
387- const recordHeaderActions = ( ( ) => {
388- const seen = new Set < string > ( ) ;
389- return ( objectDef . actions || [ ] ) . filter ( ( a : any ) => {
390- if ( ! a . locations ?. includes ( 'record_header' ) ) return false ;
391- if ( ! a . name ) return true ;
392- if ( seen . has ( a . name ) ) return false ;
393- seen . add ( a . name ) ;
394- return true ;
395- } ) ;
396- } ) ( ) ;
397-
398- // Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
399- const highlightFields : HighlightField [ ] = objectDef . views ?. detail ?. highlightFields ?? [ ] ;
400-
401- // Build sectionGroups from objectDef detail/form config if available
402- const sectionGroups : SectionGroup [ ] | undefined =
403- objectDef . views ?. detail ?. sectionGroups ?? objectDef . views ?. form ?. sectionGroups ;
404-
405- // Build related entries from reverse-reference child objects
406- const related = childRelations . map ( ( { childObject, childLabel } ) => ( {
407- title : childLabel ,
408- type : 'table' as const ,
409- api : childObject ,
410- data : childRelatedData [ childObject ] || [ ] ,
411- } ) ) ;
412-
413- const detailSchema : DetailViewSchema = useMemo ( ( ) => ( {
414- type : 'detail-view' ,
415- objectName : objectDef . name ,
416- resourceId : pureRecordId ,
417- showBack : true ,
418- onBack : 'history' ,
419- showEdit : true ,
420- title : objectDef . label ,
421- primaryField,
422- sections,
423- autoTabs : true ,
424- autoDiscoverRelated : true ,
425- ...( related . length > 0 && { related } ) ,
426- ...( highlightFields . length > 0 && { highlightFields } ) ,
427- ...( sectionGroups && sectionGroups . length > 0 && { sectionGroups } ) ,
428- ...( recordHeaderActions . length > 0 && {
429- actions : [ {
430- type : 'action:bar' ,
431- location : 'record_header' ,
432- actions : recordHeaderActions ,
433- } as any ] ,
434- } ) ,
435- // eslint-disable-next-line react-hooks/exhaustive-deps
436- } ) , [ objectDef . name , pureRecordId , childRelatedData , actionRefreshKey ] ) ;
437-
438446 return (
439447 < div className = "h-full bg-background overflow-hidden flex flex-col relative" >
440448 < div className = "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2" >
0 commit comments