66 * the object field definitions.
77 */
88
9- import { useState , useEffect , useCallback } from 'react' ;
9+ import { useState , useEffect , useCallback , useMemo } from 'react' ;
1010import { useParams } from 'react-router-dom' ;
1111import { DetailView , RecordChatterPanel } from '@object-ui/plugin-detail' ;
1212import { Empty , EmptyTitle , EmptyDescription } from '@object-ui/components' ;
@@ -15,7 +15,7 @@ import { useAuth } from '@object-ui/auth';
1515import { Database , Users } from 'lucide-react' ;
1616import { MetadataPanel , useMetadataInspector } from './MetadataInspector' ;
1717import { SkeletonDetail } from './skeletons' ;
18- import type { DetailViewSchema , FeedItem } from '@object-ui/types' ;
18+ import type { DetailViewSchema , FeedItem , HighlightField , SectionGroup } from '@object-ui/types' ;
1919
2020interface RecordDetailViewProps {
2121 dataSource : any ;
@@ -25,20 +25,77 @@ interface RecordDetailViewProps {
2525
2626const FALLBACK_USER = { id : 'current-user' , name : 'Demo User' } ;
2727
28+ /** Field names automatically promoted to the highlight banner when present. */
29+ const HIGHLIGHT_FIELD_NAMES = [ 'status' , 'stage' , 'priority' , 'category' , 'type' , 'owner' , 'amount' ] ;
30+
2831export function RecordDetailView ( { dataSource, objects, onEdit } : RecordDetailViewProps ) {
2932 const { objectName, recordId } = useParams ( ) ;
3033 const { showDebug } = useMetadataInspector ( ) ;
3134 const { user } = useAuth ( ) ;
3235 const [ isLoading , setIsLoading ] = useState ( true ) ;
3336 const [ feedItems , setFeedItems ] = useState < FeedItem [ ] > ( [ ] ) ;
3437 const [ recordViewers , setRecordViewers ] = useState < PresenceUser [ ] > ( [ ] ) ;
38+ const [ childRelatedData , setChildRelatedData ] = useState < Record < string , any [ ] > > ( { } ) ;
3539 const objectDef = objects . find ( ( o : any ) => o . name === objectName ) ;
3640
3741 // Use the URL recordId as-is — it contains the actual record _id.
3842 // Navigation code passes `record._id || record.id` directly into the URL
3943 // without adding any prefix, so no stripping is needed.
4044 const pureRecordId = recordId ;
4145
46+ // Discover reverse references: other objects with lookup/master_detail fields
47+ // pointing to the current object (e.g., order_item.order → order).
48+ const childRelations = useMemo ( ( ) => {
49+ if ( ! objectDef || ! objects ) return [ ] ;
50+ const relations : Array < { childObject : string ; childLabel : string ; referenceField : string } > = [ ] ;
51+ for ( const obj of objects ) {
52+ if ( obj . name === objectDef . name ) continue ;
53+ for ( const [ fieldName , fieldDef ] of Object . entries < any > ( obj . fields || { } ) ) {
54+ if (
55+ fieldDef &&
56+ ( fieldDef . type === 'lookup' || fieldDef . type === 'master_detail' ) &&
57+ ( fieldDef . reference_to || fieldDef . reference ) === objectDef . name
58+ ) {
59+ relations . push ( {
60+ childObject : obj . name ,
61+ childLabel : obj . label || obj . name ,
62+ referenceField : fieldName ,
63+ } ) ;
64+ }
65+ }
66+ }
67+ return relations ;
68+ } , [ objectDef , objects ] ) ;
69+
70+ // Fetch related child records for each reverse reference
71+ useEffect ( ( ) => {
72+ if ( ! dataSource || ! pureRecordId || childRelations . length === 0 ) return ;
73+ let cancelled = false ;
74+ Promise . all (
75+ childRelations . map ( ( { childObject, referenceField } ) =>
76+ dataSource . find ( childObject , {
77+ $filter : { [ referenceField ] : pureRecordId } ,
78+ } )
79+ . then ( ( res : any ) => {
80+ const items = Array . isArray ( res ) ? res : res ?. data || [ ] ;
81+ return { childObject, items } ;
82+ } )
83+ . catch ( ( err : any ) => {
84+ console . warn ( `[RecordDetailView] Failed to fetch related ${ childObject } :` , err ) ;
85+ return { childObject, items : [ ] as any [ ] } ;
86+ } )
87+ )
88+ ) . then ( ( results ) => {
89+ if ( cancelled ) return ;
90+ const data : Record < string , any [ ] > = { } ;
91+ for ( const { childObject, items } of results ) {
92+ data [ childObject ] = items ;
93+ }
94+ setChildRelatedData ( data ) ;
95+ } ) ;
96+ return ( ) => { cancelled = true ; } ;
97+ } , [ dataSource , pureRecordId , childRelations ] ) ;
98+
4299 const currentUser = user
43100 ? { id : user . id , name : user . name , avatar : user . image }
44101 : FALLBACK_USER ;
@@ -223,12 +280,13 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
223280 console . warn ( `[RecordDetailView] Field "${ fieldName } " not found in ${ objectDef . name } definition` ) ;
224281 return { name : fieldName , label : fieldName } ;
225282 }
283+ const refTarget = fieldDef . reference_to || fieldDef . reference ;
226284 return {
227285 name : fieldName ,
228286 label : fieldDef . label || fieldName ,
229287 type : fieldDef . type || 'text' ,
230288 ...( fieldDef . options && { options : fieldDef . options } ) ,
231- ...( fieldDef . reference_to && { reference_to : fieldDef . reference_to } ) ,
289+ ...( refTarget && { reference_to : refTarget } ) ,
232290 ...( fieldDef . reference_field && { reference_field : fieldDef . reference_field } ) ,
233291 ...( fieldDef . currency && { currency : fieldDef . currency } ) ,
234292 } ;
@@ -239,12 +297,13 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
239297 title : 'Details' ,
240298 fields : Object . keys ( objectDef . fields || { } ) . map ( key => {
241299 const fieldDef = objectDef . fields [ key ] ;
300+ const refTarget = fieldDef . reference_to || fieldDef . reference ;
242301 return {
243302 name : key ,
244303 label : fieldDef . label || key ,
245304 type : fieldDef . type || 'text' ,
246305 ...( fieldDef . options && { options : fieldDef . options } ) ,
247- ...( fieldDef . reference_to && { reference_to : fieldDef . reference_to } ) ,
306+ ...( refTarget && { reference_to : refTarget } ) ,
248307 ...( fieldDef . reference_field && { reference_field : fieldDef . reference_field } ) ,
249308 ...( fieldDef . currency && { currency : fieldDef . currency } ) ,
250309 } ;
@@ -257,6 +316,28 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
257316 ( a : any ) => a . locations ?. includes ( 'record_header' ) ,
258317 ) ;
259318
319+ // Build highlightFields: prefer explicit config, fallback to auto-detect key fields
320+ const explicitHighlight : HighlightField [ ] | undefined = objectDef . views ?. detail ?. highlightFields ;
321+ const highlightFields : HighlightField [ ] = explicitHighlight
322+ ?? Object . entries ( objectDef . fields || { } )
323+ . filter ( ( [ key ] : [ string , any ] ) => HIGHLIGHT_FIELD_NAMES . includes ( key ) )
324+ . map ( ( [ key , def ] : [ string , any ] ) => ( {
325+ name : key ,
326+ label : def . label || key ,
327+ ...( def . type && { type : def . type } ) ,
328+ } ) ) ;
329+
330+ // Build sectionGroups from objectDef detail/form config if available
331+ const sectionGroups : SectionGroup [ ] | undefined =
332+ objectDef . views ?. detail ?. sectionGroups ?? objectDef . views ?. form ?. sectionGroups ;
333+
334+ // Build related entries from reverse-reference child objects
335+ const related = childRelations . map ( ( { childObject, childLabel } ) => ( {
336+ title : childLabel ,
337+ type : 'table' as const ,
338+ data : childRelatedData [ childObject ] || [ ] ,
339+ } ) ) ;
340+
260341 const detailSchema : DetailViewSchema = {
261342 type : 'detail-view' ,
262343 objectName : objectDef . name ,
@@ -267,6 +348,11 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
267348 title : objectDef . label ,
268349 primaryField,
269350 sections,
351+ autoTabs : true ,
352+ autoDiscoverRelated : true ,
353+ ...( related . length > 0 && { related } ) ,
354+ ...( highlightFields . length > 0 && { highlightFields } ) ,
355+ ...( sectionGroups && sectionGroups . length > 0 && { sectionGroups } ) ,
270356 ...( recordHeaderActions . length > 0 && {
271357 actions : [ {
272358 type : 'action:bar' ,
0 commit comments