@@ -43,14 +43,50 @@ vi.mock('@object-ui/components', () => ({
4343 { ...props }
4444 />
4545 ) ,
46+ Checkbox : ( { checked, onCheckedChange, ...props } : any ) => (
47+ < input
48+ type = "checkbox"
49+ checked = { ! ! checked }
50+ onChange = { ( ) => onCheckedChange ?.( ! checked ) }
51+ { ...props }
52+ />
53+ ) ,
54+ FilterBuilder : ( { fields, value, onChange, ...props } : any ) => {
55+ let counter = 0 ;
56+ return (
57+ < div data-testid = "mock-filter-builder" data-field-count = { fields ?. length || 0 } data-condition-count = { value ?. conditions ?. length || 0 } >
58+ < button data-testid = "filter-builder-add" onClick = { ( ) => {
59+ const newConditions = [ ...( value ?. conditions || [ ] ) , { id : `mock-filter-${ Date . now ( ) } -${ ++ counter } ` , field : fields ?. [ 0 ] ?. value || '' , operator : 'equals' , value : '' } ] ;
60+ onChange ?.( { ...value , conditions : newConditions } ) ;
61+ } } > Add filter</ button >
62+ { value ?. conditions ?. map ( ( c : any , i : number ) => (
63+ < span key = { c . id || i } data-testid = { `filter-condition-${ i } ` } > { c . field } { c . operator } { String ( c . value ) } </ span >
64+ ) ) }
65+ </ div >
66+ ) ;
67+ } ,
68+ SortBuilder : ( { fields, value, onChange, ...props } : any ) => {
69+ let counter = 0 ;
70+ return (
71+ < div data-testid = "mock-sort-builder" data-field-count = { fields ?. length || 0 } data-sort-count = { value ?. length || 0 } >
72+ < button data-testid = "sort-builder-add" onClick = { ( ) => {
73+ const newItems = [ ...( value || [ ] ) , { id : `mock-sort-${ Date . now ( ) } -${ ++ counter } ` , field : fields ?. [ 0 ] ?. value || '' , order : 'asc' } ] ;
74+ onChange ?.( newItems ) ;
75+ } } > Add sort</ button >
76+ { value ?. map ( ( s : any , i : number ) => (
77+ < span key = { s . id || i } data-testid = { `sort-item-${ i } ` } > { s . field } { s . order } </ span >
78+ ) ) }
79+ </ div >
80+ ) ;
81+ } ,
4682} ) ) ;
4783
4884const mockActiveView = {
4985 id : 'all' ,
5086 label : 'All Records' ,
5187 type : 'grid' ,
5288 columns : [ 'name' , 'stage' , 'amount' ] ,
53- filter : [ { field : 'stage' , operator : '=' , value : 'active' } ] ,
89+ filter : [ 'stage' , '=' , 'active' ] , // spec-style single triplet
5490 sort : [ { field : 'name' , order : 'asc' } ] ,
5591} ;
5692
@@ -140,7 +176,7 @@ describe('ViewConfigPanel', () => {
140176 expect ( screen . getByText ( 'console.objectView.noDescription' ) ) . toBeInTheDocument ( ) ;
141177 } ) ;
142178
143- it ( 'displays column count ' , ( ) => {
179+ it ( 'displays column checkboxes for each field ' , ( ) => {
144180 render (
145181 < ViewConfigPanel
146182 open = { true }
@@ -150,8 +186,15 @@ describe('ViewConfigPanel', () => {
150186 />
151187 ) ;
152188
153- // 3 columns configured
154- expect ( screen . getByText ( 'console.objectView.columnsConfigured' . replace ( '{{count}}' , '3' ) ) ) . toBeInTheDocument ( ) ;
189+ // 3 fields → 3 checkboxes
190+ expect ( screen . getByTestId ( 'column-selector' ) ) . toBeInTheDocument ( ) ;
191+ expect ( screen . getByTestId ( 'col-checkbox-name' ) ) . toBeInTheDocument ( ) ;
192+ expect ( screen . getByTestId ( 'col-checkbox-stage' ) ) . toBeInTheDocument ( ) ;
193+ expect ( screen . getByTestId ( 'col-checkbox-amount' ) ) . toBeInTheDocument ( ) ;
194+ // Columns in activeView should be checked
195+ expect ( screen . getByTestId ( 'col-checkbox-name' ) ) . toBeChecked ( ) ;
196+ expect ( screen . getByTestId ( 'col-checkbox-stage' ) ) . toBeChecked ( ) ;
197+ expect ( screen . getByTestId ( 'col-checkbox-amount' ) ) . toBeChecked ( ) ;
155198 } ) ;
156199
157200 it ( 'displays object source name' , ( ) => {
@@ -225,7 +268,7 @@ describe('ViewConfigPanel', () => {
225268 expect ( screen . getByTestId ( 'view-type-select' ) ) . toHaveValue ( 'kanban' ) ;
226269 } ) ;
227270
228- it ( 'shows "None" for empty filters and columns ' , ( ) => {
271+ it ( 'shows inline builders with zero items for empty view ' , ( ) => {
229272 render (
230273 < ViewConfigPanel
231274 open = { true }
@@ -235,9 +278,10 @@ describe('ViewConfigPanel', () => {
235278 />
236279 ) ;
237280
238- // Should show "None" for columns, filters
239- const noneTexts = screen . getAllByText ( 'console.objectView.none' ) ;
240- expect ( noneTexts . length ) . toBeGreaterThanOrEqual ( 2 ) ;
281+ // FilterBuilder should have 0 conditions
282+ expect ( screen . getByTestId ( 'mock-filter-builder' ) ) . toHaveAttribute ( 'data-condition-count' , '0' ) ;
283+ // SortBuilder should have 0 items
284+ expect ( screen . getByTestId ( 'mock-sort-builder' ) ) . toHaveAttribute ( 'data-sort-count' , '0' ) ;
241285 } ) ;
242286
243287 it ( 'has correct ARIA attributes when open' , ( ) => {
@@ -403,62 +447,85 @@ describe('ViewConfigPanel', () => {
403447 expect ( onViewUpdate ) . toHaveBeenCalledWith ( 'type' , 'kanban' ) ;
404448 } ) ;
405449
406- it ( 'calls onOpenEditor when clicking columns row' , ( ) => {
407- const onOpenEditor = vi . fn ( ) ;
450+ it ( 'renders inline FilterBuilder with correct conditions from activeView' , ( ) => {
408451 render (
409452 < ViewConfigPanel
410453 open = { true }
411454 onClose = { vi . fn ( ) }
412455 activeView = { mockActiveView }
413456 objectDef = { mockObjectDef }
414- onOpenEditor = { onOpenEditor }
415457 />
416458 ) ;
417459
418- // Click the columns row — it's a button with the columns label
419- const columnsRow = screen . getByText ( 'console.objectView.columns' ) . closest ( 'button' ) ;
420- expect ( columnsRow ) . toBeTruthy ( ) ;
421- fireEvent . click ( columnsRow ! ) ;
422-
423- expect ( onOpenEditor ) . toHaveBeenCalledWith ( 'columns' ) ;
460+ const fb = screen . getByTestId ( 'mock-filter-builder' ) ;
461+ expect ( fb ) . toHaveAttribute ( 'data-condition-count' , '1' ) ;
462+ expect ( fb ) . toHaveAttribute ( 'data-field-count' , '3' ) ;
463+ expect ( screen . getByTestId ( 'filter-condition-0' ) ) . toHaveTextContent ( 'stage equals active' ) ;
424464 } ) ;
425465
426- it ( 'calls onOpenEditor when clicking filters row' , ( ) => {
427- const onOpenEditor = vi . fn ( ) ;
466+ it ( 'renders inline SortBuilder with correct items from activeView' , ( ) => {
428467 render (
429468 < ViewConfigPanel
430469 open = { true }
431470 onClose = { vi . fn ( ) }
432471 activeView = { mockActiveView }
433472 objectDef = { mockObjectDef }
434- onOpenEditor = { onOpenEditor }
435473 />
436474 ) ;
437475
438- const filterRow = screen . getByText ( 'console.objectView.filterBy' ) . closest ( 'button' ) ;
439- expect ( filterRow ) . toBeTruthy ( ) ;
440- fireEvent . click ( filterRow ! ) ;
476+ const sb = screen . getByTestId ( 'mock-sort-builder' ) ;
477+ expect ( sb ) . toHaveAttribute ( 'data-sort-count' , '1' ) ;
478+ expect ( sb ) . toHaveAttribute ( 'data-field-count' , '3' ) ;
479+ expect ( screen . getByTestId ( 'sort-item-0' ) ) . toHaveTextContent ( 'name asc' ) ;
480+ } ) ;
441481
442- expect ( onOpenEditor ) . toHaveBeenCalledWith ( 'filters' ) ;
482+ it ( 'updates draft when adding a filter via FilterBuilder' , ( ) => {
483+ const onViewUpdate = vi . fn ( ) ;
484+ render (
485+ < ViewConfigPanel
486+ open = { true }
487+ onClose = { vi . fn ( ) }
488+ activeView = { { id : 'empty' , label : 'Empty' , type : 'grid' } }
489+ objectDef = { mockObjectDef }
490+ onViewUpdate = { onViewUpdate }
491+ />
492+ ) ;
493+
494+ fireEvent . click ( screen . getByTestId ( 'filter-builder-add' ) ) ;
495+ expect ( onViewUpdate ) . toHaveBeenCalledWith ( 'filter' , expect . any ( Array ) ) ;
443496 } ) ;
444497
445- it ( 'calls onOpenEditor when clicking sort row ' , ( ) => {
446- const onOpenEditor = vi . fn ( ) ;
498+ it ( 'updates draft when adding a sort via SortBuilder ' , ( ) => {
499+ const onViewUpdate = vi . fn ( ) ;
447500 render (
448501 < ViewConfigPanel
449502 open = { true }
450503 onClose = { vi . fn ( ) }
451- activeView = { mockActiveView }
504+ activeView = { { id : 'empty' , label : 'Empty' , type : 'grid' } }
452505 objectDef = { mockObjectDef }
453- onOpenEditor = { onOpenEditor }
506+ onViewUpdate = { onViewUpdate }
454507 />
455508 ) ;
456509
457- const sortRow = screen . getByText ( 'console.objectView.sortBy' ) . closest ( 'button' ) ;
458- expect ( sortRow ) . toBeTruthy ( ) ;
459- fireEvent . click ( sortRow ! ) ;
510+ fireEvent . click ( screen . getByTestId ( 'sort-builder-add' ) ) ;
511+ expect ( onViewUpdate ) . toHaveBeenCalledWith ( 'sort' , expect . any ( Array ) ) ;
512+ } ) ;
513+
514+ it ( 'toggles column checkbox and calls onViewUpdate with updated columns' , ( ) => {
515+ const onViewUpdate = vi . fn ( ) ;
516+ render (
517+ < ViewConfigPanel
518+ open = { true }
519+ onClose = { vi . fn ( ) }
520+ activeView = { mockActiveView }
521+ objectDef = { mockObjectDef }
522+ onViewUpdate = { onViewUpdate }
523+ />
524+ ) ;
460525
461- expect ( onOpenEditor ) . toHaveBeenCalledWith ( 'sort' ) ;
526+ // Uncheck the 'stage' column
527+ fireEvent . click ( screen . getByTestId ( 'col-checkbox-stage' ) ) ;
528+ expect ( onViewUpdate ) . toHaveBeenCalledWith ( 'columns' , [ 'name' , 'amount' ] ) ;
462529 } ) ;
463530
464531 it ( 'saves draft via onSave when Save button is clicked' , ( ) => {
@@ -551,4 +618,149 @@ describe('ViewConfigPanel', () => {
551618 expect ( screen . getByTestId ( 'toggle-allowExport' ) ) . toHaveAttribute ( 'aria-checked' , 'false' ) ;
552619 expect ( screen . getByTestId ( 'toggle-addRecordViaForm' ) ) . toHaveAttribute ( 'aria-checked' , 'true' ) ;
553620 } ) ;
621+
622+ // ── Real-time draft propagation tests (issue fix) ──
623+
624+ it ( 'keeps dirty state when re-rendered with same view ID but updated activeView' , ( ) => {
625+ const onViewUpdate = vi . fn ( ) ;
626+ const { rerender } = render (
627+ < ViewConfigPanel
628+ open = { true }
629+ onClose = { vi . fn ( ) }
630+ activeView = { mockActiveView }
631+ objectDef = { mockObjectDef }
632+ onViewUpdate = { onViewUpdate }
633+ />
634+ ) ;
635+
636+ // Toggle showSearch — panel becomes dirty
637+ fireEvent . click ( screen . getByTestId ( 'toggle-showSearch' ) ) ;
638+ expect ( screen . getByTestId ( 'view-config-footer' ) ) . toBeInTheDocument ( ) ;
639+
640+ // Simulate parent re-rendering with the same view ID but merged draft
641+ // (this happens when onViewUpdate propagates to parent viewDraft → activeView)
642+ rerender (
643+ < ViewConfigPanel
644+ open = { true }
645+ onClose = { vi . fn ( ) }
646+ activeView = { { ...mockActiveView , showSearch : false } }
647+ objectDef = { mockObjectDef }
648+ onViewUpdate = { onViewUpdate }
649+ />
650+ ) ;
651+
652+ // Draft footer should still be visible (isDirty should NOT reset for same view ID)
653+ expect ( screen . getByTestId ( 'view-config-footer' ) ) . toBeInTheDocument ( ) ;
654+ } ) ;
655+
656+ it ( 'resets dirty state when activeView changes to a different view ID' , ( ) => {
657+ const { rerender } = render (
658+ < ViewConfigPanel
659+ open = { true }
660+ onClose = { vi . fn ( ) }
661+ activeView = { mockActiveView }
662+ objectDef = { mockObjectDef }
663+ />
664+ ) ;
665+
666+ // Make the panel dirty
667+ fireEvent . click ( screen . getByTestId ( 'toggle-showSearch' ) ) ;
668+ expect ( screen . getByTestId ( 'view-config-footer' ) ) . toBeInTheDocument ( ) ;
669+
670+ // Switch to a completely different view
671+ rerender (
672+ < ViewConfigPanel
673+ open = { true }
674+ onClose = { vi . fn ( ) }
675+ activeView = { { id : 'pipeline' , label : 'Pipeline' , type : 'kanban' , columns : [ 'name' ] } }
676+ objectDef = { mockObjectDef }
677+ />
678+ ) ;
679+
680+ // Draft should reset — footer should be gone
681+ expect ( screen . queryByTestId ( 'view-config-footer' ) ) . not . toBeInTheDocument ( ) ;
682+ } ) ;
683+
684+ it ( 'calls onViewUpdate for each real-time field change to enable live preview' , ( ) => {
685+ const onViewUpdate = vi . fn ( ) ;
686+ render (
687+ < ViewConfigPanel
688+ open = { true }
689+ onClose = { vi . fn ( ) }
690+ activeView = { mockActiveView }
691+ objectDef = { mockObjectDef }
692+ onViewUpdate = { onViewUpdate }
693+ />
694+ ) ;
695+
696+ // Toggle multiple switches
697+ fireEvent . click ( screen . getByTestId ( 'toggle-showSearch' ) ) ;
698+ fireEvent . click ( screen . getByTestId ( 'toggle-showFilters' ) ) ;
699+
700+ expect ( onViewUpdate ) . toHaveBeenCalledTimes ( 2 ) ;
701+ expect ( onViewUpdate ) . toHaveBeenCalledWith ( 'showSearch' , false ) ;
702+ expect ( onViewUpdate ) . toHaveBeenCalledWith ( 'showFilters' , false ) ;
703+ } ) ;
704+
705+ // ── Spec-style filter bridge tests ──
706+
707+ it ( 'parses nested spec-style filter array [[field,op,val],[field,op,val]]' , ( ) => {
708+ render (
709+ < ViewConfigPanel
710+ open = { true }
711+ onClose = { vi . fn ( ) }
712+ activeView = { {
713+ ...mockActiveView ,
714+ filter : [ [ 'stage' , '=' , 'active' ] , [ 'name' , '!=' , 'Test' ] ] ,
715+ } }
716+ objectDef = { mockObjectDef }
717+ />
718+ ) ;
719+
720+ const fb = screen . getByTestId ( 'mock-filter-builder' ) ;
721+ expect ( fb ) . toHaveAttribute ( 'data-condition-count' , '2' ) ;
722+ expect ( screen . getByTestId ( 'filter-condition-0' ) ) . toHaveTextContent ( 'stage equals active' ) ;
723+ expect ( screen . getByTestId ( 'filter-condition-1' ) ) . toHaveTextContent ( 'name notEquals Test' ) ;
724+ } ) ;
725+
726+ it ( 'parses and/or logic prefix: ["or", [...], [...]]' , ( ) => {
727+ render (
728+ < ViewConfigPanel
729+ open = { true }
730+ onClose = { vi . fn ( ) }
731+ activeView = { {
732+ ...mockActiveView ,
733+ filter : [ 'or' , [ 'stage' , '=' , 'active' ] , [ 'stage' , '=' , 'pending' ] ] ,
734+ } }
735+ objectDef = { mockObjectDef }
736+ />
737+ ) ;
738+
739+ const fb = screen . getByTestId ( 'mock-filter-builder' ) ;
740+ expect ( fb ) . toHaveAttribute ( 'data-condition-count' , '2' ) ;
741+ } ) ;
742+
743+ it ( 'normalizes field types for FilterBuilder (currency→number)' , ( ) => {
744+ render (
745+ < ViewConfigPanel
746+ open = { true }
747+ onClose = { vi . fn ( ) }
748+ activeView = { mockActiveView }
749+ objectDef = { {
750+ ...mockObjectDef ,
751+ fields : {
752+ name : { label : 'Name' , type : 'text' } ,
753+ revenue : { label : 'Revenue' , type : 'currency' } ,
754+ created : { label : 'Created' , type : 'datetime' } ,
755+ active : { label : 'Active' , type : 'boolean' } ,
756+ status : { label : 'Status' , type : 'picklist' } ,
757+ } ,
758+ } }
759+ />
760+ ) ;
761+
762+ // The mock FilterBuilder receives normalized fields via data-field-count
763+ const fb = screen . getByTestId ( 'mock-filter-builder' ) ;
764+ expect ( fb ) . toHaveAttribute ( 'data-field-count' , '5' ) ;
765+ } ) ;
554766} ) ;
0 commit comments