1- import { useEffect , useMemo , useState } from "react" ;
1+ import { useCallback , useEffect , useMemo , useState } from "react" ;
22import type { FieldDefinition , FieldMenuProps } from "../types" ;
33
44export const FieldMenu : React . FC < FieldMenuProps > = ( {
@@ -37,11 +37,61 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
3737 } ;
3838 } , [ position ] ) ;
3939
40- if ( ! isVisible ) return null ;
41-
42- const visibleFields = filteredFields ?? availableFields ;
40+ const fieldsToDisplay = filteredFields ?? availableFields ;
4341 const hasFilter = Boolean ( filterQuery ) ;
44- const hasVisibleFields = visibleFields . length > 0 ;
42+
43+ const groupedFields = useMemo ( ( ) => {
44+ const groups : { category : string ; fields : FieldDefinition [ ] } [ ] = [ ] ;
45+ const categoryIndex = new Map < string , number > ( ) ;
46+
47+ fieldsToDisplay . forEach ( ( field ) => {
48+ const categoryName = field . category ?. trim ( ) || "Uncategorized" ;
49+ const existingIndex = categoryIndex . get ( categoryName ) ;
50+
51+ if ( existingIndex !== undefined ) {
52+ groups [ existingIndex ] . fields . push ( field ) ;
53+ return ;
54+ }
55+
56+ categoryIndex . set ( categoryName , groups . length ) ;
57+ groups . push ( { category : categoryName , fields : [ field ] } ) ;
58+ } ) ;
59+
60+ return groups ;
61+ } , [ fieldsToDisplay ] ) ;
62+
63+ const [ expandedCategories , setExpandedCategories ] = useState < Record < string , boolean > > ( { } ) ;
64+
65+ useEffect ( ( ) => {
66+ setExpandedCategories ( ( previous ) => {
67+ if ( groupedFields . length === 0 ) {
68+ return Object . keys ( previous ) . length === 0 ? previous : { } ;
69+ }
70+
71+ const next : Record < string , boolean > = { } ;
72+ let hasChanges = Object . keys ( previous ) . length !== groupedFields . length ;
73+
74+ groupedFields . forEach ( ( { category } , index ) => {
75+ // Auto-expand all categories when filtering is active
76+ const target = hasFilter ? true : ( previous [ category ] ?? index === 0 ) ;
77+ next [ category ] = target ;
78+ if ( ! hasChanges && previous [ category ] !== target ) {
79+ hasChanges = true ;
80+ }
81+ } ) ;
82+
83+ return hasChanges ? next : previous ;
84+ } ) ;
85+ } , [ groupedFields , hasFilter ] ) ;
86+
87+ const toggleCategory = useCallback ( ( category : string ) => {
88+ setExpandedCategories ( ( previous ) => ( {
89+ ...previous ,
90+ [ category ] : ! previous [ category ] ,
91+ } ) ) ;
92+ } , [ ] ) ;
93+
94+ if ( ! isVisible ) return null ;
4595
4696 const handleCreateField = async ( ) => {
4797 const trimmedName = newFieldName . trim ( ) ;
@@ -68,69 +118,22 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
68118
69119 return (
70120 < div className = "superdoc-field-menu" style = { menuStyle } >
71- < div
72- style = { {
73- padding : "0 16px 8px 16px" ,
74- borderBottom : "1px solid #f3f4f6" ,
75- marginBottom : "8px" ,
76- } }
77- >
121+ { hasFilter && (
78122 < div
79123 style = { {
80- fontSize : "12px " ,
81- color : "#6b7280 " ,
82- textTransform : "uppercase " ,
124+ padding : "8px 16px " ,
125+ borderBottom : "1px solid #f0f0f0 " ,
126+ marginBottom : "4px " ,
83127 } }
84128 >
85- Insert Field
86- </ div >
87- { hasFilter && (
88- < div style = { { fontSize : "12px" , color : "#6b7280" , marginTop : "4px" } } >
129+ < div style = { { fontSize : "12px" , color : "#6b7280" } } >
89130 Filtering results for
90131 < span
91132 style = { { fontWeight : 600 , color : "#111827" , marginLeft : "4px" } }
92133 >
93134 { filterQuery }
94135 </ span >
95136 </ div >
96- ) }
97- </ div >
98-
99- { hasVisibleFields ? (
100- visibleFields . map ( ( field ) => (
101- < div
102- key = { field . id }
103- className = "field-menu-item"
104- onClick = { ( ) => onSelect ( field ) }
105- style = { {
106- padding : "8px 16px" ,
107- cursor : "pointer" ,
108- } }
109- >
110- < span style = { { fontWeight : 500 } } > { field . label } </ span >
111- { field . category && (
112- < span
113- style = { {
114- fontSize : "0.85em" ,
115- color : "#666" ,
116- marginLeft : "8px" ,
117- } }
118- >
119- { field . category }
120- </ span >
121- ) }
122- </ div >
123- ) )
124- ) : (
125- < div
126- style = { {
127- padding : "16px" ,
128- fontSize : "13px" ,
129- color : "#6b7280" ,
130- textAlign : "center" ,
131- } }
132- >
133- No matching fields
134137 </ div >
135138 ) }
136139
@@ -211,6 +214,105 @@ export const FieldMenu: React.FC<FieldMenuProps> = ({
211214 </ div >
212215 ) }
213216
217+ { allowCreate && availableFields . length > 0 && (
218+ < div
219+ style = { {
220+ borderTop : "1px solid #eee" ,
221+ margin : "4px 0" ,
222+ } }
223+ />
224+ ) }
225+
226+ { groupedFields . length === 0 ? (
227+ < div
228+ style = { {
229+ padding : "16px" ,
230+ fontSize : "13px" ,
231+ color : "#6b7280" ,
232+ textAlign : "center" ,
233+ } }
234+ >
235+ No matching fields
236+ </ div >
237+ ) : (
238+ groupedFields . map ( ( { category, fields } , index ) => {
239+ const isExpanded = Boolean ( expandedCategories [ category ] ) ;
240+ const itemsMaxHeight = `${ Math . max ( fields . length * 40 , 0 ) } px` ;
241+
242+ return (
243+ < div
244+ key = { category }
245+ style = { {
246+ borderTop : index === 0 && allowCreate ? undefined : "1px solid #f0f0f0" ,
247+ } }
248+ >
249+ < button
250+ type = "button"
251+ onClick = { ( ) => toggleCategory ( category ) }
252+ style = { {
253+ width : "100%" ,
254+ display : "flex" ,
255+ alignItems : "center" ,
256+ justifyContent : "space-between" ,
257+ padding : "8px 16px" ,
258+ background : "transparent" ,
259+ border : "none" ,
260+ cursor : "pointer" ,
261+ fontWeight : 500 ,
262+ textAlign : "left" ,
263+ } }
264+ >
265+ < span >
266+ { category } ({ fields . length } )
267+ </ span >
268+ < span
269+ aria-hidden
270+ style = { {
271+ display : "inline-block" ,
272+ width : "8px" ,
273+ height : "8px" ,
274+ borderRight : "2px solid #666" ,
275+ borderBottom : "2px solid #666" ,
276+ transform : isExpanded ? "rotate(45deg)" : "rotate(-45deg)" ,
277+ transition : "transform 0.2s ease" ,
278+ marginLeft : "12px" ,
279+ } }
280+ />
281+ </ button >
282+ < div
283+ data-category = { category }
284+ aria-hidden = { ! isExpanded }
285+ style = { {
286+ overflow : "hidden" ,
287+ maxHeight : isExpanded ? itemsMaxHeight : "0px" ,
288+ opacity : isExpanded ? 1 : 0 ,
289+ transition : "max-height 0.2s ease, opacity 0.2s ease" ,
290+ pointerEvents : isExpanded ? "auto" : "none" ,
291+ } }
292+ >
293+ < div style = { { padding : isExpanded ? "4px 0" : 0 } } >
294+ { fields . map ( ( field ) => (
295+ < div
296+ key = { field . id }
297+ className = "field-menu-item"
298+ onClick = { ( ) => onSelect ( field ) }
299+ style = { {
300+ padding : "8px 16px" ,
301+ cursor : "pointer" ,
302+ display : "flex" ,
303+ alignItems : "center" ,
304+ justifyContent : "space-between" ,
305+ } }
306+ >
307+ < span style = { { fontWeight : 500 } } > { field . label } </ span >
308+ </ div >
309+ ) ) }
310+ </ div >
311+ </ div >
312+ </ div >
313+ ) ;
314+ } ) ) }
315+
214316 < div
215317 style = { {
216318 borderTop : "1px solid #eee" ,
0 commit comments