@@ -22,83 +22,187 @@ vi.mock('@object-ui/i18n', () => ({
2222} ) ) ;
2323
2424// Mock components to simple HTML elements
25- vi . mock ( '@object-ui/components' , ( ) => ( {
26- Button : ( { children, onClick, title, ...props } : any ) => (
27- < button onClick = { onClick } title = { title } { ...props } > { children } </ button >
28- ) ,
29- Switch : ( { checked, onCheckedChange, ...props } : any ) => (
30- < button
31- role = "switch"
32- aria-checked = { checked }
33- onClick = { ( ) => onCheckedChange ?.( ! checked ) }
34- { ...props }
35- />
36- ) ,
37- Input : ( { value, onChange, readOnly, placeholder, ...props } : any ) => (
38- < input
39- value = { value }
40- onChange = { onChange }
41- readOnly = { readOnly }
42- placeholder = { placeholder }
43- { ...props }
44- />
45- ) ,
46- Checkbox : ( { checked, onCheckedChange, ...props } : any ) => (
47- < input
48- type = "checkbox"
49- checked = { ! ! checked }
50- onChange = { ( ) => onCheckedChange ?.( ! checked ) }
51- { ...props }
52- />
53- ) ,
54- ConfigRow : ( { label, value, onClick, children, className } : any ) => {
55- const Wrapper = onClick ? 'button' : 'div' ;
56- return (
57- < Wrapper className = { className } onClick = { onClick } type = { onClick ? 'button' : undefined } >
58- < span > { label } </ span >
59- { children || < span > { value } </ span > }
60- </ Wrapper >
61- ) ;
62- } ,
63- SectionHeader : ( { title, collapsible, collapsed, onToggle, testId } : any ) => {
64- if ( collapsible ) {
25+ vi . mock ( '@object-ui/components' , ( ) => {
26+ const React = require ( 'react' ) ;
27+
28+ // useConfigDraft mock — mirrors real implementation
29+ function useConfigDraft ( source : any , options ?: any ) {
30+ const [ draft , setDraft ] = React . useState ( { ...source } ) ;
31+ const [ isDirty , setIsDirty ] = React . useState ( options ?. mode === 'create' ) ;
32+
33+ React . useEffect ( ( ) => {
34+ setDraft ( { ...source } ) ;
35+ setIsDirty ( options ?. mode === 'create' ) ;
36+ } , [ source ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
37+
38+ const updateField = React . useCallback ( ( field : string , value : any ) => {
39+ setDraft ( ( prev : any ) => ( { ...prev , [ field ] : value } ) ) ;
40+ setIsDirty ( true ) ;
41+ options ?. onUpdate ?.( field , value ) ;
42+ } , [ options ?. onUpdate ] ) ;
43+
44+ const discard = React . useCallback ( ( ) => {
45+ setDraft ( { ...source } ) ;
46+ setIsDirty ( false ) ;
47+ } , [ source ] ) ;
48+
49+ return { draft, isDirty, updateField, discard, setDraft } ;
50+ }
51+
52+ // ConfigPanelRenderer mock — renders schema sections with proper collapse/visibility
53+ function ConfigPanelRenderer ( { open, onClose, schema, draft, isDirty, onFieldChange, onSave, onDiscard, panelRef, role, ariaLabel, tabIndex, testId, saveLabel, discardLabel, className } : any ) {
54+ const [ collapsed , setCollapsed ] = React . useState < Record < string , boolean > > ( { } ) ;
55+ if ( ! open ) return null ;
56+
57+ return React . createElement ( 'div' , {
58+ ref : panelRef ,
59+ 'data-testid' : testId || 'config-panel' ,
60+ role,
61+ 'aria-label' : ariaLabel ,
62+ tabIndex,
63+ className : `absolute inset-y-0 right-0 w-full sm:w-72 lg:w-80 sm:relative sm:inset-auto border-l bg-background flex flex-col shrink-0 z-20 ${ className || '' } ` ,
64+ } ,
65+ // Header with breadcrumb
66+ React . createElement ( 'div' , { className : 'px-4 py-3 border-b flex items-center justify-between shrink-0' } ,
67+ React . createElement ( 'div' , { 'data-testid' : 'panel-breadcrumb' , className : 'flex items-center gap-1 text-sm truncate' } ,
68+ ...schema . breadcrumb . map ( ( seg : string , i : number ) => [
69+ i > 0 && React . createElement ( 'span' , { key : `sep-${ i } ` , className : 'text-muted-foreground' } , '›' ) ,
70+ React . createElement ( 'span' , {
71+ key : `seg-${ i } ` ,
72+ className : i === schema . breadcrumb . length - 1 ? 'text-foreground font-semibold' : 'text-muted-foreground' ,
73+ } , seg ) ,
74+ ] ) . flat ( ) . filter ( Boolean ) ,
75+ ) ,
76+ React . createElement ( 'button' , {
77+ onClick : onClose ,
78+ title : 'console.objectView.closePanel' ,
79+ className : 'h-7 w-7 p-0' ,
80+ } , '×' ) ,
81+ ) ,
82+ // Scrollable sections
83+ React . createElement ( 'div' , { className : 'flex-1 overflow-auto px-4 pb-4' } ,
84+ ...schema . sections . map ( ( section : any ) => {
85+ if ( section . visibleWhen && ! section . visibleWhen ( draft ) ) return null ;
86+ const isCollapsed = collapsed [ section . key ] ?? section . defaultCollapsed ?? false ;
87+
88+ return React . createElement ( 'div' , { key : section . key } ,
89+ // Section header
90+ section . collapsible
91+ ? React . createElement ( 'button' , {
92+ 'data-testid' : `section-${ section . key } ` ,
93+ onClick : ( ) => setCollapsed ( ( prev : any ) => ( { ...prev , [ section . key ] : ! isCollapsed } ) ) ,
94+ type : 'button' ,
95+ 'aria-expanded' : ! isCollapsed ,
96+ } , React . createElement ( 'h3' , null , section . title ) )
97+ : React . createElement ( 'div' , null , React . createElement ( 'h3' , null , section . title ) ) ,
98+ // Section hint
99+ section . hint && React . createElement ( 'p' , { className : 'text-[10px] text-muted-foreground mb-1' } , section . hint ) ,
100+ // Section fields
101+ ! isCollapsed && React . createElement ( 'div' , { className : 'space-y-0.5' } ,
102+ ...section . fields . map ( ( field : any ) => {
103+ if ( field . visibleWhen && ! field . visibleWhen ( draft ) ) return null ;
104+ if ( field . type === 'custom' && field . render ) {
105+ return React . createElement ( React . Fragment , { key : field . key } ,
106+ field . render ( draft [ field . key ] , ( v : any ) => onFieldChange ( field . key , v ) , draft ) ,
107+ ) ;
108+ }
109+ return null ;
110+ } ) ,
111+ ) ,
112+ ) ;
113+ } ) ,
114+ ) ,
115+ // Footer
116+ isDirty && React . createElement ( 'div' , {
117+ 'data-testid' : 'view-config-footer' ,
118+ className : 'px-4 py-3 border-t flex items-center justify-end gap-2 shrink-0 bg-background' ,
119+ } ,
120+ React . createElement ( 'button' , { 'data-testid' : 'view-config-discard' , onClick : onDiscard } , discardLabel ) ,
121+ React . createElement ( 'button' , { 'data-testid' : 'view-config-save' , onClick : onSave } , saveLabel ) ,
122+ ) ,
123+ ) ;
124+ }
125+
126+ return {
127+ useConfigDraft,
128+ ConfigPanelRenderer,
129+ Button : ( { children, onClick, title, ...props } : any ) => (
130+ < button onClick = { onClick } title = { title } { ...props } > { children } </ button >
131+ ) ,
132+ Switch : ( { checked, onCheckedChange, ...props } : any ) => (
133+ < button
134+ role = "switch"
135+ aria-checked = { checked }
136+ onClick = { ( ) => onCheckedChange ?.( ! checked ) }
137+ { ...props }
138+ />
139+ ) ,
140+ Input : ( { value, onChange, readOnly, placeholder, ...props } : any ) => (
141+ < input
142+ value = { value }
143+ onChange = { onChange }
144+ readOnly = { readOnly }
145+ placeholder = { placeholder }
146+ { ...props }
147+ />
148+ ) ,
149+ Checkbox : ( { checked, onCheckedChange, ...props } : any ) => (
150+ < input
151+ type = "checkbox"
152+ checked = { ! ! checked }
153+ onChange = { ( ) => onCheckedChange ?.( ! checked ) }
154+ { ...props }
155+ />
156+ ) ,
157+ ConfigRow : ( { label, value, onClick, children, className } : any ) => {
158+ const Wrapper = onClick ? 'button' : 'div' ;
65159 return (
66- < button data-testid = { testId } onClick = { onToggle } type = "button" aria-expanded = { ! collapsed } >
67- < h3 > { title } </ h3 >
68- </ button >
160+ < Wrapper className = { className } onClick = { onClick } type = { onClick ? 'button' : undefined } >
161+ < span > { label } </ span >
162+ { children || < span > { value } </ span > }
163+ </ Wrapper >
69164 ) ;
70- }
71- return < div data-testid = { testId } > < h3 > { title } </ h3 > </ div > ;
72- } ,
73- FilterBuilder : ( { fields, value, onChange } : any ) => {
74- let counter = 0 ;
75- return (
76- < div data-testid = "mock-filter-builder" data-field-count = { fields ?. length || 0 } data-condition-count = { value ?. conditions ?. length || 0 } >
77- < button data-testid = "filter-builder-add" onClick = { ( ) => {
78- const newConditions = [ ...( value ?. conditions || [ ] ) , { id : `mock-filter-${ Date . now ( ) } -${ ++ counter } ` , field : fields ?. [ 0 ] ?. value || '' , operator : 'equals' , value : '' } ] ;
79- onChange ?.( { ...value , conditions : newConditions } ) ;
80- } } > Add filter</ button >
81- { value ?. conditions ?. map ( ( c : any , i : number ) => (
82- < span key = { c . id || i } data-testid = { `filter-condition-${ i } ` } > { c . field } { c . operator } { String ( c . value ) } </ span >
83- ) ) }
84- </ div >
85- ) ;
86- } ,
87- SortBuilder : ( { fields, value, onChange } : any ) => {
88- let counter = 0 ;
89- return (
90- < div data-testid = "mock-sort-builder" data-field-count = { fields ?. length || 0 } data-sort-count = { value ?. length || 0 } >
91- < button data-testid = "sort-builder-add" onClick = { ( ) => {
92- const newItems = [ ...( value || [ ] ) , { id : `mock-sort-${ Date . now ( ) } -${ ++ counter } ` , field : fields ?. [ 0 ] ?. value || '' , order : 'asc' } ] ;
93- onChange ?.( newItems ) ;
94- } } > Add sort</ button >
95- { value ?. map ( ( s : any , i : number ) => (
96- < span key = { s . id || i } data-testid = { `sort-item-${ i } ` } > { s . field } { s . order } </ span >
97- ) ) }
98- </ div >
99- ) ;
100- } ,
101- } ) ) ;
165+ } ,
166+ SectionHeader : ( { title, collapsible, collapsed, onToggle, testId } : any ) => {
167+ if ( collapsible ) {
168+ return (
169+ < button data-testid = { testId } onClick = { onToggle } type = "button" aria-expanded = { ! collapsed } >
170+ < h3 > { title } </ h3 >
171+ </ button >
172+ ) ;
173+ }
174+ return < div data-testid = { testId } > < h3 > { title } </ h3 > </ div > ;
175+ } ,
176+ FilterBuilder : ( { fields, value, onChange } : any ) => {
177+ let counter = 0 ;
178+ return (
179+ < div data-testid = "mock-filter-builder" data-field-count = { fields ?. length || 0 } data-condition-count = { value ?. conditions ?. length || 0 } >
180+ < button data-testid = "filter-builder-add" onClick = { ( ) => {
181+ const newConditions = [ ...( value ?. conditions || [ ] ) , { id : `mock-filter-${ Date . now ( ) } -${ ++ counter } ` , field : fields ?. [ 0 ] ?. value || '' , operator : 'equals' , value : '' } ] ;
182+ onChange ?.( { ...value , conditions : newConditions } ) ;
183+ } } > Add filter</ button >
184+ { value ?. conditions ?. map ( ( c : any , i : number ) => (
185+ < span key = { c . id || i } data-testid = { `filter-condition-${ i } ` } > { c . field } { c . operator } { String ( c . value ) } </ span >
186+ ) ) }
187+ </ div >
188+ ) ;
189+ } ,
190+ SortBuilder : ( { fields, value, onChange } : any ) => {
191+ let counter = 0 ;
192+ return (
193+ < div data-testid = "mock-sort-builder" data-field-count = { fields ?. length || 0 } data-sort-count = { value ?. length || 0 } >
194+ < button data-testid = "sort-builder-add" onClick = { ( ) => {
195+ const newItems = [ ...( value || [ ] ) , { id : `mock-sort-${ Date . now ( ) } -${ ++ counter } ` , field : fields ?. [ 0 ] ?. value || '' , order : 'asc' } ] ;
196+ onChange ?.( newItems ) ;
197+ } } > Add sort</ button >
198+ { value ?. map ( ( s : any , i : number ) => (
199+ < span key = { s . id || i } data-testid = { `sort-item-${ i } ` } > { s . field } { s . order } </ span >
200+ ) ) }
201+ </ div >
202+ ) ;
203+ } ,
204+ } ;
205+ } ) ;
102206
103207const mockActiveView = {
104208 id : 'all' ,
0 commit comments