mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4806 from thornbill/common-component-refactor
This commit is contained in:
commit
0d5b97455a
22 changed files with 353 additions and 262 deletions
24
src/components/AppBody.tsx
Normal file
24
src/components/AppBody.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import viewContainer from './viewContainer';
|
||||
|
||||
/**
|
||||
* A simple component that includes the correct structure for ViewManager pages
|
||||
* to exist alongside standard React pages.
|
||||
*/
|
||||
const AppBody: FC = ({ children }) => {
|
||||
useEffect(() => () => {
|
||||
// Reset view container state on unload
|
||||
viewContainer.reset();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppBody;
|
|
@ -1,19 +1,29 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
|
||||
const AppHeader = () => {
|
||||
interface AppHeaderParams {
|
||||
isHidden?: boolean
|
||||
}
|
||||
|
||||
const AppHeader: FC<AppHeaderParams> = ({
|
||||
isHidden = false
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
// Initialize the UI components after first render
|
||||
import('../scripts/libraryMenu');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
/**
|
||||
* NOTE: These components are not used with the new layouts, but legacy views interact with the elements
|
||||
* directly so they need to be present in the DOM. We use display: none to hide them and prevent errors.
|
||||
*/
|
||||
<div style={isHidden ? { display: 'none' } : undefined}>
|
||||
<div className='mainDrawer hide'>
|
||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||
</div>
|
||||
<div className='skinHeader focuscontainer-x' />
|
||||
<div className='mainDrawerHandle' />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
21
src/components/ElevationScroll.tsx
Normal file
21
src/components/ElevationScroll.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import useScrollTrigger from '@mui/material/useScrollTrigger';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
/**
|
||||
* Component that changes the elevation of a child component when scrolled.
|
||||
*/
|
||||
const ElevationScroll = ({ children, elevate = false }: { children: ReactElement, elevate?: boolean }) => {
|
||||
const trigger = useScrollTrigger({
|
||||
disableHysteresis: true,
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
const isElevated = elevate || trigger;
|
||||
|
||||
return React.cloneElement(children, {
|
||||
color: isElevated ? 'primary' : 'transparent',
|
||||
elevation: isElevated ? 4 : 0
|
||||
});
|
||||
};
|
||||
|
||||
export default ElevationScroll;
|
45
src/components/ListItemLink.tsx
Normal file
45
src/components/ListItemLink.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import ListItemButton, { ListItemButtonBaseProps } from '@mui/material/ListItemButton';
|
||||
import React, { FC } from 'react';
|
||||
import { Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||
|
||||
interface ListItemLinkProps extends ListItemButtonBaseProps {
|
||||
to: string
|
||||
}
|
||||
|
||||
const isMatchingParams = (routeParams: URLSearchParams, currentParams: URLSearchParams) => {
|
||||
for (const param of routeParams) {
|
||||
if (currentParams.get(param[0]) !== param[1]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const ListItemLink: FC<ListItemLinkProps> = ({
|
||||
children,
|
||||
to,
|
||||
...params
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
|
||||
const [ toPath, toParams ] = to.split('?');
|
||||
// eslint-disable-next-line compat/compat
|
||||
const toSearchParams = new URLSearchParams(`?${toParams}`);
|
||||
|
||||
const selected = location.pathname === toPath && (!toParams || isMatchingParams(toSearchParams, searchParams));
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={to}
|
||||
selected={selected}
|
||||
{...params}
|
||||
>
|
||||
{children}
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItemLink;
|
84
src/components/ResponsiveDrawer.tsx
Normal file
84
src/components/ResponsiveDrawer.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Theme } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import browser from 'scripts/browser';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
export interface ResponsiveDrawerProps {
|
||||
hasSecondaryToolBar?: boolean
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onOpen: () => void
|
||||
}
|
||||
|
||||
const ResponsiveDrawer: FC<ResponsiveDrawerProps> = ({
|
||||
children,
|
||||
hasSecondaryToolBar = false,
|
||||
open = false,
|
||||
onClose,
|
||||
onOpen
|
||||
}) => {
|
||||
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('sm'));
|
||||
const isLargeScreen = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg'));
|
||||
|
||||
const getToolbarStyles = useCallback((theme: Theme) => ({
|
||||
marginBottom: (hasSecondaryToolBar && !isLargeScreen) ? theme.spacing(6) : 0
|
||||
}), [ hasSecondaryToolBar, isLargeScreen ]);
|
||||
|
||||
return ( isSmallScreen ? (
|
||||
/* DESKTOP DRAWER */
|
||||
<Drawer
|
||||
sx={{
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width: DRAWER_WIDTH,
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
}}
|
||||
variant='persistent'
|
||||
anchor='left'
|
||||
open={open}
|
||||
>
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={getToolbarStyles}
|
||||
/>
|
||||
{children}
|
||||
</Drawer>
|
||||
) : (
|
||||
/* MOBILE DRAWER */
|
||||
<SwipeableDrawer
|
||||
anchor='left'
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
// Disable swipe to open on iOS since it interferes with back navigation
|
||||
disableDiscovery={browser.iOS}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={getToolbarStyles}
|
||||
/>
|
||||
<Box
|
||||
role='presentation'
|
||||
// Close the drawer when the content is clicked
|
||||
onClick={onClose}
|
||||
onKeyDown={onClose}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</SwipeableDrawer>
|
||||
));
|
||||
};
|
||||
|
||||
export default ResponsiveDrawer;
|
|
@ -1,4 +1,4 @@
|
|||
import loadable from '@loadable/component';
|
||||
import loadable, { LoadableComponent } from '@loadable/component';
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
|
@ -10,13 +10,18 @@ export enum AsyncRouteType {
|
|||
export interface AsyncRoute {
|
||||
/** The URL path for this route. */
|
||||
path: string
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
/** The route should use the page component from the experimental app. */
|
||||
/**
|
||||
* The relative path to the page component in the routes directory.
|
||||
* Will fallback to using the `path` value if not specified.
|
||||
*/
|
||||
page?: string
|
||||
/** The page element to render. */
|
||||
element?: LoadableComponent<AsyncPageProps>
|
||||
/** The page type used to load the correct page element. */
|
||||
type?: AsyncRouteType
|
||||
}
|
||||
|
||||
interface AsyncPageProps {
|
||||
export interface AsyncPageProps {
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
}
|
||||
|
@ -31,14 +36,19 @@ const StableAsyncPage = loadable(
|
|||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={(
|
||||
export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
|
||||
const Element = element
|
||||
|| (
|
||||
type === AsyncRouteType.Experimental ?
|
||||
<ExperimentalAsyncPage page={page} /> :
|
||||
<StableAsyncPage page={page} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
ExperimentalAsyncPage :
|
||||
StableAsyncPage
|
||||
);
|
||||
|
||||
return (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={<Element page={page ?? path} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
129
src/components/toolbar/AppToolbar.tsx
Normal file
129
src/components/toolbar/AppToolbar.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import appIcon from 'assets/img/icon-transparent.png';
|
||||
import { appRouter } from 'components/router/appRouter';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import UserMenuButton from './UserMenuButton';
|
||||
|
||||
interface AppToolbarProps {
|
||||
buttons?: ReactNode
|
||||
isDrawerAvailable: boolean
|
||||
isDrawerOpen: boolean
|
||||
onDrawerButtonClick: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const onBackButtonClick = () => {
|
||||
appRouter.back()
|
||||
.catch(err => {
|
||||
console.error('[AppToolbar] error calling appRouter.back', err);
|
||||
});
|
||||
};
|
||||
|
||||
const AppToolbar: FC<AppToolbarProps> = ({
|
||||
buttons,
|
||||
children,
|
||||
isDrawerAvailable,
|
||||
isDrawerOpen,
|
||||
onDrawerButtonClick
|
||||
}) => {
|
||||
const { user } = useApi();
|
||||
const isUserLoggedIn = Boolean(user);
|
||||
|
||||
const isBackButtonAvailable = appRouter.canGoBack();
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
variant='dense'
|
||||
sx={{
|
||||
flexWrap: {
|
||||
xs: 'wrap',
|
||||
lg: 'nowrap'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isUserLoggedIn && isDrawerAvailable && (
|
||||
<Tooltip title={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
edge='start'
|
||||
color='inherit'
|
||||
aria-label={globalize.translate(isDrawerOpen ? 'MenuClose' : 'MenuOpen')}
|
||||
onClick={onDrawerButtonClick}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isBackButtonAvailable && (
|
||||
<Tooltip title={globalize.translate('ButtonBack')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
// Set the edge if the drawer button is not shown
|
||||
edge={!(isUserLoggedIn && isDrawerAvailable) ? 'start' : undefined}
|
||||
color='inherit'
|
||||
aria-label={globalize.translate('ButtonBack')}
|
||||
onClick={onBackButtonClick}
|
||||
>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Box
|
||||
component={Link}
|
||||
to='/'
|
||||
color='inherit'
|
||||
aria-label={globalize.translate('Home')}
|
||||
sx={{
|
||||
ml: 2,
|
||||
display: 'inline-flex',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component='img'
|
||||
src={appIcon}
|
||||
sx={{
|
||||
height: '2rem',
|
||||
marginInlineEnd: 1
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant='h6'
|
||||
noWrap
|
||||
component='div'
|
||||
sx={{ display: { xs: 'none', sm: 'inline-block' } }}
|
||||
>
|
||||
Jellyfin
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{children}
|
||||
|
||||
{isUserLoggedIn && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
|
||||
{buttons}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<UserMenuButton />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppToolbar;
|
196
src/components/toolbar/AppUserMenu.tsx
Normal file
196
src/components/toolbar/AppUserMenu.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||
import AppSettingsAlt from '@mui/icons-material/AppSettingsAlt';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import Logout from '@mui/icons-material/Logout';
|
||||
import PhonelinkLock from '@mui/icons-material/PhonelinkLock';
|
||||
import Settings from '@mui/icons-material/Settings';
|
||||
import Storage from '@mui/icons-material/Storage';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu, { MenuProps } from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
export const ID = 'app-user-menu';
|
||||
|
||||
interface AppUserMenuProps extends MenuProps {
|
||||
onMenuClose: () => void
|
||||
}
|
||||
|
||||
const AppUserMenu: FC<AppUserMenuProps> = ({
|
||||
anchorEl,
|
||||
open,
|
||||
onMenuClose
|
||||
}) => {
|
||||
const { user } = useApi();
|
||||
|
||||
const onClientSettingsClick = useCallback(() => {
|
||||
window.NativeShell?.openClientSettings();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onExitAppClick = useCallback(() => {
|
||||
appHost.exit();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onLogoutClick = useCallback(() => {
|
||||
Dashboard.logout();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
const onSelectServerClick = useCallback(() => {
|
||||
Dashboard.selectServer();
|
||||
onMenuClose();
|
||||
}, [ onMenuClose ]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
id={ID}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={onMenuClose}
|
||||
>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={`/userprofile.html?userId=${user?.Id}`}
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AccountCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('Profile')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to='/mypreferencesmenu.html'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Settings />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('Settings')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('clientsettings') && ([
|
||||
<Divider key='client-settings-divider' />,
|
||||
<MenuItem
|
||||
key='client-settings-button'
|
||||
onClick={onClientSettingsClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AppSettingsAlt />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('ClientSettings')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
])}
|
||||
|
||||
{/* ADMIN LINKS */}
|
||||
{user?.Policy?.IsAdministrator && ([
|
||||
<Divider key='admin-links-divider' />,
|
||||
<MenuItem
|
||||
key='admin-dashboard-link'
|
||||
component={Link}
|
||||
to='/dashboard.html'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
|
||||
<ListItemIcon>
|
||||
<DashboardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('TabDashboard')} />
|
||||
</MenuItem>,
|
||||
<MenuItem
|
||||
key='admin-metadata-link'
|
||||
component={Link}
|
||||
to='/edititemmetadata.html'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Edit />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={globalize.translate('MetadataManager')} />
|
||||
</MenuItem>
|
||||
])}
|
||||
|
||||
<Divider />
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to='/quickconnect'
|
||||
onClick={onMenuClose}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PhonelinkLock />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('QuickConnect')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('multiserver') && (
|
||||
<MenuItem
|
||||
onClick={onSelectServerClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Storage />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('SelectServer')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={onLogoutClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Logout />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('ButtonSignOut')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
{appHost.supports('exitmenu') && ([
|
||||
<Divider key='exit-menu-divider' />,
|
||||
<MenuItem
|
||||
key='exit-menu-button'
|
||||
onClick={onExitAppClick}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Close />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{globalize.translate('ButtonExitApp')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
])}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppUserMenu;
|
51
src/components/toolbar/UserMenuButton.tsx
Normal file
51
src/components/toolbar/UserMenuButton.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import globalize from 'scripts/globalize';
|
||||
|
||||
import AppUserMenu, { ID } from './AppUserMenu';
|
||||
|
||||
const UserMenuButton = () => {
|
||||
const { user } = useApi();
|
||||
|
||||
const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||
const isUserMenuOpen = Boolean(userMenuAnchorEl);
|
||||
|
||||
const onUserButtonClick = useCallback((event) => {
|
||||
setUserMenuAnchorEl(event.currentTarget);
|
||||
}, [ setUserMenuAnchorEl ]);
|
||||
|
||||
const onUserMenuClose = useCallback(() => {
|
||||
setUserMenuAnchorEl(null);
|
||||
}, [ setUserMenuAnchorEl ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={globalize.translate('UserMenu')}>
|
||||
<IconButton
|
||||
size='large'
|
||||
edge='end'
|
||||
aria-label={globalize.translate('UserMenu')}
|
||||
aria-controls={ID}
|
||||
aria-haspopup='true'
|
||||
onClick={onUserButtonClick}
|
||||
color='inherit'
|
||||
sx={{ padding: 0 }}
|
||||
>
|
||||
<UserAvatar user={user} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<AppUserMenu
|
||||
open={isUserMenuOpen}
|
||||
anchorEl={userMenuAnchorEl}
|
||||
onMenuClose={onUserMenuClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenuButton;
|
Loading…
Add table
Add a link
Reference in a new issue