From 016d82a797b7ef443c153e7973ee286017b70731 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Thu, 29 May 2025 13:26:26 +0200 Subject: [PATCH] feat: update configuration menu (#10041) Updated "Configure" navigation, with all interactions including expanding/collapsing size of the menu. --- .../ConfigurationAccordion.tsx | 77 ++++++++ ...gation.tsx => ConfigurationNavigation.tsx} | 4 +- ...st.tsx => ConfigurationNavigationList.tsx} | 26 +-- .../NavigationSidebar/ListItems.tsx | 174 ++++++++++++++---- .../MobileNavigationSidebar.tsx | 16 +- .../NavigationSidebar/NavigationList.tsx | 174 ++++++++---------- .../NavigationSidebar/NavigationSidebar.tsx | 37 ++-- 7 files changed, 338 insertions(+), 170 deletions(-) create mode 100644 frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationAccordion.tsx rename frontend/src/component/layout/MainLayout/NavigationSidebar/{SecondaryNavigation.tsx => ConfigurationNavigation.tsx} (93%) rename frontend/src/component/layout/MainLayout/NavigationSidebar/{SecondaryNavigationList.tsx => ConfigurationNavigationList.tsx} (69%) diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationAccordion.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationAccordion.tsx new file mode 100644 index 0000000000..e1347dd3f4 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationAccordion.tsx @@ -0,0 +1,77 @@ +import { type FC, useEffect, useState } from 'react'; +import { MenuListAccordion } from './ListItems.tsx'; +import { useExpanded } from './useExpanded.ts'; +import type { NavigationMode } from './NavigationMode.tsx'; +import { IconRenderer } from './IconRenderer.tsx'; +import { ConfigurationNavigationList } from './ConfigurationNavigationList.tsx'; +import { useRoutes } from './useRoutes.ts'; + +type ConfigurationAccordionProps = { + mode: NavigationMode; + setMode; + activeItem?: string; + onClick?: () => void; +}; + +export const ConfigurationAccordion: FC = ({ + mode, + setMode, + activeItem, + onClick, +}) => { + const [expanded, changeExpanded] = useExpanded<'configure'>(); + const [temporarilyExpanded, setTemporarilyExpanded] = useState(false); + const { routes } = useRoutes(); + const subRoutes = routes.mainNavRoutes; + const hasActiveItem = Boolean( + activeItem && subRoutes.some((route) => route.path === activeItem), + ); + + useEffect(() => { + if (mode === 'mini') { + setTemporarilyExpanded(false); + } + }, [mode]); + + const onExpandChange = () => { + changeExpanded('configure', !expanded.includes('configure')); + + if (temporarilyExpanded) { + setTemporarilyExpanded(false); + setMode('mini'); + } + if (mode === 'mini') { + setTemporarilyExpanded(true); + setMode('full'); + } + }; + + const onItemClick = () => { + if (temporarilyExpanded) { + setTemporarilyExpanded(false); + setMode('mini'); + } + onClick?.(); + }; + + return ( + } + active={hasActiveItem} + > + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/SecondaryNavigation.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationNavigation.tsx similarity index 93% rename from frontend/src/component/layout/MainLayout/NavigationSidebar/SecondaryNavigation.tsx rename to frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationNavigation.tsx index 968229e570..ac2960904e 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/SecondaryNavigation.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationNavigation.tsx @@ -1,6 +1,6 @@ import type React from 'react'; import type { FC } from 'react'; -import type { NavigationMode } from './NavigationMode.tsx'; +import type { NavigationMode } from './NavigationMode.ts'; import { Typography } from '@mui/material'; import Accordion from '@mui/material/Accordion'; import AccordionDetails from '@mui/material/AccordionDetails'; @@ -21,7 +21,7 @@ const AccordionHeader: FC<{ children?: React.ReactNode }> = ({ children }) => { ); }; -export const SecondaryNavigation: FC<{ +export const ConfigurationNavigation: FC<{ expanded: boolean; onExpandChange: (expanded: boolean) => void; mode: NavigationMode; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/SecondaryNavigationList.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationNavigationList.tsx similarity index 69% rename from frontend/src/component/layout/MainLayout/NavigationSidebar/SecondaryNavigationList.tsx rename to frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationNavigationList.tsx index ac2a433adf..f4301fab7c 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/SecondaryNavigationList.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/ConfigurationNavigationList.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import type { INavigationMenuItem } from 'interfaces/route'; -import type { NavigationMode } from './NavigationMode.tsx'; -import { FullListItem, MiniListItem } from './ListItems.tsx'; +import type { NavigationMode } from './NavigationMode.ts'; +import { MenuListItem } from './ListItems.tsx'; import { List } from '@mui/material'; import { IconRenderer } from './IconRenderer.tsx'; import { useUiFlag } from 'hooks/useUiFlag.ts'; @@ -9,20 +9,19 @@ import StopRoundedIcon from '@mui/icons-material/StopRounded'; import { useShowBadge } from 'component/layout/components/EnterprisePlanBadge/useShowBadge'; import { EnterprisePlanBadge } from 'component/layout/components/EnterprisePlanBadge/EnterprisePlanBadge'; -export const SecondaryNavigationList: FC<{ +export const ConfigurationNavigationList: FC<{ routes: INavigationMenuItem[]; mode: NavigationMode; onClick: (activeItem: string) => void; activeItem?: string; }> = ({ routes, mode, onClick, activeItem }) => { const showBadge = useShowBadge(); - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; const sideMenuCleanup = useUiFlag('sideMenuCleanup'); return ( {routes.map((route) => ( - onClick(route.path)} href={route.path} @@ -33,13 +32,16 @@ export const SecondaryNavigationList: FC<{ ) : null } - > - {sideMenuCleanup ? ( - - ) : ( - - )} - + mode={mode} + icon={ + sideMenuCleanup ? ( + + ) : ( + + ) + } + secondary={sideMenuCleanup} + /> ))} ); diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx index c25db30328..d7969c111f 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx @@ -1,6 +1,8 @@ -import type React from 'react'; import type { FC, ReactNode } from 'react'; import { + Accordion, + AccordionDetails, + AccordionSummary, ListItem, ListItemButton, ListItemIcon, @@ -13,6 +15,8 @@ import { Link } from 'react-router-dom'; import { basePath } from 'utils/formatPath'; import SignOutIcon from '@mui/icons-material/ExitToApp'; import type { Theme } from '@mui/material/styles/createTheme'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import type { NavigationMode } from './NavigationMode.tsx'; const listItemButtonStyle = (theme: Theme) => ({ borderRadius: theme.spacing(0.5), @@ -22,12 +26,17 @@ const listItemButtonStyle = (theme: Theme) => ({ }, }); -const CappedText = styled(Typography)({ +const CappedText = styled(Typography)<{ + bold?: boolean; +}>(({ theme, bold }) => ({ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', width: '100%', -}); + fontWeight: bold + ? theme.typography.fontWeightBold + : theme.typography.fontWeightRegular, +})); const StyledListItemIcon = styled(ListItemIcon)(({ theme }) => ({ minWidth: theme.spacing(4), @@ -38,37 +47,10 @@ const StyledListItemText = styled(ListItemText)(({ theme }) => ({ margin: 0, })); -export const FullListItem: FC<{ - href: string; - text: string; - badge?: ReactNode; - onClick: () => void; - selected?: boolean; - children?: React.ReactNode; -}> = ({ href, text, badge, onClick, selected, children }) => { - return ( - - - {children} - - {text} - - {badge} - - - ); -}; - export const ExternalFullListItem: FC<{ href: string; text: string; - children?: React.ReactNode; + children?: ReactNode; }> = ({ href, text, children }) => { return ( @@ -88,6 +70,7 @@ export const ExternalFullListItem: FC<{ ); }; + export const SignOutItem = () => { return (
@@ -110,26 +93,139 @@ export const SignOutItem = () => { ); }; -export const MiniListItem: FC<{ +export const MenuListItem: FC<{ href: string; text: string; selected?: boolean; + badge?: ReactNode; onClick: () => void; - children?: React.ReactNode; -}> = ({ href, text, selected, onClick, children }) => { + icon?: ReactNode; + children?: ReactNode; + mode?: NavigationMode; + secondary?: boolean; +}> = ({ + href, + text, + selected, + onClick, + icon, + mode = 'full', + badge, + children, + secondary, +}) => { return ( ({ + ...listItemButtonStyle(theme), + ...(mode === 'full' && + secondary && { + paddingLeft: theme.spacing(4), + }), + })} selected={selected} > - - {children} - + {mode === 'mini' ? ( + + {icon} + + ) : ( + <> + {icon} + + {text} + + {badge} + + )} + {children} + + ); +}; + +const StyledAccordion = styled(Accordion)(({ theme }) => ({ + flexGrow: 1, + '.MuiAccordionSummary-root': { + minHeight: 'auto', + borderRadius: theme.spacing(1), + borderLeft: `${theme.spacing(0.5)} solid transparent`, + margin: 0, + paddingTop: theme.spacing(0.5), + paddingBottom: theme.spacing(0.5), + '.MuiAccordionSummary-content': { margin: 0 }, + '&>.MuiAccordionSummary-content.MuiAccordionSummary-content': { + margin: '0', + alignItems: 'center', + }, + }, + '.MuiAccordionSummary-content': { + margin: 0, + display: 'flex', + alignItems: 'center', + }, + '.MuiAccordionSummary-expandIconWrapper': { + position: 'absolute', + right: theme.spacing(1), + }, +})); + +export const MenuListAccordion: FC<{ + title: string; + expanded: boolean; + onExpandChange: (expanded: boolean) => void; + children?: ReactNode; + mode?: NavigationMode; + icon?: ReactNode; + active?: boolean; +}> = ({ title, expanded, mode, icon, onExpandChange, children, active }) => { + return ( + + { + onExpandChange(expand); + }} + > + : null} + > + + {mode === 'mini' ? ( + + {icon} + + ) : ( + <> + {icon} + + + {title} + + + + )} + + + {children} + ); }; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/MobileNavigationSidebar.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/MobileNavigationSidebar.tsx index 7e1fcbc159..d43be00e15 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/MobileNavigationSidebar.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/MobileNavigationSidebar.tsx @@ -5,24 +5,32 @@ import { OtherLinksList, } from './NavigationList.tsx'; import type { NewInUnleash } from './NewInUnleash/NewInUnleash.tsx'; -import { SecondaryNavigationList } from './SecondaryNavigationList.tsx'; +import { ConfigurationNavigationList } from './ConfigurationNavigationList.tsx'; import { useRoutes } from './useRoutes.ts'; +import { useUiFlag } from 'hooks/useUiFlag.ts'; export const MobileNavigationSidebar: FC<{ onClick: () => void; NewInUnleash?: typeof NewInUnleash; }> = ({ onClick, NewInUnleash }) => { const { routes } = useRoutes(); + const sideMenuCleanup = useUiFlag('sideMenuCleanup'); return ( <> {NewInUnleash ? : null} - - {}} /> + {!sideMenuCleanup ? ( + + ) : null} diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx index 6dacf4358a..2d0ea02373 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx @@ -1,10 +1,9 @@ -import type { FC } from 'react'; +import type { ComponentProps, FC } from 'react'; import type { INavigationMenuItem } from 'interfaces/route'; import type { NavigationMode } from './NavigationMode.tsx'; import { ExternalFullListItem, - FullListItem, - MiniListItem, + MenuListItem, SignOutItem, } from './ListItems.tsx'; import { Box, List, Typography } from '@mui/material'; @@ -16,6 +15,7 @@ import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectO import { useNewAdminMenu } from 'hooks/useNewAdminMenu'; import { AdminMenuNavigation } from '../AdminMenu/AdminNavigationItems.tsx'; import { useUiFlag } from 'hooks/useUiFlag.ts'; +import { ConfigurationAccordion } from './ConfigurationAccordion.tsx'; export const OtherLinksList = () => { const { uiConfig } = useUiConfig(); @@ -36,105 +36,92 @@ export const OtherLinksList = () => { ); }; +/** + * @deprecated remove with `sideMenuCleanup` flag + */ export const RecentProjectsList: FC<{ projectId: string; projectName: string; mode: NavigationMode; onClick: () => void; -}> = ({ projectId, projectName, mode, onClick }) => { - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; - - return ( - - - - - - ); -}; +}> = ({ projectId, projectName, mode, onClick }) => ( + + } + /> + +); +/** + * @deprecated remove with `sideMenuCleanup` flag + */ export const RecentFlagsList: FC<{ flags: { featureId: string; projectId: string }[]; mode: NavigationMode; onClick: () => void; -}> = ({ flags, mode, onClick }) => { - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; - - return ( - - {flags.map((flag) => ( - - - - ))} - - ); -}; +}> = ({ flags, mode, onClick }) => ( + + {flags.map((flag) => ( + } + /> + ))} + +); export const PrimaryNavigationList: FC<{ mode: NavigationMode; + setMode: (mode: NavigationMode) => void; onClick: (activeItem: string) => void; activeItem?: string; -}> = ({ mode, onClick, activeItem }) => { - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; +}> = ({ mode, setMode, onClick, activeItem }) => { + const PrimaryListItem = ({ + href, + text, + }: Pick, 'href' | 'text'>) => ( + } + onClick={() => onClick(href)} + selected={activeItem === href} + mode={mode} + /> + ); + const { isOss } = useUiConfig(); const sideMenuCleanup = useUiFlag('sideMenuCleanup'); return ( - onClick('/personal')} - selected={activeItem === '/personal'} - > - - - - onClick('/projects')} - selected={activeItem === '/projects'} - > - - - onClick('/search')} - selected={activeItem === '/search'} - > - - - onClick('/playground')} - selected={activeItem === '/playground'} - > - - + + + + {!isOss() ? ( - onClick('/insights')} - selected={activeItem === '/insights'} - > - - + /> + ) : null} + {sideMenuCleanup ? ( + onClick('configure')} + /> ) : null} ); @@ -173,22 +160,19 @@ export const AdminSettingsNavigation: FC<{ export const AdminSettingsLink: FC<{ mode: NavigationMode; onClick: (activeItem: string) => void; -}> = ({ mode, onClick }) => { - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; - return ( - - - onClick('/admin')} - > - - - - - ); -}; +}> = ({ mode, onClick }) => ( + + + onClick('/admin')} + mode={mode} + icon={} + /> + + +); export const RecentProjectsNavigation: FC<{ mode: NavigationMode; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx index 6d25fababb..463282d5f8 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx @@ -10,9 +10,8 @@ import { RecentProjectsNavigation, AdminSettingsNavigation, } from './NavigationList.tsx'; -import { SecondaryNavigationList } from './SecondaryNavigationList.tsx'; -import { SecondaryNavigation } from './SecondaryNavigation.tsx'; -import { FullListItem, MiniListItem } from './ListItems.tsx'; +import { ConfigurationNavigationList } from './ConfigurationNavigationList.tsx'; +import { ConfigurationNavigation } from './ConfigurationNavigation.tsx'; import { useInitialPathname } from './useInitialPathname.ts'; import { useLastViewedProject } from 'hooks/useLastViewedProject'; import { useLastViewedFlags } from 'hooks/useLastViewedFlags'; @@ -105,7 +104,6 @@ export const NavigationSidebar: FC<{ NewInUnleash?: typeof NewInUnleash }> = ({ const { lastViewed: lastViewedFlags } = useLastViewedFlags(); const showRecentFlags = !sideMenuCleanup && mode === 'full' && lastViewedFlags.length > 0; - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; useEffect(() => { setActiveItem(initialPathname); @@ -155,24 +153,27 @@ export const NavigationSidebar: FC<{ NewInUnleash?: typeof NewInUnleash }> = ({ <> - { - changeExpanded('configure', expand); - }} - mode={mode} - title='Configure' - > - { + changeExpanded('configure', expand); + }} mode={mode} - onClick={setActiveItem} - activeItem={activeItem} - /> - + title='Configure' + > + + + ) : null}