88
99import { useState , useEffect , useCallback , useMemo , useRef } from 'react' ;
1010import { useParams , useNavigate , useLocation , Link } from 'react-router-dom' ;
11- import { DetailView , RecordChatterPanel , buildDefaultPageSchema , extractMentions } from '@object-ui/plugin-detail' ;
11+ import { DetailView , RecordChatterPanel , buildDefaultPageSchema , deriveFieldGroupDetailSections , extractMentions } from '@object-ui/plugin-detail' ;
1212import { Empty , EmptyTitle , EmptyDescription } from '@object-ui/components' ;
1313import { useAuth , createAuthenticatedFetch } from '@object-ui/auth' ;
1414import { ActionProvider , useObjectTranslation , useObjectLabel , usePageAssignment , RecordContextProvider , SchemaRenderer , DiscussionContextProvider , HighlightFieldsProvider , useGlobalUndo } from '@object-ui/react' ;
@@ -1225,8 +1225,14 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12251225 ( key ) => key === 'name' || key === 'title'
12261226 ) ;
12271227
1228- // Build sections: prefer form sections from objectDef, fallback to flat field list
1229- const formSections = objectDef . views ?. form ?. sections ;
1228+ // Build sections, in priority order:
1229+ // 1) explicit sections — the spec-writable `detail.sections` block,
1230+ // else legacy `views.form.sections` (the spec's ObjectSchema has no
1231+ // `views` key, so that source only exists on non-spec metadata);
1232+ // 2) sections derived from the designer's `fieldGroups` metadata
1233+ // (see the fieldGroups branch in the fallback below);
1234+ // 3) auto-grouping (primary + collapsible "More details").
1235+ const formSections = ( objectDef as any ) . detail ?. sections ?? objectDef . views ?. form ?. sections ;
12301236 const sections = formSections && formSections . length > 0
12311237 ? formSections . map ( ( sec : any ) => ( {
12321238 title : sec . name ? sectionLabel ( objectDef . name , sec . name , sec . title || sec . name ) : sec . title ,
@@ -1262,14 +1268,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12621268 } ) ,
12631269 } ) )
12641270 : ( ( ) => {
1265- // Auto-grouping (platform B): when no form sections are authored,
1266- // split fields into a primary section and a collapsible
1267- // "More details" section so long-form/secondary fields don't
1268- // dilute the main grid. The primary section stays untitled so
1269- // DetailSection still flattens its chrome when alone.
1270- const allFields = Object . keys ( objectDef . fields || { } )
1271- . filter ( ( key ) => ! AUDIT_FIELD_NAMES . has ( key ) && ! HIDDEN_SYSTEM_FIELD_NAMES . has ( key ) && ! objectDef . fields [ key ] ?. hidden ) ;
1272-
12731271 const toField = ( key : string ) => {
12741272 const fieldDef = objectDef . fields [ key ] ;
12751273 const refTarget = fieldDef . reference_to || fieldDef . reference ;
@@ -1284,35 +1282,70 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12841282 } ;
12851283 } ;
12861284
1287- const primaryKeys = allFields . filter ( ( k ) => ! isSecondaryField ( k , objectDef . fields [ k ] ) ) ;
1288- const secondaryKeys = allFields . filter ( ( k ) => isSecondaryField ( k , objectDef . fields [ k ] ) ) ;
1285+ // Auto-grouping (platform B): split fields into a primary section
1286+ // and a collapsible "More details" section so long-form/secondary
1287+ // fields don't dilute the main grid. The primary section stays
1288+ // untitled so DetailSection still flattens its chrome when alone.
1289+ // Shared by the pure-fallback path and the ungrouped remainder of
1290+ // the fieldGroups path below.
1291+ const splitPrimarySecondary = ( keys : string [ ] ) => {
1292+ const primaryKeys = keys . filter ( ( k ) => ! isSecondaryField ( k , objectDef . fields [ k ] ) ) ;
1293+ const secondaryKeys = keys . filter ( ( k ) => isSecondaryField ( k , objectDef . fields [ k ] ) ) ;
1294+
1295+ // Keep the legacy single-untitled-section behaviour when the
1296+ // split would leave one side empty.
1297+ if ( secondaryKeys . length === 0 || primaryKeys . length === 0 ) {
1298+ return [
1299+ {
1300+ showBorder : false as const ,
1301+ fields : keys . map ( toField ) ,
1302+ } ,
1303+ ] ;
1304+ }
12891305
1290- // Below ~6 primary fields the second section often looks awkward
1291- // — keep the legacy single-untitled-section behaviour. Also
1292- // honour the "no secondary fields" case the same way.
1293- if ( secondaryKeys . length === 0 || primaryKeys . length === 0 ) {
12941306 return [
12951307 {
12961308 showBorder : false as const ,
1297- fields : allFields . map ( toField ) ,
1309+ fields : primaryKeys . map ( toField ) ,
1310+ } ,
1311+ {
1312+ name : 'details' ,
1313+ title : sectionLabel ( objectDef . name , 'details' , t ( 'detail.sectionMoreDetails' , 'More details' ) ) ,
1314+ collapsible : true ,
1315+ defaultCollapsed : false ,
1316+ showBorder : true as const ,
1317+ fields : secondaryKeys . map ( toField ) ,
12981318 } ,
12991319 ] ;
1320+ } ;
1321+
1322+ // 2) fieldGroups-derived sections (object-designer metadata,
1323+ // same source the runtime form honours). Declared groups
1324+ // render as titled cards in declared order; the helper's
1325+ // trailing untitled bucket (ungrouped fields) still goes
1326+ // through the primary/"More details" split so long-form
1327+ // fields stay tucked away. Objects can opt out via
1328+ // `detail.useFieldGroups: false`.
1329+ const grouped = deriveFieldGroupDetailSections ( objectDef as any ) ;
1330+ if ( grouped ) {
1331+ return grouped . flatMap ( ( sec : any ) => {
1332+ if ( ! sec . name ) {
1333+ return splitPrimarySecondary (
1334+ ( sec . fields as any [ ] ) . map ( ( f : any ) => f . name ) ,
1335+ ) ;
1336+ }
1337+ return [ {
1338+ ...sec ,
1339+ title : sectionLabel ( objectDef . name , sec . name , sec . title ) ,
1340+ showBorder : true as const ,
1341+ } ] ;
1342+ } ) ;
13001343 }
13011344
1302- return [
1303- {
1304- showBorder : false as const ,
1305- fields : primaryKeys . map ( toField ) ,
1306- } ,
1307- {
1308- name : 'details' ,
1309- title : sectionLabel ( objectDef . name , 'details' , t ( 'detail.sectionMoreDetails' , 'More details' ) ) ,
1310- collapsible : true ,
1311- defaultCollapsed : false ,
1312- showBorder : true as const ,
1313- fields : secondaryKeys . map ( toField ) ,
1314- } ,
1315- ] ;
1345+ // 3) Pure auto-grouping fallback.
1346+ const allFields = Object . keys ( objectDef . fields || { } )
1347+ . filter ( ( key ) => ! AUDIT_FIELD_NAMES . has ( key ) && ! HIDDEN_SYSTEM_FIELD_NAMES . has ( key ) && ! objectDef . fields [ key ] ?. hidden ) ;
1348+ return splitPrimarySecondary ( allFields ) ;
13161349 } ) ( ) ;
13171350
13181351 // Audit fields (created_at/created_by/updated_at/updated_by) are NOT
@@ -1385,12 +1418,33 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
13851418 return base ;
13861419 } ) ( ) ;
13871420
1388- // Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
1389- const highlightFields : HighlightField [ ] = objectDef . views ?. detail ?. highlightFields ?? [ ] ;
1421+ // Build highlightFields: exclusively from objectDef metadata (no
1422+ // hardcoded fallback). The spec-writable `detail.highlightFields`
1423+ // wins; `views.detail.highlightFields` stays as back-compat for
1424+ // non-spec metadata (the spec's ObjectSchema has no `views` key).
1425+ // Entries may be bare field names — normalize them to the
1426+ // HighlightField shape by resolving label/type from the field def.
1427+ const rawHighlightFields =
1428+ ( objectDef as any ) . detail ?. highlightFields ?? objectDef . views ?. detail ?. highlightFields ?? [ ] ;
1429+ const highlightFields : HighlightField [ ] = ( Array . isArray ( rawHighlightFields ) ? rawHighlightFields : [ ] )
1430+ . map ( ( f : any ) : HighlightField | null => {
1431+ const name = typeof f === 'string' ? f : f ?. name ;
1432+ if ( ! name ) return null ;
1433+ if ( typeof f === 'object' && f . label ) return f as HighlightField ;
1434+ const fieldDef = objectDef . fields ?. [ name ] ;
1435+ return {
1436+ name,
1437+ label : fieldDef ?. label || name ,
1438+ ...( fieldDef ?. type ? { type : fieldDef . type } : { } ) ,
1439+ } ;
1440+ } )
1441+ . filter ( ( f ) : f is HighlightField => ! ! f ) ;
13901442
13911443 // Build sectionGroups from objectDef detail/form config if available
13921444 const sectionGroups : SectionGroup [ ] | undefined =
1393- objectDef . views ?. detail ?. sectionGroups ?? objectDef . views ?. form ?. sectionGroups ;
1445+ ( objectDef as any ) . detail ?. sectionGroups
1446+ ?? objectDef . views ?. detail ?. sectionGroups
1447+ ?? objectDef . views ?. form ?. sectionGroups ;
13941448
13951449 // Build related entries from reverse-reference child objects.
13961450 // `referenceField` is the FK field on the child pointing back to this
0 commit comments