Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4568607
feat: add ProfileInfo component and enhance AccountActionButton with …
Oct 2, 2025
5d3f718
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
jekabs-karklins Oct 2, 2025
f61a11d
fix: update personal information test to use new account management b…
Oct 3, 2025
96ffc13
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
jekabs-karklins Oct 3, 2025
18f2d79
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
jekabs-karklins Oct 6, 2025
bc70eca
fix: update invite test to use data attribute for chip existence check
Oct 12, 2025
718171d
Merge branch 'SWAP-4997-move-user-info-under-profile-submenu' of gith…
Oct 12, 2025
8b575b4
fix: update invite test to check for non-existence of invites chips
Oct 12, 2025
c7771fa
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
jekabs-karklins Oct 12, 2025
22e0a7f
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 15, 2025
39a6b9d
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 15, 2025
2b30860
fix: optimised the element find fixing the issue
yoganandaness Oct 15, 2025
8d6beaa
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 15, 2025
0524de1
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 16, 2025
2895f30
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 16, 2025
b1d1eaa
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 17, 2025
5c7adea
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
yoganandaness Oct 24, 2025
3e50e00
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
bolmsten Oct 24, 2025
33a45e2
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
jekabs-karklins Oct 26, 2025
c0dc3de
fix: update invite tests to check for visibility of invite chips inst…
Oct 26, 2025
3213aa1
Merge branch 'develop' into SWAP-4997-move-user-info-under-profile-su…
bolmsten Oct 27, 2025
c351e80
fix: update invite tests to check for non-existence of invite chips u…
Oct 27, 2025
2a0f4be
fix: update invite test to click submit button with specific coordinates
Oct 27, 2025
64554e4
fix: update invite test to use default click for submit button and sp…
Oct 28, 2025
9a59abb
fix: update add participant button click to use force option for reli…
Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions apps/e2e/cypress/e2e/invites.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ context('Invites tests', () => {
.should('be.enabled')
.click();

cy.get('.MuiChip-label').should('not.exist');
cy.get('[data-cy="co-proposers"]').contains(lastName);
cy.get('[data-cy="invites-chips"]').should('not.exist');
});

it('Should not be able to invite email already invited on proposal', function () {
Expand Down Expand Up @@ -370,7 +370,9 @@ context('Invites tests', () => {
`${randomSearch}{enter}`
);

cy.get('.MuiChip-label').should('not.exist');
cy.get('[data-cy="participant-selector"] .MuiChip-label').should(
'not.exist'
);
});
});

Expand Down Expand Up @@ -410,7 +412,9 @@ context('Invites tests', () => {
.should('be.enabled')
.click();

cy.get('.MuiChip-label').should('not.exist');
cy.get('[data-cy="participant-selector"] .MuiChip-label').should(
'not.exist'
);
cy.get('[data-cy="co-proposers"]').contains(lastName);
});

Expand All @@ -435,7 +439,9 @@ context('Invites tests', () => {
cy.get('[data-cy="invite-user-autocomplete"]').type('{enter}');
cy.get('[role=presentation]').contains(`No results found for "${email}"`);
cy.get('[data-cy="invite-user-submit-button"]').should('be.disabled');
cy.get('.MuiChip-label').should('not.exist');
cy.get('[data-cy="participant-selector"] .MuiChip-label').should(
'not.exist'
);
});

it('Should not be able to add duplicate co-proposer in modal', () => {
Expand Down Expand Up @@ -465,7 +471,7 @@ context('Invites tests', () => {
.should('be.enabled')
.click();

cy.get('[data-cy="add-participant-button"]').click();
cy.get('[data-cy="add-participant-button"]').click({ force: true });

cy.get('[data-cy="invite-user-autocomplete"]').type(email);
cy.get('[role=presentation]')
Expand Down
4 changes: 3 additions & 1 deletion apps/e2e/cypress/e2e/personalInformation.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ context('Personal information tests', () => {
cy.login('user1');
cy.visit('/');

cy.get('[data-cy="active-user-profile"]').click();
cy.get('[data-cy="profile-page-btn"]').click();

cy.get('[data-cy="manage-account-button"]').click();

cy.get("[name='firstname']").clear().type(newFirstName);

Expand Down
6 changes: 5 additions & 1 deletion apps/e2e/cypress/e2e/visits.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,11 @@ context('visits tests', () => {
cy.finishedLoading();
cy.get('[name=email]').type('david@teleworm.us{enter}');
cy.finishedLoading();
cy.contains('Beckley').parent().find('[type=checkbox]').click();
cy.get('[data-cy=co-proposers]')
.contains('Beckley')
.parent()
.find('[type=checkbox]')
.click();
cy.get('[data-cy=assign-selected-users]').click();

// specify team lead
Expand Down
70 changes: 63 additions & 7 deletions apps/frontend/src/components/AppToolbar/AccountActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ExitToApp } from '@mui/icons-material';
import { ExitToApp, ManageAccounts } from '@mui/icons-material';
import AccountCircle from '@mui/icons-material/AccountCircle';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import SupervisedUserCircleIcon from '@mui/icons-material/SupervisedUserCircle';
import SwitchAccountOutlinedIcon from '@mui/icons-material/SwitchAccountOutlined';
import { Chip, Divider } from '@mui/material';
import Badge from '@mui/material/Badge';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
Expand All @@ -12,26 +13,37 @@ import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Tooltip from '@mui/material/Tooltip';
import React, { useContext, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';

import ImpersonateButton from 'components/common/ImpersonateButton';
import StyledDialog from 'components/common/StyledDialog';
import UOLoader from 'components/common/UOLoader';
import { SettingsContext } from 'context/SettingsContextProvider';
import { UserContext } from 'context/UserContextProvider';
import { SettingsId } from 'generated/sdk';
import { getUniqueArrayBy } from 'utils/helperFunctions';

import ProfileInfo from './ProfileInfo';
import RoleSelection from './RoleSelection';

const AccountActionButton = () => {
const navigate = useNavigate();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const [show, setShow] = useState(false);
const { roles, handleLogout, impersonatingUserId } = useContext(UserContext);
const { user } = useContext(UserContext);
const settingsContext = useContext(SettingsContext);
const { roles, currentRole, handleLogout, impersonatingUserId } =
useContext(UserContext);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);

const [searchParams, setSearchParams] = useSearchParams();

const hasMultipleRoles = getUniqueArrayBy(roles, 'id').length > 1;
const humanReadableActiveRole = useMemo(
() =>
roles.find(({ shortCode }) => shortCode.toUpperCase() === currentRole)
?.title ?? 'Unknown',
[roles, currentRole]
);

useEffect(() => {
if (searchParams.get('selectRoles')) {
Expand All @@ -46,6 +58,10 @@ const AccountActionButton = () => {

const isUserImpersonated = typeof impersonatingUserId === 'number';

const externalProfileLink = settingsContext.settingsMap.get(
SettingsId.PROFILE_PAGE_LINK
)?.settingsValue;

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
Expand All @@ -63,6 +79,15 @@ const AccountActionButton = () => {
});
};

const handleManageAccountClick = () => {
handleClose();
if (externalProfileLink) {
window.open(externalProfileLink, '_blank', 'noopener,noreferrer');
} else {
navigate(`/ProfilePage/${user.id}`);
}
};

return (
<>
<StyledDialog
Expand Down Expand Up @@ -121,6 +146,18 @@ const AccountActionButton = () => {
open={Boolean(anchorEl)}
onClose={handleClose}
>
<ProfileInfo />
<Divider style={{ marginBottom: '7px' }} />
<MenuItem
onClick={handleManageAccountClick}
disabled={isLoggingOut}
data-cy="manage-account-button"
>
<Box paddingRight={1} paddingTop={1}>
<ManageAccounts />
</Box>
Manage account
</MenuItem>
{hasMultipleRoles && (
<MenuItem
onClick={() => {
Expand All @@ -132,7 +169,26 @@ const AccountActionButton = () => {
<Box paddingRight={1} paddingTop={1}>
<SupervisedUserCircleIcon />
</Box>
Roles
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flex: 1,
}}
>
<Box>Roles</Box>
<Chip
label={humanReadableActiveRole}
color="primary"
size="small"
sx={{
fontSize: '10px',
height: 'inherit',
lineHeight: 2,
}}
/>
</Box>
</MenuItem>
)}
{isUserImpersonated && (
Expand Down
111 changes: 29 additions & 82 deletions apps/frontend/src/components/AppToolbar/AppToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import MenuIcon from '@mui/icons-material/Menu';
import { Box } from '@mui/material';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import MuiLink from '@mui/material/Link';
import { useTheme } from '@mui/material/styles';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import useMediaQuery from '@mui/material/useMediaQuery';
import PropTypes from 'prop-types';
import React, { useContext, useMemo, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';

import { SettingsContext } from 'context/SettingsContextProvider';
import { UserContext } from 'context/UserContextProvider';
import { SettingsId } from 'generated/sdk';

import AccountActionButton from './AccountActionButton';
Expand Down Expand Up @@ -44,18 +42,6 @@ const AppToolbar = ({ open, handleDrawerOpen, header }: AppToolbarProps) => {
setLogo('/images/' + logoFilename);
}, [logoFilename]);

const { user, roles, currentRole } = useContext(UserContext);
const settingsContext = useContext(SettingsContext);
const humanReadableActiveRole = useMemo(
() =>
roles.find(({ shortCode }) => shortCode.toUpperCase() === currentRole)
?.title ?? 'Unknown',
[roles, currentRole]
);
const externalProfileLink = settingsContext.settingsMap.get(
SettingsId.PROFILE_PAGE_LINK
)?.settingsValue;

return (
<AppBar
position="fixed"
Expand All @@ -80,75 +66,36 @@ const AppToolbar = ({ open, handleDrawerOpen, header }: AppToolbarProps) => {
}}
>
<Toolbar>
<IconButton
edge="start"
color="inherit"
aria-label="Open drawer"
onClick={handleDrawerOpen}
sx={{
marginRight: 15,
...(open && { display: isTabletOrMobile ? 'inline-flex' : 'none' }),
}}
data-cy="open-drawer"
>
<MenuIcon />
</IconButton>
{(!isTabletOrMobile || !isPortraitMode) && logo && (
<Link className={'header-logo-container'} to="/">
<img src={logo} alt="Institution logo" className={'header-logo'} />
</Link>
)}
{(!isTabletOrMobile || !isPortraitMode) && (
<Typography
component="h1"
variant="h6"
<Box sx={{ display: 'flex', flexGrow: 1, alignItems: 'center' }}>
<IconButton
edge="start"
color="inherit"
noWrap
sx={{ flexGrow: 1 }}
aria-label="Open drawer"
onClick={handleDrawerOpen}
sx={{
...(open && {
marginRight: '220px',
display: isTabletOrMobile ? 'inline-flex' : 'none',
}),
}}
data-cy="open-drawer"
>
{location.pathname === '/' ? 'Dashboard' : header}
</Typography>
)}
<Box sx={{ marginLeft: 'auto', margin: theme.spacing(0, 0.5) }}>
Logged in as{' '}
{externalProfileLink ? (
<MuiLink
href={externalProfileLink}
target="_blank"
rel="noreferrer"
sx={{
color: theme.palette.common.white,
textDecoration: 'none',
borderBottom: '1px dashed',
borderBottomColor: theme.palette.common.white,
padding: '3px',
'&:hover': {
textDecoration: 'none',
},
}}
>
{user.email}
</MuiLink>
) : (
<MuiLink
data-cy="active-user-profile"
component={Link}
to={`/ProfilePage/${user.id}`}
sx={{
color: theme.palette.common.white,
textDecoration: 'none',
borderBottom: '1px dashed',
borderBottomColor: theme.palette.common.white,
padding: '3px',
'&:hover': {
textDecoration: 'none',
},
}}
>
{user.email}
</MuiLink>
<MenuIcon />
</IconButton>
{(!isTabletOrMobile || !isPortraitMode) && logo && (
<Link className={'header-logo-container'} to="/">
<img
src={logo}
alt="Institution logo"
className={'header-logo'}
/>
</Link>
)}
{(!isTabletOrMobile || !isPortraitMode) && (
<Typography component="h1" variant="h6" color="inherit" noWrap>
{location.pathname === '/' ? 'Dashboard' : header}
</Typography>
)}
{roles.length > 1 && ` (${humanReadableActiveRole})`}
</Box>
<AccountActionButton />
</Toolbar>
Expand Down
48 changes: 48 additions & 0 deletions apps/frontend/src/components/AppToolbar/ProfileInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// react functional component

import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import React, { useContext } from 'react';

import { UserContext } from 'context/UserContextProvider';
import { getFullUserName } from 'utils/user';

const ProfileInfo = () => {
const { user } = useContext(UserContext);

return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
p: '15px 30px 15px 15px',
}}
>
<Avatar sx={{ width: 50, height: 50, mr: '15px' }} />
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography
noWrap
sx={{
color: 'black',
fontSize: '18px',
lineHeight: 1.3,
}}
>
{getFullUserName(user)}
</Typography>
<Typography
sx={{
color: 'dimgray',
fontSize: '14px',
lineHeight: 1.3,
}}
>
{user?.email.toLocaleLowerCase()}
</Typography>
</Box>
</Box>
);
};

export default ProfileInfo;
Loading
Loading