@@ -51,9 +51,67 @@ describe('DashboardContainer schema', () => {
5151 } ) . success ,
5252 ) . toBe ( false ) ;
5353 } ) ;
54+
55+ it ( 'validates a tab container with tabs array' , ( ) => {
56+ const result = DashboardContainerSchema . safeParse ( {
57+ id : 'tab-1' ,
58+ type : 'tab' ,
59+ title : 'Overview Tabs' ,
60+ collapsed : false ,
61+ tabs : [
62+ { id : 'tab-a' , title : 'Tab A' } ,
63+ { id : 'tab-b' , title : 'Tab B' } ,
64+ ] ,
65+ activeTabId : 'tab-a' ,
66+ } ) ;
67+ expect ( result . success ) . toBe ( true ) ;
68+ if ( result . success ) {
69+ expect ( result . data . type ) . toBe ( 'tab' ) ;
70+ expect ( result . data . tabs ) . toHaveLength ( 2 ) ;
71+ expect ( result . data . activeTabId ) . toBe ( 'tab-a' ) ;
72+ }
73+ } ) ;
74+
75+ it ( 'validates a group container' , ( ) => {
76+ const result = DashboardContainerSchema . safeParse ( {
77+ id : 'group-1' ,
78+ type : 'group' ,
79+ title : 'Key Metrics' ,
80+ collapsed : false ,
81+ } ) ;
82+ expect ( result . success ) . toBe ( true ) ;
83+ if ( result . success ) {
84+ expect ( result . data . type ) . toBe ( 'group' ) ;
85+ }
86+ } ) ;
87+
88+ it ( 'validates a tab container with activeTabId' , ( ) => {
89+ const result = DashboardContainerSchema . safeParse ( {
90+ id : 'tab-1' ,
91+ type : 'tab' ,
92+ title : 'Tabs' ,
93+ collapsed : false ,
94+ tabs : [ { id : 'sub-tab-1' , title : 'First Tab' } ] ,
95+ activeTabId : 'sub-tab-1' ,
96+ } ) ;
97+ expect ( result . success ) . toBe ( true ) ;
98+ if ( result . success ) {
99+ expect ( result . data . activeTabId ) . toBe ( 'sub-tab-1' ) ;
100+ }
101+ } ) ;
102+
103+ it ( 'rejects an invalid container type' , ( ) => {
104+ const result = DashboardContainerSchema . safeParse ( {
105+ id : 'c-1' ,
106+ type : 'invalid' ,
107+ title : 'Bad Type' ,
108+ collapsed : false ,
109+ } ) ;
110+ expect ( result . success ) . toBe ( false ) ;
111+ } ) ;
54112} ) ;
55113
56- describe ( 'Tile schema with containerId' , ( ) => {
114+ describe ( 'Tile schema with containerId and tabId ' , ( ) => {
57115 const baseTile = {
58116 id : 'tile-1' ,
59117 x : 0 ,
@@ -79,6 +137,7 @@ describe('Tile schema with containerId', () => {
79137 expect ( result . success ) . toBe ( true ) ;
80138 if ( result . success ) {
81139 expect ( result . data . containerId ) . toBeUndefined ( ) ;
140+ expect ( result . data . tabId ) . toBeUndefined ( ) ;
82141 }
83142 } ) ;
84143
@@ -92,6 +151,30 @@ describe('Tile schema with containerId', () => {
92151 expect ( result . data . containerId ) . toBe ( 'section-1' ) ;
93152 }
94153 } ) ;
154+
155+ it ( 'validates a tile with containerId and tabId' , ( ) => {
156+ const result = TileSchema . safeParse ( {
157+ ...baseTile ,
158+ containerId : 'tab-container-1' ,
159+ tabId : 'tab-a' ,
160+ } ) ;
161+ expect ( result . success ) . toBe ( true ) ;
162+ if ( result . success ) {
163+ expect ( result . data . containerId ) . toBe ( 'tab-container-1' ) ;
164+ expect ( result . data . tabId ) . toBe ( 'tab-a' ) ;
165+ }
166+ } ) ;
167+
168+ it ( 'validates a tile with tabId but no containerId' , ( ) => {
169+ const result = TileSchema . safeParse ( {
170+ ...baseTile ,
171+ tabId : 'orphan-tab' ,
172+ } ) ;
173+ expect ( result . success ) . toBe ( true ) ;
174+ if ( result . success ) {
175+ expect ( result . data . tabId ) . toBe ( 'orphan-tab' ) ;
176+ }
177+ } ) ;
95178} ) ;
96179
97180describe ( 'Dashboard schema with sections' , ( ) => {
@@ -193,11 +276,58 @@ describe('Dashboard schema with sections', () => {
193276 expect ( result . data . containers ! [ 0 ] . title ) . toBe ( 'Infrastructure' ) ;
194277 }
195278 } ) ;
279+
280+ it ( 'validates a dashboard with tab container and tiles using tabId' , ( ) => {
281+ const result = DashboardSchema . safeParse ( {
282+ ...baseDashboard ,
283+ tiles : [
284+ {
285+ id : 'tile-1' ,
286+ x : 0 ,
287+ y : 0 ,
288+ w : 8 ,
289+ h : 10 ,
290+ containerId : 'tc1' ,
291+ tabId : 'tab-a' ,
292+ config : {
293+ source : 'source-1' ,
294+ select : [
295+ {
296+ aggFn : 'count' ,
297+ aggCondition : '' ,
298+ valueExpression : '' ,
299+ } ,
300+ ] ,
301+ where : '' ,
302+ from : { databaseName : 'default' , tableName : 'logs' } ,
303+ } ,
304+ } ,
305+ ] ,
306+ containers : [
307+ {
308+ id : 'tc1' ,
309+ type : 'tab' ,
310+ title : 'My Tabs' ,
311+ collapsed : false ,
312+ tabs : [
313+ { id : 'tab-a' , title : 'Tab A' } ,
314+ { id : 'tab-b' , title : 'Tab B' } ,
315+ ] ,
316+ activeTabId : 'tab-a' ,
317+ } ,
318+ ] ,
319+ } ) ;
320+ expect ( result . success ) . toBe ( true ) ;
321+ if ( result . success ) {
322+ expect ( result . data . tiles [ 0 ] . tabId ) . toBe ( 'tab-a' ) ;
323+ expect ( result . data . containers ! [ 0 ] . tabs ) . toHaveLength ( 2 ) ;
324+ }
325+ } ) ;
196326} ) ;
197327
198328describe ( 'section tile grouping logic' , ( ) => {
199329 // Test the grouping logic used in DBDashboardPage
200- type SimpleTile = { id : string ; containerId ?: string } ;
330+ type SimpleTile = { id : string ; containerId ?: string ; tabId ?: string } ;
201331 type SimpleSection = { id : string ; title : string ; collapsed : boolean } ;
202332
203333 function groupTilesBySection ( tiles : SimpleTile [ ] , sections : SimpleSection [ ] ) {
@@ -289,10 +419,29 @@ describe('section tile grouping logic', () => {
289419 expect ( ungrouped . map ( t => t . id ) ) . toEqual ( [ 'b' , 'c' ] ) ;
290420 expect ( bySectionId . get ( 's1' ) ) . toHaveLength ( 1 ) ;
291421 } ) ;
422+
423+ it ( 'filters tab container tiles by tabId' , ( ) => {
424+ const tiles : SimpleTile [ ] = [
425+ { id : 'a' , containerId : 'tc1' , tabId : 'tab-1' } ,
426+ { id : 'b' , containerId : 'tc1' , tabId : 'tab-2' } ,
427+ { id : 'c' , containerId : 'tc1' , tabId : 'tab-1' } ,
428+ ] ;
429+ const sections : SimpleSection [ ] = [
430+ { id : 'tc1' , title : 'Tab Container' , collapsed : false } ,
431+ ] ;
432+ const { bySectionId } = groupTilesBySection ( tiles , sections ) ;
433+ const allTabTiles = bySectionId . get ( 'tc1' ) ?? [ ] ;
434+ expect ( allTabTiles ) . toHaveLength ( 3 ) ;
435+ // Filter by tabId (as done in DBDashboardPage)
436+ const tab1Tiles = allTabTiles . filter ( t => t . tabId === 'tab-1' ) ;
437+ const tab2Tiles = allTabTiles . filter ( t => t . tabId === 'tab-2' ) ;
438+ expect ( tab1Tiles ) . toHaveLength ( 2 ) ;
439+ expect ( tab2Tiles ) . toHaveLength ( 1 ) ;
440+ } ) ;
292441} ) ;
293442
294443describe ( 'section authoring operations' , ( ) => {
295- type SimpleTile = { id : string ; containerId ?: string } ;
444+ type SimpleTile = { id : string ; containerId ?: string ; tabId ?: string } ;
296445 type SimpleSection = { id : string ; title : string ; collapsed : boolean } ;
297446 type SimpleDashboard = {
298447 tiles : SimpleTile [ ] ;
@@ -324,7 +473,9 @@ describe('section authoring operations', () => {
324473 ...dashboard ,
325474 containers : dashboard . containers ?. filter ( s => s . id !== containerId ) ,
326475 tiles : dashboard . tiles . map ( t =>
327- t . containerId === containerId ? { ...t , containerId : undefined } : t ,
476+ t . containerId === containerId
477+ ? { ...t , containerId : undefined , tabId : undefined }
478+ : t ,
328479 ) ,
329480 } ;
330481 }
@@ -345,11 +496,12 @@ describe('section authoring operations', () => {
345496 dashboard : SimpleDashboard ,
346497 tileId : string ,
347498 containerId : string | undefined ,
499+ tabId ?: string ,
348500 ) {
349501 return {
350502 ...dashboard ,
351503 tiles : dashboard . tiles . map ( t =>
352- t . id === tileId ? { ...t , containerId } : t ,
504+ t . id === tileId ? { ...t , containerId, tabId } : t ,
353505 ) ,
354506 } ;
355507 }
@@ -462,6 +614,25 @@ describe('section authoring operations', () => {
462614 expect ( result . tiles . find ( t => t . id === 'd' ) ?. containerId ) . toBeUndefined ( ) ;
463615 } ) ;
464616
617+ it ( 'clears tabId when deleting a tab container' , ( ) => {
618+ const dashboard : SimpleDashboard = {
619+ tiles : [
620+ { id : 'a' , containerId : 'tc1' , tabId : 'tab-1' } ,
621+ { id : 'b' , containerId : 'tc1' , tabId : 'tab-2' } ,
622+ { id : 'c' , containerId : 's1' } ,
623+ ] ,
624+ containers : [
625+ { id : 'tc1' , title : 'Tab Container' , collapsed : false } ,
626+ { id : 's1' , title : 'Section' , collapsed : false } ,
627+ ] ,
628+ } ;
629+ const result = deleteSection ( dashboard , 'tc1' ) ;
630+ expect ( result . tiles . find ( t => t . id === 'a' ) ?. containerId ) . toBeUndefined ( ) ;
631+ expect ( result . tiles . find ( t => t . id === 'a' ) ?. tabId ) . toBeUndefined ( ) ;
632+ expect ( result . tiles . find ( t => t . id === 'b' ) ?. tabId ) . toBeUndefined ( ) ;
633+ expect ( result . tiles . find ( t => t . id === 'c' ) ?. containerId ) . toBe ( 's1' ) ;
634+ } ) ;
635+
465636 it ( 'handles deleting the last section' , ( ) => {
466637 const dashboard : SimpleDashboard = {
467638 tiles : [ { id : 'a' , containerId : 's1' } ] ,
@@ -524,5 +695,129 @@ describe('section authoring operations', () => {
524695 const result = moveTileToSection ( dashboard , 'a' , undefined ) ;
525696 expect ( result . tiles [ 0 ] . containerId ) . toBeUndefined ( ) ;
526697 } ) ;
698+
699+ it ( 'moves a tile to a specific tab' , ( ) => {
700+ const dashboard : SimpleDashboard = {
701+ tiles : [ { id : 'a' } ] ,
702+ containers : [ { id : 'tc1' , title : 'Tab Container' , collapsed : false } ] ,
703+ } ;
704+ const result = moveTileToSection ( dashboard , 'a' , 'tc1' , 'tab-1' ) ;
705+ expect ( result . tiles [ 0 ] . containerId ) . toBe ( 'tc1' ) ;
706+ expect ( result . tiles [ 0 ] . tabId ) . toBe ( 'tab-1' ) ;
707+ } ) ;
708+
709+ it ( 'clears tabId when moving from tab to regular section' , ( ) => {
710+ const dashboard : SimpleDashboard = {
711+ tiles : [ { id : 'a' , containerId : 'tc1' , tabId : 'tab-1' } ] ,
712+ containers : [
713+ { id : 'tc1' , title : 'Tab Container' , collapsed : false } ,
714+ { id : 's1' , title : 'Section' , collapsed : false } ,
715+ ] ,
716+ } ;
717+ const result = moveTileToSection ( dashboard , 'a' , 's1' ) ;
718+ expect ( result . tiles [ 0 ] . containerId ) . toBe ( 's1' ) ;
719+ expect ( result . tiles [ 0 ] . tabId ) . toBeUndefined ( ) ;
720+ } ) ;
721+ } ) ;
722+
723+ describe ( 'reorder sections' , ( ) => {
724+ function reorderSections (
725+ dashboard : SimpleDashboard ,
726+ fromIndex : number ,
727+ toIndex : number ,
728+ ) {
729+ if ( ! dashboard . containers ) return dashboard ;
730+ const containers = [ ...dashboard . containers ] ;
731+ const [ removed ] = containers . splice ( fromIndex , 1 ) ;
732+ containers . splice ( toIndex , 0 , removed ) ;
733+ return { ...dashboard , containers } ;
734+ }
735+
736+ it ( 'moves a section from first to last' , ( ) => {
737+ const dashboard : SimpleDashboard = {
738+ tiles : [ ] ,
739+ containers : [
740+ { id : 's1' , title : 'First' , collapsed : false } ,
741+ { id : 's2' , title : 'Second' , collapsed : false } ,
742+ { id : 's3' , title : 'Third' , collapsed : false } ,
743+ ] ,
744+ } ;
745+ const result = reorderSections ( dashboard , 0 , 2 ) ;
746+ expect ( result . containers ! . map ( c => c . id ) ) . toEqual ( [ 's2' , 's3' , 's1' ] ) ;
747+ } ) ;
748+
749+ it ( 'moves a section from last to first' , ( ) => {
750+ const dashboard : SimpleDashboard = {
751+ tiles : [ ] ,
752+ containers : [
753+ { id : 's1' , title : 'First' , collapsed : false } ,
754+ { id : 's2' , title : 'Second' , collapsed : false } ,
755+ { id : 's3' , title : 'Third' , collapsed : false } ,
756+ ] ,
757+ } ;
758+ const result = reorderSections ( dashboard , 2 , 0 ) ;
759+ expect ( result . containers ! . map ( c => c . id ) ) . toEqual ( [ 's3' , 's1' , 's2' ] ) ;
760+ } ) ;
761+
762+ it ( 'does not affect tiles when sections are reordered' , ( ) => {
763+ const dashboard : SimpleDashboard = {
764+ tiles : [
765+ { id : 'a' , containerId : 's1' } ,
766+ { id : 'b' , containerId : 's2' } ,
767+ ] ,
768+ containers : [
769+ { id : 's1' , title : 'First' , collapsed : false } ,
770+ { id : 's2' , title : 'Second' , collapsed : false } ,
771+ ] ,
772+ } ;
773+ const result = reorderSections ( dashboard , 0 , 1 ) ;
774+ expect ( result . tiles ) . toEqual ( dashboard . tiles ) ;
775+ expect ( result . containers ! . map ( c => c . id ) ) . toEqual ( [ 's2' , 's1' ] ) ;
776+ } ) ;
777+ } ) ;
778+
779+ describe ( 'group selected tiles' , ( ) => {
780+ function groupTilesIntoSection (
781+ dashboard : SimpleDashboard ,
782+ tileIds : string [ ] ,
783+ newSection : SimpleSection ,
784+ ) {
785+ const containers = [ ...( dashboard . containers ?? [ ] ) , newSection ] ;
786+ const tiles = dashboard . tiles . map ( t =>
787+ tileIds . includes ( t . id ) ? { ...t , containerId : newSection . id } : t ,
788+ ) ;
789+ return { ...dashboard , containers, tiles } ;
790+ }
791+
792+ it ( 'groups selected tiles into a new section' , ( ) => {
793+ const dashboard : SimpleDashboard = {
794+ tiles : [ { id : 'a' } , { id : 'b' } , { id : 'c' } ] ,
795+ } ;
796+ const result = groupTilesIntoSection ( dashboard , [ 'a' , 'c' ] , {
797+ id : 'new-s' ,
798+ title : 'New Section' ,
799+ collapsed : false ,
800+ } ) ;
801+ expect ( result . containers ) . toHaveLength ( 1 ) ;
802+ expect ( result . tiles . find ( t => t . id === 'a' ) ?. containerId ) . toBe ( 'new-s' ) ;
803+ expect ( result . tiles . find ( t => t . id === 'b' ) ?. containerId ) . toBeUndefined ( ) ;
804+ expect ( result . tiles . find ( t => t . id === 'c' ) ?. containerId ) . toBe ( 'new-s' ) ;
805+ } ) ;
806+
807+ it ( 'preserves existing sections when grouping' , ( ) => {
808+ const dashboard : SimpleDashboard = {
809+ tiles : [ { id : 'a' , containerId : 's1' } , { id : 'b' } , { id : 'c' } ] ,
810+ containers : [ { id : 's1' , title : 'Existing' , collapsed : false } ] ,
811+ } ;
812+ const result = groupTilesIntoSection ( dashboard , [ 'b' , 'c' ] , {
813+ id : 'new-s' ,
814+ title : 'Grouped' ,
815+ collapsed : false ,
816+ } ) ;
817+ expect ( result . containers ) . toHaveLength ( 2 ) ;
818+ expect ( result . tiles . find ( t => t . id === 'a' ) ?. containerId ) . toBe ( 's1' ) ;
819+ expect ( result . tiles . find ( t => t . id === 'b' ) ?. containerId ) . toBe ( 'new-s' ) ;
820+ expect ( result . tiles . find ( t => t . id === 'c' ) ?. containerId ) . toBe ( 'new-s' ) ;
821+ } ) ;
527822 } ) ;
528823} ) ;
0 commit comments