@@ -64,12 +64,38 @@ export interface UseGroupedDataResult {
6464 toggleGroup : ( key : string ) => void ;
6565}
6666
67+ /**
68+ * Extract a stable identity key from a value. For lookup / master_detail
69+ * fields the cell contains an expanded object (e.g. `{ id, name, ... }`); we
70+ * key off `id` so different referenced records produce distinct groups even
71+ * when they happen to share the same display name. Plain primitives are
72+ * stringified directly.
73+ */
74+ function extractValueKey ( value : any ) : string {
75+ if ( value === undefined || value === null || value === '' ) return '' ;
76+ if ( Array . isArray ( value ) ) {
77+ return value . map ( ( v ) => extractValueKey ( v ) ) . join ( '|' ) ;
78+ }
79+ if ( typeof value === 'object' ) {
80+ const id = ( value as any ) . id ?? ( value as any ) . _id ?? ( value as any ) . pk ?? ( value as any ) . value ;
81+ if ( id !== undefined && id !== null && id !== '' ) return String ( id ) ;
82+ const label = ( value as any ) . name ?? ( value as any ) . label ?? ( value as any ) . title ;
83+ if ( label !== undefined && label !== null && label !== '' ) return String ( label ) ;
84+ try {
85+ return JSON . stringify ( value ) ;
86+ } catch {
87+ return '' ;
88+ }
89+ }
90+ return String ( value ) ;
91+ }
92+
6793/**
6894 * Build a value-only key segment for a single grouping field. Used to compose
6995 * stable composite keys across nesting levels.
7096 */
7197function buildSegmentKey ( row : Record < string , any > , field : string ) : string {
72- return String ( row [ field ] ?? '' ) ;
98+ return extractValueKey ( row [ field ] ) ;
7399}
74100
75101/**
@@ -102,11 +128,28 @@ function buildSegmentLabel(
102128 const f2 = formatValue ( field , v ) ;
103129 if ( f2 !== undefined && f2 !== '' ) return f2 ;
104130 }
105- return String ( v ) ;
131+ return buildSegmentLabel ( v , field ) ;
106132 } )
107133 . join ( ', ' ) ;
108134 return joined || '(empty)' ;
109135 }
136+ // Lookup / master_detail fields: the cell is an expanded object such as
137+ // `{ id, name, ... }`. Stringifying directly produces "[object Object]",
138+ // so prefer common display fields, falling back to the id.
139+ if ( typeof value === 'object' ) {
140+ const label =
141+ ( value as any ) . name ??
142+ ( value as any ) . label ??
143+ ( value as any ) . title ??
144+ ( value as any ) . display_name ??
145+ ( value as any ) . displayName ??
146+ ( value as any ) . fullName ??
147+ ( value as any ) . full_name ;
148+ if ( label !== undefined && label !== null && label !== '' ) return String ( label ) ;
149+ const id = ( value as any ) . id ?? ( value as any ) . _id ?? ( value as any ) . pk ;
150+ if ( id !== undefined && id !== null && id !== '' ) return String ( id ) ;
151+ return '(empty)' ;
152+ }
110153 return String ( value ) ;
111154}
112155
@@ -214,7 +257,11 @@ export function useGroupedData(
214257 }
215258
216259 const order = f . order ?? 'asc' ;
217- keyOrder . sort ( ( a , b ) => compareGroups ( a , b , order ) ) ;
260+ keyOrder . sort ( ( a , b ) => {
261+ const labelA = map . get ( a ) ?. label ?? a ;
262+ const labelB = map . get ( b ) ?. label ?? b ;
263+ return compareGroups ( labelA , labelB , order ) ;
264+ } ) ;
218265
219266 return keyOrder . map ( ( segment ) => {
220267 const entry = map . get ( segment ) ! ;
0 commit comments