From 2fa5c288d21331dcf762b50c0f9be68bc919034e Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Tue, 15 Sep 2026 03:26:21 +0100 Subject: [PATCH] Implement Mobile Footer Navigation and Enhance Dashboard Layout - Added MobileFooterNavigation component for improved navigation on mobile devices, featuring segmented navigation for key sections. - Updated DashboardLayout to conditionally render MobileFooterNavigation based on screen size. - Enhanced DashboardNavigation to dynamically adjust menu items based on mobile view, improving user experience. - Refactored CSS styles for mobile footer navigation to ensure proper layout and responsiveness. - Adjusted ObjectTable component to account for new mobile layout, ensuring consistent display across devices. --- assets/stylesheets/App.css | 93 +++++++++++ src/components/Dashboard/Layout.jsx | 6 +- .../Dashboard/common/DashboardNavigation.jsx | 153 ++++++++++-------- .../Dashboard/common/DashboardSidebar.jsx | 14 +- .../common/DashboardSidebarSplitter.jsx | 3 +- .../common/MobileFooterNavigation.jsx | 98 +++++++++++ .../Dashboard/common/ObjectTable.jsx | 2 +- .../Dashboard/context/ThemeContext.jsx | 12 ++ 8 files changed, 296 insertions(+), 85 deletions(-) create mode 100644 src/components/Dashboard/common/MobileFooterNavigation.jsx diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index a46c1306..833e47f3 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -3356,3 +3356,96 @@ body.objectKanbanColumnResizing * { .ant-steps-item-content { min-height: 0; } + +.mobile-footer-navigation.ant-layout-footer { + flex: 0 0 auto; + width: 100%; + line-height: normal; + padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px)); + background: var(--color-mobile-footer-bg); + color: var(--color-text); + border-top: 1px solid var(--color-mobile-footer-item-border); +} + +.mobile-footer-navigation-tabs.segmented-nav { + display: flex; + width: 100%; +} + +.mobile-footer-navigation .segmented-nav-group { + width: 100%; +} + +.mobile-footer-navigation .segmented-nav-item { + flex: 1 1 0; + min-width: 0; + flex-direction: column; + gap: 10px; + padding: 12px; + border: none; + color: var(--color-text); + transition: + background 0.2s ease, + color 0.2s ease; +} + +.mobile-footer-navigation .segmented-nav-item-icon { + padding-inline: 0; + min-width: 0; +} + +.mobile-footer-navigation .segmented-nav-item-icon-contents { + min-height: 24px; + line-height: 24px; +} + +.mobile-footer-navigation .segmented-nav-item-icon-contents .anticon { + font-size: 24px; +} + +.mobile-footer-navigation .segmented-nav-thumb { + background: var(--color-mobile-footer-item-bg); + border: none; +} + +.mobile-footer-navigation .segmented-nav-item-selected, +.mobile-footer-navigation + .segmented-nav-item:not(.segmented-nav-item-disabled):hover { + background: var(--color-mobile-footer-item-bg); + border: none; +} + +.mobile-footer-navigation .segmented-nav-item-selected { + color: var(--color-primary); +} + +.mobile-footer-navigation .segmented-nav-item-selected .anticon, +.mobile-footer-navigation .segmented-nav-item-selected svg { + color: inherit; +} + +.mobile-footer-navigation .segmented-nav-item-label { + min-height: 0; + line-height: 1.2; + padding-inline: 0; + font-size: 11px; + font-weight: 500; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.main-layout.is-mobile { + overflow: hidden; +} + +.mobile-menu.ant-menu-horizontal { + max-width: 32px; + margin-left: 4px; + margin-right: 12px; +} + +.mobile-menu.ant-menu-horizontal .ant-menu-submenu { + padding: 0; +} diff --git a/src/components/Dashboard/Layout.jsx b/src/components/Dashboard/Layout.jsx index adf0353d..4516ab36 100644 --- a/src/components/Dashboard/Layout.jsx +++ b/src/components/Dashboard/Layout.jsx @@ -11,14 +11,17 @@ import DashboardNavigation from './common/DashboardNavigation' import DashboardBreadcrumb from './common/DashboardBreadcrumb' import DeveloperSidebar from './Developer/DeveloperSidebar' import DashboardSidebarSplitter from './common/DashboardSidebarSplitter' +import MobileFooterNavigation from './common/MobileFooterNavigation' import { useThemeContext } from './context/ThemeContext' import { MessageProvider } from './context/MessageContext' import { useDashboardObjectToolsContext } from './context/DashboardObjectToolsContext' +import { useMediaQuery } from 'react-responsive' const { Content } = Layout const DashboardLayout = ({ children }) => { const location = useLocation() + const isMobile = useMediaQuery({ maxWidth: 768 }) const isProduction = location.pathname.startsWith('/dashboard/production') const isInventory = location.pathname.startsWith('/dashboard/inventory') const isFinance = location.pathname.startsWith('/dashboard/finance') @@ -49,7 +52,7 @@ const DashboardLayout = ({ children }) => { @@ -68,6 +71,7 @@ const DashboardLayout = ({ children }) => { + {isMobile ? : null} ) diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index 5468ca07..a18dc49f 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -22,7 +22,6 @@ import { Header } from 'antd/es/layout/layout' import { useMediaQuery } from 'react-responsive' import KeyboardShortcut from './KeyboardShortcut' import UserProfilePopover from './UserProfilePopover' -import ElipsisText from './ElipsisText' import FarmControlLogo from '../../Logos/FarmControlLogo' import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall' @@ -42,10 +41,14 @@ import DashboardWindowButtons from './DashboardWindowButtons' import WindowAppMenu from './WindowAppMenu' import WebAppSwitcher from './WebAppSwitcher' import { + filterSidebarItemsByListPermission, getSidebarDefaultPath, + getSidebarItems, getSidebarMenuSections, + getSidebarSelectedKey, isSidebarSectionVisible } from '../../../database/Sidebars' +import { getSidebarIconNode } from '../../Icons/sidebarIconMap' import { useAppUpdateContext } from '../context/AppUpdateContext' import { useThemeContext } from '../context/ThemeContext' @@ -53,6 +56,29 @@ import { getEffectiveAppearance } from '../../../database/Settings' const { Text } = Typography +const mapSidebarItemsToMenu = (items, navigate) => + items.map((item) => { + if (item?.type === 'divider') { + return item + } + + const mappedItem = { + key: item.key, + icon: item.icon || getSidebarIconNode(item.iconKey), + label: item.label + } + + if (item.path) { + mappedItem.onClick = () => navigate(item.path) + } + + if (item?.children && Array.isArray(item.children)) { + mappedItem.children = mapSidebarItemsToMenu(item.children, navigate) + } + + return mappedItem + }) + const DashboardNavigation = () => { const { userProfile } = useContext(AuthContext) const { showSpotlight } = useContext(SpotlightContext) @@ -71,11 +97,6 @@ const DashboardNavigation = () => { const navigate = useNavigate() const location = useLocation() const [selectedKey, setSelectedKey] = useState('production') - const [selectedMenuItem, setSelectedMenuItem] = useState({ - key: 'production', - label: 'Production', - icon: - }) const isMobile = useMediaQuery({ maxWidth: 768 }) const { platform, @@ -116,6 +137,7 @@ const DashboardNavigation = () => { ]) const includeDev = import.meta.env.DEV + const includeDevItems = import.meta.env.MODE === 'development' const mainMenuItems = useMemo(() => { const iconStyles = { fontSize: !showNavigationLabels ? '16px' : undefined, @@ -135,7 +157,10 @@ const DashboardNavigation = () => { key: 'production', label: 'Production', className: 'electrobun-webkit-app-region-no-drag', - icon: navigationIcon(, 'Production') + icon: navigationIcon( + , + 'Production' + ) }, { key: 'inventory', @@ -166,6 +191,29 @@ const DashboardNavigation = () => { ) }, [includeDev, showNavigationLabels, userProfile]) + const sidebarMenuItems = useMemo(() => { + const items = filterSidebarItemsByListPermission( + getSidebarItems(selectedKey, { includeDev: includeDevItems }), + userProfile + ) + return mapSidebarItemsToMenu(items, navigate) + }, [includeDevItems, navigate, selectedKey, userProfile]) + + const selectedSidebarItemKey = useMemo( + () => + getSidebarSelectedKey(selectedKey, location.pathname, { + includeDev: includeDevItems, + userProfile + }), + [includeDevItems, location.pathname, selectedKey, userProfile] + ) + + const headerMenuItems = isMobile + ? sidebarMenuItems + : showNavigationLabels + ? mainMenuItems + : mainMenuItems.map((item) => ({ ...item, label: undefined })) + const [userPopoverOpen, setUserPopoverOpen] = useState(false) const userPopoverContent = ( @@ -174,15 +222,10 @@ const DashboardNavigation = () => { useEffect(() => { const pathParts = location.pathname.split('/').filter(Boolean) - if (pathParts.length > 2) { - const nextItem = - mainMenuItems.find((item) => item.key == pathParts[1]) || - mainMenuItems[0] - if (nextItem) { - setSelectedMenuItem(nextItem) - setSelectedKey(nextItem.key) - } - } + const sectionKey = pathParts[1] + if (!sectionKey) return + const nextItem = mainMenuItems.find((item) => item.key == sectionKey) + setSelectedKey(nextItem?.key || sectionKey) }, [location.pathname, mainMenuItems]) useEffect(() => { @@ -213,6 +256,29 @@ const DashboardNavigation = () => { const showControls = !isElectron || authenticated + const menu = ( + } + style={{ marginBottom: '4px' }} + /> + } + /> + ) + const navigationContents = ( {isMacOSApp ? : null} @@ -225,6 +291,7 @@ const DashboardNavigation = () => { /> ) : null} + {showControls && isMobile && menu} {showDesktopLogo == true ? ( { }} /> ) : null} - {isMobile && ( - - {selectedMenuItem?.icon} - - {selectedMenuItem?.label} - - - )}
- {showControls && ( - ({ ...item, label: undefined })) - } - style={{ - flexWrap: 'wrap', - flexGrow: isMobile ? 0 : 1, - border: 0, - width: isMobile ? '64px' : 'unset' - }} - onClick={handleMainMenuClick} - selectedKeys={[selectedKey]} - overflowedIndicator={ -
- {isMobile &&
}
{ align='center' className='ant-menu-light' style={{ - padding: '0 26px', + padding: isMobile ? '0 12px' : '0 26px', height: '100%', borderBottom: '1px solid rgba(5, 5, 5, 0.00)' }} diff --git a/src/components/Dashboard/common/DashboardSidebar.jsx b/src/components/Dashboard/common/DashboardSidebar.jsx index 46ed81a5..5d564afe 100644 --- a/src/components/Dashboard/common/DashboardSidebar.jsx +++ b/src/components/Dashboard/common/DashboardSidebar.jsx @@ -6,7 +6,6 @@ import { useContext } from 'react' import { Layout, Menu, Flex, Button, Divider } from 'antd' -import { CaretDownFilled } from '@ant-design/icons' import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon' import ExpandSidebarIcon from '../../Icons/ExpandSidebarIcon' import { useMediaQuery } from 'react-responsive' @@ -82,8 +81,6 @@ const DashboardSidebar = ({ return item } - console.log(item) - const icon = item.icon || getSidebarIconNode(item.iconKey) const mappedItem = { key: item.key, @@ -114,16 +111,7 @@ const DashboardSidebar = ({ const _items = mapItemsRecursively(allowedItems) if (isMobile) { - return ( - } />} - /> - ) + return null } return ( diff --git a/src/components/Dashboard/common/DashboardSidebarSplitter.jsx b/src/components/Dashboard/common/DashboardSidebarSplitter.jsx index 8dae4aa6..0892e133 100644 --- a/src/components/Dashboard/common/DashboardSidebarSplitter.jsx +++ b/src/components/Dashboard/common/DashboardSidebarSplitter.jsx @@ -158,8 +158,7 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => { if (isMobile) { return ( - - {sidebarNode} + {children} ) diff --git a/src/components/Dashboard/common/MobileFooterNavigation.jsx b/src/components/Dashboard/common/MobileFooterNavigation.jsx new file mode 100644 index 00000000..6c995fc3 --- /dev/null +++ b/src/components/Dashboard/common/MobileFooterNavigation.jsx @@ -0,0 +1,98 @@ +import { useContext, useMemo } from 'react' +import { Layout } from 'antd' +import { useMediaQuery } from 'react-responsive' +import { useLocation, useNavigate } from 'react-router-dom' +import { AuthContext } from '../context/AuthContext' +import { ElectronContext } from '../context/ElectronContext' +import { + getSidebarDefaultPath, + isSidebarSectionVisible +} from '../../../database/Sidebars' +import { getSidebarIconNode } from '../../Icons/sidebarIconMap' +import SegmentedNav from './SegmentedNav' + +const { Footer } = Layout + +const MobileFooterNavigation = () => { + const isMobile = useMediaQuery({ maxWidth: 768 }) + const { userProfile, authenticated } = useContext(AuthContext) + const { isElectron } = useContext(ElectronContext) + const navigate = useNavigate() + const location = useLocation() + const includeDev = import.meta.env.DEV + + const sectionItems = useMemo( + () => + [ + { + key: 'production', + label: 'Production', + iconKey: 'production' + }, + { + key: 'inventory', + label: 'Inventory', + iconKey: 'inventory' + }, + { + key: 'sales', + label: 'Sales', + iconKey: 'sales' + }, + { + key: 'finance', + label: 'Finance', + iconKey: 'finance' + }, + { + key: 'management', + label: 'Management', + iconKey: 'settings' + } + ].filter((item) => + isSidebarSectionVisible(item.key, { includeDev, userProfile }) + ), + [includeDev, userProfile] + ) + + const selectedKey = useMemo(() => { + const pathParts = location.pathname.split('/').filter(Boolean) + const sectionKey = pathParts[1] + return ( + sectionItems.find((item) => item.key === sectionKey)?.key || + sectionItems[0]?.key + ) + }, [location.pathname, sectionItems]) + + const options = useMemo( + () => + sectionItems.map((item) => ({ + value: item.key, + label: item.label, + icon: getSidebarIconNode(item.iconKey) + })), + [sectionItems] + ) + + const showControls = !isElectron || authenticated + + if (!isMobile || !showControls || options.length === 0) { + return null + } + + return ( + + ) +} + +export default MobileFooterNavigation diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx index 9159fe8a..108a3555 100644 --- a/src/components/Dashboard/common/ObjectTable.jsx +++ b/src/components/Dashboard/common/ObjectTable.jsx @@ -388,7 +388,7 @@ const ObjectTable = forwardRef( const { getViewFromUrl } = useTableState() var adjustedScrollHeight = scrollHeight if (isMobile) { - adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)' + adjustedScrollHeight = 'calc(var(--unit-100vh) - 348px)' } if (isCards || isKanban || isTimeline) { adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)' diff --git a/src/components/Dashboard/context/ThemeContext.jsx b/src/components/Dashboard/context/ThemeContext.jsx index e8fea3b9..24714d87 100644 --- a/src/components/Dashboard/context/ThemeContext.jsx +++ b/src/components/Dashboard/context/ThemeContext.jsx @@ -204,6 +204,18 @@ export const ThemeProvider = ({ children }) => { '--color-text-placeholder', isDarkMode ? '#4F4F4F' : '#BFBFBF' ) + root.style.setProperty( + '--color-mobile-footer-bg', + isDarkMode ? '#141414' : '#ffffff' + ) + root.style.setProperty( + '--color-mobile-footer-item-bg', + isDarkMode ? '#1d1d1d' : '#f5f5f5' + ) + root.style.setProperty( + '--color-mobile-footer-item-border', + isDarkMode ? '#303030' : '#f0f0f0' + ) }, [isDarkMode, primaryColorOverride]) const themeConfig = {