@@ -61,6 +61,7 @@ import { notifications } from '@mantine/notifications';
6161import {
6262 IconArrowsMaximize ,
6363 IconBell ,
64+ IconBoxMultiple ,
6465 IconChartBar ,
6566 IconCopy ,
6667 IconDeviceFloppy ,
@@ -1164,7 +1165,14 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
11641165 } ) ;
11651166 } ;
11661167
1168+ // Top-level containers: exclude child tabs (those with parentId)
11671169 const sections = useMemo (
1170+ ( ) => ( dashboard ?. containers ?? [ ] ) . filter ( c => ! c . parentId ) ,
1171+ [ dashboard ?. containers ] ,
1172+ ) ;
1173+
1174+ // All containers (including child tabs) for lookups
1175+ const allContainers = useMemo (
11681176 ( ) => dashboard ?. containers ?? [ ] ,
11691177 [ dashboard ?. containers ] ,
11701178 ) ;
@@ -1406,12 +1414,27 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
14061414 setDashboard (
14071415 produce ( dashboard , draft => {
14081416 if ( ! draft . containers ) draft . containers = [ ] ;
1417+ const containerId = makeId ( ) ;
14091418 draft . containers . push ( {
1410- id : makeId ( ) ,
1419+ id : containerId ,
14111420 type,
14121421 title : titles [ type ] ,
14131422 collapsed : false ,
14141423 } ) ;
1424+ // Tab containers get an initial child tab
1425+ if ( type === 'tab' ) {
1426+ const firstTabId = makeId ( ) ;
1427+ draft . containers . push ( {
1428+ id : firstTabId ,
1429+ type : 'tab' ,
1430+ title : 'Tab 1' ,
1431+ collapsed : false ,
1432+ parentId : containerId ,
1433+ } ) ;
1434+ // Set the initial active tab
1435+ const parent = draft . containers . find ( c => c . id === containerId ) ;
1436+ if ( parent ) parent . activeTabId = firstTabId ;
1437+ }
14151438 } ) ,
14161439 ) ;
14171440 } ,
@@ -1463,23 +1486,31 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
14631486
14641487 setDashboard (
14651488 produce ( dashboard , draft => {
1466- const sectionIds = new Set ( draft . containers ?. map ( c => c . id ) ?? [ ] ) ;
1489+ // Collect IDs to delete: the container + any child tabs
1490+ const childIds = new Set (
1491+ ( draft . containers ?? [ ] )
1492+ . filter ( c => c . parentId === containerId )
1493+ . map ( c => c . id ) ,
1494+ ) ;
1495+ const idsToDelete = new Set ( [ containerId , ...childIds ] ) ;
1496+
1497+ const allSectionIds = new Set ( draft . containers ?. map ( c => c . id ) ?? [ ] ) ;
14671498 let maxUngroupedY = 0 ;
14681499 for ( const tile of draft . tiles ) {
1469- if ( ! tile . containerId || ! sectionIds . has ( tile . containerId ) ) {
1500+ if ( ! tile . containerId || ! allSectionIds . has ( tile . containerId ) ) {
14701501 maxUngroupedY = Math . max ( maxUngroupedY , tile . y + tile . h ) ;
14711502 }
14721503 }
14731504
14741505 for ( const tile of draft . tiles ) {
1475- if ( tile . containerId === containerId ) {
1506+ if ( tile . containerId && idsToDelete . has ( tile . containerId ) ) {
14761507 tile . y += maxUngroupedY ;
14771508 delete tile . containerId ;
14781509 }
14791510 }
14801511
14811512 draft . containers = draft . containers ?. filter (
1482- s => s . id !== containerId ,
1513+ s => ! idsToDelete . has ( s . id ) ,
14831514 ) ;
14841515 } ) ,
14851516 ) ;
@@ -1501,18 +1532,106 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
15011532 [ dashboard , setDashboard ] ,
15021533 ) ;
15031534
1504- // Group tiles by section; orphaned tiles (containerId not matching any
1505- // section) fall back to ungrouped to avoid silently hiding them.
1535+ // --- Tab management ---
1536+
1537+ const handleAddTab = useCallback (
1538+ ( parentId : string ) => {
1539+ if ( ! dashboard ) return ;
1540+ const siblings =
1541+ dashboard . containers ?. filter ( c => c . parentId === parentId ) ?? [ ] ;
1542+ const newTabId = makeId ( ) ;
1543+ setDashboard (
1544+ produce ( dashboard , draft => {
1545+ if ( ! draft . containers ) draft . containers = [ ] ;
1546+ draft . containers . push ( {
1547+ id : newTabId ,
1548+ type : 'tab' ,
1549+ title : `Tab ${ siblings . length + 1 } ` ,
1550+ collapsed : false ,
1551+ parentId,
1552+ } ) ;
1553+ // Auto-switch to the new tab
1554+ const parent = draft . containers . find ( c => c . id === parentId ) ;
1555+ if ( parent ) parent . activeTabId = newTabId ;
1556+ } ) ,
1557+ ) ;
1558+ } ,
1559+ [ dashboard , setDashboard ] ,
1560+ ) ;
1561+
1562+ const handleRenameTab = useCallback (
1563+ ( tabId : string , newTitle : string ) => {
1564+ if ( ! dashboard || ! newTitle . trim ( ) ) return ;
1565+ setDashboard (
1566+ produce ( dashboard , draft => {
1567+ const tab = draft . containers ?. find ( c => c . id === tabId ) ;
1568+ if ( tab ) tab . title = newTitle . trim ( ) ;
1569+ } ) ,
1570+ ) ;
1571+ } ,
1572+ [ dashboard , setDashboard ] ,
1573+ ) ;
1574+
1575+ const handleDeleteTab = useCallback (
1576+ ( tabId : string ) => {
1577+ if ( ! dashboard ) return ;
1578+ const tab = dashboard . containers ?. find ( c => c . id === tabId ) ;
1579+ if ( ! tab ?. parentId ) return ;
1580+ const parentId = tab . parentId ;
1581+ const siblings =
1582+ dashboard . containers ?. filter (
1583+ c => c . parentId === parentId && c . id !== tabId ,
1584+ ) ?? [ ] ;
1585+ // Don't delete the last tab
1586+ if ( siblings . length === 0 ) return ;
1587+
1588+ setDashboard (
1589+ produce ( dashboard , draft => {
1590+ // Move tiles from deleted tab to first remaining sibling
1591+ const targetId = siblings [ 0 ] . id ;
1592+ for ( const tile of draft . tiles ) {
1593+ if ( tile . containerId === tabId ) {
1594+ tile . containerId = targetId ;
1595+ }
1596+ }
1597+ // Remove the tab
1598+ draft . containers = draft . containers ?. filter ( c => c . id !== tabId ) ;
1599+ // Update parent's activeTabId if it pointed to deleted tab
1600+ const parent = draft . containers ?. find ( c => c . id === parentId ) ;
1601+ if ( parent ?. activeTabId === tabId ) {
1602+ parent . activeTabId = targetId ;
1603+ }
1604+ } ) ,
1605+ ) ;
1606+ } ,
1607+ [ dashboard , setDashboard ] ,
1608+ ) ;
1609+
1610+ const handleTabChange = useCallback (
1611+ ( parentId : string , tabId : string ) => {
1612+ if ( ! dashboard ) return ;
1613+ setDashboard (
1614+ produce ( dashboard , draft => {
1615+ const parent = draft . containers ?. find ( c => c . id === parentId ) ;
1616+ if ( parent ) parent . activeTabId = tabId ;
1617+ } ) ,
1618+ ) ;
1619+ } ,
1620+ [ dashboard , setDashboard ] ,
1621+ ) ;
1622+
1623+ // Group tiles by container (including child tabs).
1624+ // Orphaned tiles (containerId not matching any container) become ungrouped.
15061625 const tilesByContainerId = useMemo ( ( ) => {
15071626 const map = new Map < string , Tile [ ] > ( ) ;
1508- for ( const section of sections ) {
1627+ for ( const c of allContainers ) {
15091628 map . set (
1510- section . id ,
1511- allTiles . filter ( t => t . containerId === section . id ) ,
1629+ c . id ,
1630+ allTiles . filter ( t => t . containerId === c . id ) ,
15121631 ) ;
15131632 }
15141633 return map ;
1515- } , [ sections , allTiles ] ) ;
1634+ } , [ allContainers , allTiles ] ) ;
15161635
15171636 const ungroupedTiles = useMemo (
15181637 ( ) =>
@@ -1524,6 +1643,19 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
15241643 [ hasSections , allTiles , tilesByContainerId ] ,
15251644 ) ;
15261645
1646+ // Child tabs grouped by parent ID
1647+ const tabsByParentId = useMemo ( ( ) => {
1648+ const map = new Map < string , DashboardContainer [ ] > ( ) ;
1649+ for ( const c of allContainers ) {
1650+ if ( c . parentId ) {
1651+ const list = map . get ( c . parentId ) ?? [ ] ;
1652+ list . push ( c ) ;
1653+ map . set ( c . parentId , list ) ;
1654+ }
1655+ }
1656+ return map ;
1657+ } , [ allContainers ] ) ;
1658+
15271659 const onUngroupedLayoutChange = useMemo (
15281660 ( ) => makeOnLayoutChange ( ungroupedTiles ) ,
15291661 [ makeOnLayoutChange , ungroupedTiles ] ,
@@ -1968,6 +2100,10 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
19682100 }
19692101
19702102 if ( container . type === 'tab' ) {
2103+ const childTabs = tabsByParentId . get ( container . id ) ?? [ ] ;
2104+ const activeTabId =
2105+ container . activeTabId ?? childTabs [ 0 ] ?. id ;
2106+
19712107 return (
19722108 < SortableSectionWrapper
19732109 key = { container . id }
@@ -1977,41 +2113,49 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
19772113 { ( dragHandleProps : Record < string , unknown > ) => (
19782114 < TabContainer
19792115 container = { container }
1980- tabs = { [
1981- {
1982- id : container . id ,
1983- title : container . title ,
1984- } ,
1985- ] }
1986- activeTabId = { container . id }
1987- onTabChange = { ( ) => { } }
2116+ tabs = { childTabs . map ( t => ( {
2117+ id : t . id ,
2118+ title : t . title ,
2119+ } ) ) }
2120+ activeTabId = { activeTabId }
2121+ onTabChange = { tabId =>
2122+ handleTabChange ( container . id , tabId )
2123+ }
2124+ onAddTab = { ( ) => handleAddTab ( container . id ) }
2125+ onRenameTab = { handleRenameTab }
2126+ onDeleteTab = { handleDeleteTab }
19882127 onRename = { newTitle =>
19892128 handleRenameSection ( container . id , newTitle )
19902129 }
19912130 onDelete = { ( ) => handleDeleteSection ( container . id ) }
1992- onAddTile = { ( ) => onAddTile ( container . id ) }
19932131 dragHandleProps = { dragHandleProps }
19942132 >
1995- < SectionDropZone
1996- sectionId = { container . id }
1997- isEmpty = { isEmpty }
1998- >
1999- { containerTiles . length > 0 && (
2000- < ReactGridLayout
2001- layout = { containerTiles . map (
2002- tileToLayoutItem ,
2003- ) }
2004- containerPadding = { [ 0 , 0 ] }
2005- onLayoutChange = { sectionLayoutChangeHandlers . get (
2006- container . id ,
2007- ) }
2008- cols = { 24 }
2009- rowHeight = { 32 }
2133+ { ( currentTabId : string | undefined ) => {
2134+ if ( ! currentTabId ) return null ;
2135+ const tabTiles =
2136+ tilesByContainerId . get ( currentTabId ) ?? [ ] ;
2137+ const tabIsEmpty = tabTiles . length === 0 ;
2138+ return (
2139+ < SectionDropZone
2140+ sectionId = { currentTabId }
2141+ isEmpty = { tabIsEmpty }
20102142 >
2011- { containerTiles . map ( renderTileComponent ) }
2012- </ ReactGridLayout >
2013- ) }
2014- </ SectionDropZone >
2143+ { tabTiles . length > 0 && (
2144+ < ReactGridLayout
2145+ layout = { tabTiles . map ( tileToLayoutItem ) }
2146+ containerPadding = { [ 0 , 0 ] }
2147+ onLayoutChange = { sectionLayoutChangeHandlers . get (
2148+ currentTabId ,
2149+ ) }
2150+ cols = { 24 }
2151+ rowHeight = { 32 }
2152+ >
2153+ { tabTiles . map ( renderTileComponent ) }
2154+ </ ReactGridLayout >
2155+ ) }
2156+ </ SectionDropZone >
2157+ ) ;
2158+ } }
20152159 </ TabContainer >
20162160 ) }
20172161 </ SortableSectionWrapper >
@@ -2110,7 +2254,13 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
21102254 >
21112255 New Section
21122256 </ Menu . Item >
2113- { /* Tab container hidden until multi-tab support is implemented */ }
2257+ < Menu . Item
2258+ data-testid = "add-new-tab-menu-item"
2259+ leftSection = { < IconBoxMultiple size = { 16 } /> }
2260+ onClick = { ( ) => handleAddContainer ( 'tab' ) }
2261+ >
2262+ New Tab Container
2263+ </ Menu . Item >
21142264 < Menu . Item
21152265 data-testid = "add-new-group-menu-item"
21162266 leftSection = { < IconSquaresDiagonal size = { 16 } /> }
0 commit comments