Compare commits

..

3 Commits

Author SHA1 Message Date
5e46de8b60 Refactor Dashboard Layout and Sidebar Components for Improved Responsiveness
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new DashboardSidebarSplitter component to manage sidebar resizing and collapsing behavior, enhancing user experience on different screen sizes.
- Updated DashboardSidebar to support dynamic collapsed width and improved styling for better visual consistency.
- Refactored DashboardLayout to utilize the new splitter, streamlining the layout structure and improving maintainability.
- Enhanced CSS styles for sidebar components to ensure consistent behavior and appearance across various states.
2026-08-20 21:36:02 +01:00
4ea6d1e7a3 Enhance DashboardSidebar with CollapsedItemIcon and Tooltip integration
- Introduced a new CollapsedItemIcon component to improve the display of sidebar items when collapsed, providing additional context through tooltips.
- Updated the DashboardSidebar to utilize the CollapsedItemIcon for better user experience and visual clarity when navigating collapsed sidebar items.
- Enhanced code readability by organizing the new component and its prop types within the DashboardSidebar file.
2026-08-20 21:05:45 +01:00
d52a8eb570 Refactor ElipsisText component to integrate Tooltip for truncated text
- Updated the ElipsisText component to conditionally wrap truncated text in a Tooltip, enhancing user experience by providing additional context on hover.
- Simplified the rendering logic by consolidating the content structure, improving code readability and maintainability.
2026-08-20 21:03:32 +01:00
5 changed files with 344 additions and 47 deletions

View File

@ -253,8 +253,8 @@ html.macos-vibrancy .dark-mode .ant-layout.main-content-layout .ant-card {
} }
.electron-sider.ant-layout-sider-collapsed { .electron-sider.ant-layout-sider-collapsed {
max-width: 55px !important; min-width: var(--dashboard-sidebar-collapsed-width, 55px) !important;
min-width: 55px !important; max-width: var(--dashboard-sidebar-collapsed-width, 55px) !important;
} }
.loading-modal .ant-modal-footer { .loading-modal .ant-modal-footer {
@ -362,8 +362,10 @@ code {
} }
.cursor-tooltip .ant-card { .cursor-tooltip .ant-card {
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), box-shadow:
0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05); 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
} }
/* --- Start of src/index.css --- */ /* --- Start of src/index.css --- */
@ -412,9 +414,21 @@ body {
/* --- Start of src/components/Dashboard/common/DashboardSidebar.css --- */ /* --- Start of src/components/Dashboard/common/DashboardSidebar.css --- */
.ant-layout-sider { .ant-layout-sider {
width: 250px; /* Adjust the width as per your preference */ height: 100%;
min-width: 250px; /* Ensure a minimum width to avoid collapsing */ }
max-width: 400px; /* Optionally set a maximum width */
.dashboard-sider.ant-layout-sider {
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
.dashboard-sider.ant-layout-sider-collapsed {
width: var(--dashboard-sidebar-collapsed-width, 80px) !important;
min-width: var(--dashboard-sidebar-collapsed-width, 80px) !important;
max-width: var(--dashboard-sidebar-collapsed-width, 80px) !important;
flex: 0 0 var(--dashboard-sidebar-collapsed-width, 80px) !important;
} }
/* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */ /* --- End of src/components/Dashboard/common/DashboardSidebar.css --- */
@ -995,6 +1009,11 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
min-width: 0; min-width: 0;
} }
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning
.ant-splitter-panel {
transition: flex-basis 0.15s ease-in-out;
}
.farmcontrol-splitter .farmcontrol-splitter
.ant-splitter-panel:has(~ .ant-splitter-panel) .ant-splitter-panel:has(~ .ant-splitter-panel)
.info-collapse.ant-collapse.ant-collapse-icon-position-end .info-collapse.ant-collapse.ant-collapse-icon-position-end
@ -1355,7 +1374,8 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
1, 1,
abs( abs(
calc( calc(
var(--permissions-col, 0) - var(--permissions-hovered-col, -1) var(--permissions-col, 0) -
var(--permissions-hovered-col, -1)
) )
) )
) )
@ -1512,3 +1532,19 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
stroke: currentColor; stroke: currentColor;
stroke-width: 8px; stroke-width: 8px;
} }
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar {
margin: 0;
}
.dashboard-splitter.farmcontrol-splitter
> .ant-splitter-bar
.ant-splitter-bar-dragger::before {
background: transparent;
}
.dashboard-splitter.farmcontrol-splitter
> .ant-splitter-bar
.ant-splitter-bar-dragger::after {
content: none;
}

View File

@ -10,6 +10,7 @@ import ManagementSidebar from './Management/ManagementSidebar'
import DashboardNavigation from './common/DashboardNavigation' import DashboardNavigation from './common/DashboardNavigation'
import DashboardBreadcrumb from './common/DashboardBreadcrumb' import DashboardBreadcrumb from './common/DashboardBreadcrumb'
import DeveloperSidebar from './Developer/DeveloperSidebar' import DeveloperSidebar from './Developer/DeveloperSidebar'
import DashboardSidebarSplitter from './common/DashboardSidebarSplitter'
import { useThemeContext } from './context/ThemeContext' import { useThemeContext } from './context/ThemeContext'
import { MessageProvider } from './context/MessageContext' import { MessageProvider } from './context/MessageContext'
@ -26,15 +27,7 @@ const DashboardLayout = ({ children }) => {
const { isDarkMode } = useThemeContext() const { isDarkMode } = useThemeContext()
return ( const sidebar = isProduction ? (
<MessageProvider>
<Layout
style={{ height: 'var(--unit-100vh)' }}
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout`}
>
<DashboardNavigation />
<Layout>
{isProduction ? (
<ProductionSidebar /> <ProductionSidebar />
) : isInventory ? ( ) : isInventory ? (
<InventorySidebar /> <InventorySidebar />
@ -47,20 +40,28 @@ const DashboardLayout = ({ children }) => {
) : isDeveloper ? ( ) : isDeveloper ? (
<DeveloperSidebar /> <DeveloperSidebar />
) : ( ) : (
<ProductionSidebar /> // Default to production sidebar <ProductionSidebar />
)} )
return (
<MessageProvider>
<Layout
style={{ height: 'var(--unit-100vh)' }}
className={`${isDarkMode ? 'dark-mode' : 'light-mode'} main-layout`}
>
<DashboardNavigation />
<DashboardSidebarSplitter sidebar={sidebar}>
<Layout style={{ padding: '24px' }} className='main-content-layout'> <Layout style={{ padding: '24px' }} className='main-content-layout'>
<Content> <Content>
<Flex vertical style={{ height: '100%' }} gap='20px'> <Flex vertical style={{ height: '100%' }} gap='20px'>
<Flex justify='space-between'> <Flex justify='space-between'>
<DashboardBreadcrumb style={{ margin: '16px 0' }} /> <DashboardBreadcrumb style={{ margin: '16px 0' }} />
</Flex> </Flex>
{children} {children}
</Flex> </Flex>
</Content> </Content>
</Layout> </Layout>
</Layout> </DashboardSidebarSplitter>
</Layout> </Layout>
</MessageProvider> </MessageProvider>
) )

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useContext } from 'react' import { cloneElement, isValidElement, useState, useEffect, useContext } from 'react'
import { Layout, Menu, Flex, Button } from 'antd' import { Layout, Menu, Flex, Button } from 'antd'
import { CaretDownFilled } from '@ant-design/icons' import { CaretDownFilled } from '@ant-design/icons'
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon' import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
@ -12,13 +12,35 @@ import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
import SimpleBar from 'simplebar-react' import SimpleBar from 'simplebar-react'
import { useThemeContext } from '../context/ThemeContext' import { useThemeContext } from '../context/ThemeContext'
import { filterSidebarItemsByListPermission } from '../../../database/Sidebars' import { filterSidebarItemsByListPermission } from '../../../database/Sidebars'
import Tooltip from './Tooltip'
const { Sider } = Layout const { Sider } = Layout
const CollapsedItemIcon = ({ className, icon, title }) => {
const iconNode = isValidElement(icon)
? cloneElement(icon, {
className: [icon.props.className, className].filter(Boolean).join(' ')
})
: icon
return (
<Tooltip title={title}>
{iconNode ?? <span className={className} />}
</Tooltip>
)
}
CollapsedItemIcon.propTypes = {
className: PropTypes.string,
icon: PropTypes.node,
title: PropTypes.node
}
const DashboardSidebar = ({ const DashboardSidebar = ({
items = [], items = [],
selectedKey = '', selectedKey = '',
onCollapse, onCollapse,
collapsed: collapsedProp, collapsed: collapsedProp,
collapsedWidth: collapsedWidthProp,
key = 'DashboardSidebar_collapseState' key = 'DashboardSidebar_collapseState'
}) => { }) => {
const [collapsed, setCollapsed] = useState(() => { const [collapsed, setCollapsed] = useState(() => {
@ -30,6 +52,7 @@ const DashboardSidebar = ({
const navigate = useNavigate() const navigate = useNavigate()
const { isDarkMode } = useThemeContext() const { isDarkMode } = useThemeContext()
const { isElectron } = useContext(ElectronContext) const { isElectron } = useContext(ElectronContext)
const collapsedWidth = collapsedWidthProp ?? (isElectron ? 55 : 80)
const { userProfile } = useContext(AuthContext) const { userProfile } = useContext(AuthContext)
const allowedItems = filterSidebarItemsByListPermission(items, userProfile) const allowedItems = filterSidebarItemsByListPermission(items, userProfile)
@ -52,9 +75,15 @@ const DashboardSidebar = ({
return item return item
} }
const icon = item.icon || getSidebarIconNode(item.iconKey)
const mappedItem = { const mappedItem = {
key: item.key, key: item.key,
icon: item.icon || getSidebarIconNode(item.iconKey), icon:
collapsed && !isMobile ? (
<CollapsedItemIcon title={item.label} icon={icon} />
) : (
icon
),
label: item.label label: item.label
} }
@ -90,10 +119,14 @@ const DashboardSidebar = ({
return ( return (
<Sider <Sider
width={250} width='100%'
collapsedWidth={collapsedWidth}
theme='light' theme='light'
collapsed={collapsed} collapsed={collapsed}
className={isElectron ? 'electron-sider' : ''} className={`dashboard-sider${isElectron ? ' electron-sider' : ''}`}
style={{
'--dashboard-sidebar-collapsed-width': `${collapsedWidth}px`
}}
> >
<Flex <Flex
style={{ height: '100%', minHeight: 0 }} style={{ height: '100%', minHeight: 0 }}
@ -132,6 +165,7 @@ DashboardSidebar.propTypes = {
selectedKey: PropTypes.string, selectedKey: PropTypes.string,
onCollapse: PropTypes.func, onCollapse: PropTypes.func,
collapsed: PropTypes.bool, collapsed: PropTypes.bool,
collapsedWidth: PropTypes.number,
key: PropTypes.string key: PropTypes.string
} }

View File

@ -0,0 +1,222 @@
import {
cloneElement,
isValidElement,
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import { Layout, Splitter } from 'antd'
import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext'
const COLLAPSE_BELOW_PX = 150
const DEFAULT_SIDEBAR_PX = 300
const MAX_SIDEBAR_PX = 400
const COLLAPSED_WIDTH_PX = 80
const ELECTRON_COLLAPSED_WIDTH_PX = 55
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
const readStoredBoolean = (key, fallback) => {
try {
const saved = sessionStorage.getItem(key)
return saved ? JSON.parse(saved) : fallback
} catch {
return fallback
}
}
const readStoredNumber = (key, fallback) => {
try {
const saved = sessionStorage.getItem(key)
const value = saved ? JSON.parse(saved) : fallback
return typeof value === 'number' && !Number.isNaN(value) ? value : fallback
} catch {
return fallback
}
}
const clampExpandedSize = (size) =>
Math.min(MAX_SIDEBAR_PX, Math.max(size, COLLAPSE_BELOW_PX))
const DashboardSidebarSplitter = ({ sidebar, children }) => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const { isElectron } = useContext(ElectronContext)
const collapsedWidth = isElectron
? ELECTRON_COLLAPSED_WIDTH_PX
: COLLAPSED_WIDTH_PX
const [collapsed, setCollapsed] = useState(() =>
readStoredBoolean(COLLAPSE_STORAGE_KEY, false)
)
const [sidebarSize, setSidebarSize] = useState(() => {
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
return isElectron ? ELECTRON_COLLAPSED_WIDTH_PX : COLLAPSED_WIDTH_PX
}
return clampExpandedSize(
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
)
})
const collapsedRef = useRef(collapsed)
collapsedRef.current = collapsed
const lastExpandedSizeRef = useRef(
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX))
)
const [sizeTransitioning, setSizeTransitioning] = useState(false)
const sizeTransitionTimeoutRef = useRef(null)
const startSizeTransition = useCallback(() => {
setSizeTransitioning(true)
if (sizeTransitionTimeoutRef.current) {
clearTimeout(sizeTransitionTimeoutRef.current)
}
sizeTransitionTimeoutRef.current = setTimeout(() => {
setSizeTransitioning(false)
sizeTransitionTimeoutRef.current = null
}, 150)
}, [])
useEffect(
() => () => {
if (sizeTransitionTimeoutRef.current) {
clearTimeout(sizeTransitionTimeoutRef.current)
}
},
[]
)
const persistCollapsed = (nextCollapsed) => {
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
}
const persistSize = (size) => {
sessionStorage.setItem(SIZE_STORAGE_KEY, JSON.stringify(size))
}
const collapseSidebar = useCallback(
(expandedSize) => {
if (expandedSize >= COLLAPSE_BELOW_PX) {
lastExpandedSizeRef.current = expandedSize
persistSize(expandedSize)
}
collapsedRef.current = true
setCollapsed(true)
persistCollapsed(true)
startSizeTransition()
setSidebarSize(collapsedWidth)
},
[collapsedWidth, startSizeTransition]
)
const expandSidebar = useCallback(() => {
collapsedRef.current = false
setCollapsed(false)
persistCollapsed(false)
startSizeTransition()
setSidebarSize(DEFAULT_SIDEBAR_PX)
persistSize(DEFAULT_SIDEBAR_PX)
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX
}, [startSizeTransition])
const handleSidebarCollapse = useCallback(
(nextCollapsed) => {
if (nextCollapsed) {
collapseSidebar(sidebarSize)
} else {
expandSidebar()
}
},
[collapseSidebar, expandSidebar, sidebarSize]
)
const handleResize = useCallback(
(sizes) => {
if (collapsedRef.current) {
setSidebarSize(collapsedWidth)
return
}
const size = sizes[0]
if (size < COLLAPSE_BELOW_PX) {
collapseSidebar(lastExpandedSizeRef.current)
return
}
setSidebarSize(size)
lastExpandedSizeRef.current = clampExpandedSize(size)
},
[collapseSidebar, collapsedWidth]
)
const handleResizeEnd = useCallback(
(sizes) => {
if (collapsedRef.current) {
setSidebarSize(collapsedWidth)
return
}
const next = clampExpandedSize(sizes[0])
lastExpandedSizeRef.current = next
setSidebarSize(next)
persistSize(next)
},
[collapsedWidth]
)
const sidebarNode = isValidElement(sidebar)
? cloneElement(sidebar, {
collapsed,
collapsedWidth,
onCollapse: handleSidebarCollapse
})
: sidebar
if (isMobile) {
return (
<Layout>
{sidebarNode}
{children}
</Layout>
)
}
return (
<Layout style={{ height: '100%', minHeight: 0 }}>
<Splitter
className={`farmcontrol-splitter dashboard-splitter${
sizeTransitioning ? ' is-size-transitioning' : ''
}`}
style={{ height: '100%' }}
onResize={handleResize}
onResizeEnd={handleResizeEnd}
>
<Splitter.Panel
size={sidebarSize}
min={collapsedWidth}
max={collapsed ? collapsedWidth : MAX_SIDEBAR_PX}
resizable={!collapsed}
style={{
overflow: 'hidden',
minWidth: collapsedWidth,
maxWidth:
collapsed && !sizeTransitioning ? collapsedWidth : undefined
}}
>
{sidebarNode}
</Splitter.Panel>
<Splitter.Panel min={320}>{children}</Splitter.Panel>
</Splitter>
</Layout>
)
}
DashboardSidebarSplitter.propTypes = {
sidebar: PropTypes.node.isRequired,
children: PropTypes.node.isRequired
}
export default DashboardSidebarSplitter

View File

@ -1,6 +1,7 @@
import { useLayoutEffect, useRef, useState } from 'react' import { useLayoutEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Typography } from 'antd' import { Typography } from 'antd'
import Tooltip from './Tooltip'
const { Text } = Typography const { Text } = Typography
@ -70,13 +71,7 @@ const ElipsisText = ({ children, style, className, title, code, ...rest }) => {
const showTruncated = truncated && Boolean(end) const showTruncated = truncated && Boolean(end)
const RootTag = code ? 'code' : 'span' const RootTag = code ? 'code' : 'span'
return ( const content = (
<Text
{...rest}
className={['elipsis-text-wrapper', className].filter(Boolean).join(' ')}
title={title ?? text}
style={style}
>
<span <span
className={showTruncated ? 'elipsis-text is-truncated' : 'elipsis-text'} className={showTruncated ? 'elipsis-text is-truncated' : 'elipsis-text'}
ref={containerRef} ref={containerRef}
@ -93,6 +88,15 @@ const ElipsisText = ({ children, style, className, title, code, ...rest }) => {
</span> </span>
</RootTag> </RootTag>
</span> </span>
)
return (
<Text
{...rest}
className={['elipsis-text-wrapper', className].filter(Boolean).join(' ')}
style={style}
>
{truncated ? <Tooltip title={title ?? text}>{content}</Tooltip> : content}
</Text> </Text>
) )
} }