From 456860702708483ba6057dd798fbe882d7662def Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Thu, 2 Oct 2025 16:35:25 +0200 Subject: [PATCH 01/10] feat: add ProfileInfo component and enhance AccountActionButton with account management options --- .../AppToolbar/AccountActionButton.tsx | 66 +++++++++-- .../src/components/AppToolbar/AppToolbar.tsx | 111 +++++------------- .../src/components/AppToolbar/ProfileInfo.tsx | 48 ++++++++ 3 files changed, 136 insertions(+), 89 deletions(-) create mode 100644 apps/frontend/src/components/AppToolbar/ProfileInfo.tsx diff --git a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx index 33694e99cf..31a156e83f 100644 --- a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx +++ b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx @@ -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'; @@ -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); - 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')) { @@ -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) => { setAnchorEl(event.currentTarget); }; @@ -63,6 +79,15 @@ const AccountActionButton = () => { }); }; + const handleManageAccountClick = () => { + handleClose(); + if (externalProfileLink) { + window.open(externalProfileLink, '_blank', 'noopener,noreferrer'); + } else { + navigate(`/ProfilePage/${user.id}`); + } + }; + return ( <> { open={Boolean(anchorEl)} onClose={handleClose} > + + + + + + + Manage account + {hasMultipleRoles && ( { @@ -132,7 +165,26 @@ const AccountActionButton = () => { - Roles + + Roles + + )} {isUserImpersonated && ( diff --git a/apps/frontend/src/components/AppToolbar/AppToolbar.tsx b/apps/frontend/src/components/AppToolbar/AppToolbar.tsx index 7341a09470..b628274e9a 100644 --- a/apps/frontend/src/components/AppToolbar/AppToolbar.tsx +++ b/apps/frontend/src/components/AppToolbar/AppToolbar.tsx @@ -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'; @@ -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 ( { }} > - - - - {(!isTabletOrMobile || !isPortraitMode) && logo && ( - - Institution logo - - )} - {(!isTabletOrMobile || !isPortraitMode) && ( - + - {location.pathname === '/' ? 'Dashboard' : header} - - )} - - Logged in as{' '} - {externalProfileLink ? ( - - {user.email} - - ) : ( - - {user.email} - + + + {(!isTabletOrMobile || !isPortraitMode) && logo && ( + + Institution logo + + )} + {(!isTabletOrMobile || !isPortraitMode) && ( + + {location.pathname === '/' ? 'Dashboard' : header} + )} - {roles.length > 1 && ` (${humanReadableActiveRole})`} diff --git a/apps/frontend/src/components/AppToolbar/ProfileInfo.tsx b/apps/frontend/src/components/AppToolbar/ProfileInfo.tsx new file mode 100644 index 0000000000..20e9aff603 --- /dev/null +++ b/apps/frontend/src/components/AppToolbar/ProfileInfo.tsx @@ -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 ( + + + + + {getFullUserName(user)} + + + {user?.email.toLocaleLowerCase()} + + + + ); +}; + +export default ProfileInfo; From f61a11db37597711839c784e22a51b8bffabd35c Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Fri, 3 Oct 2025 11:01:19 +0200 Subject: [PATCH 02/10] fix: update personal information test to use new account management button --- apps/e2e/cypress/e2e/personalInformation.cy.ts | 4 +++- .../src/components/AppToolbar/AccountActionButton.tsx | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/e2e/cypress/e2e/personalInformation.cy.ts b/apps/e2e/cypress/e2e/personalInformation.cy.ts index 744b9be6f1..92f5e63d3b 100644 --- a/apps/e2e/cypress/e2e/personalInformation.cy.ts +++ b/apps/e2e/cypress/e2e/personalInformation.cy.ts @@ -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); diff --git a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx index 31a156e83f..f749967f7c 100644 --- a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx +++ b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx @@ -148,7 +148,11 @@ const AccountActionButton = () => { > - + From bc70ecacc5cf097c74ff384dc949fa6f90bf7a83 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Sun, 12 Oct 2025 12:55:40 +0200 Subject: [PATCH 03/10] fix: update invite test to use data attribute for chip existence check --- apps/e2e/cypress/e2e/invites.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index 0fb0c3edf3..9e449a0534 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -101,7 +101,7 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('.MuiChip-label').should('not.exist'); + cy.get('[data-cy="invites-chips"]').contains(email).should('not.exist'); cy.get('[data-cy="co-proposers"]').contains(lastName); }); From 8b575b4247b73df08bd750877e6ab69b2cb5600d Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Sun, 12 Oct 2025 15:10:43 +0200 Subject: [PATCH 04/10] fix: update invite test to check for non-existence of invites chips --- apps/e2e/cypress/e2e/invites.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index 1404f36ab9..56c2db79c0 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -101,8 +101,8 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('[data-cy="invites-chips"]').contains(email).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 () { From 2b30860d7c30723ae7bba2075b9e9c46d6c4d9fe Mon Sep 17 00:00:00 2001 From: Yoganandan Pandiyan Date: Wed, 15 Oct 2025 17:30:31 +0200 Subject: [PATCH 05/10] fix: optimised the element find fixing the issue --- apps/e2e/cypress/e2e/visits.cy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/visits.cy.ts b/apps/e2e/cypress/e2e/visits.cy.ts index 294c3975b9..d7a0ee58ec 100644 --- a/apps/e2e/cypress/e2e/visits.cy.ts +++ b/apps/e2e/cypress/e2e/visits.cy.ts @@ -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 From c0dc3de7f3bc7193f0340f4de13bc9fc28c3baed Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Mon, 27 Oct 2025 01:11:33 +0200 Subject: [PATCH 06/10] fix: update invite tests to check for visibility of invite chips instead of existence --- apps/e2e/cypress/e2e/invites.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index 5466b19ff4..f89f18cafc 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -370,7 +370,7 @@ context('Invites tests', () => { `${randomSearch}{enter}` ); - cy.get('.MuiChip-label').should('not.exist'); + cy.get('.MuiChip-label').should('not.be.visible'); }); }); @@ -410,7 +410,7 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('.MuiChip-label').should('not.exist'); + cy.get('.MuiChip-label').should('not.be.visible'); cy.get('[data-cy="co-proposers"]').contains(lastName); }); @@ -435,7 +435,7 @@ 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('.MuiChip-label').should('not.be.visible'); }); it('Should not be able to add duplicate co-proposer in modal', () => { From c351e80548022d5a4b5f7a7aaefd119c3a700aaf Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Mon, 27 Oct 2025 21:51:09 +0200 Subject: [PATCH 07/10] fix: update invite tests to check for non-existence of invite chips using data attributes --- apps/e2e/cypress/e2e/invites.cy.ts | 12 +++++++++--- .../src/components/proposal/ParticipantSelector.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index f89f18cafc..a6a2a41bf3 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -370,7 +370,9 @@ context('Invites tests', () => { `${randomSearch}{enter}` ); - cy.get('.MuiChip-label').should('not.be.visible'); + cy.get('[data-cy="participant-selector"] .MuiChip-label').should( + 'not.exist' + ); }); }); @@ -410,7 +412,9 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('.MuiChip-label').should('not.be.visible'); + cy.get('[data-cy="participant-selector"] .MuiChip-label').should( + 'not.exist' + ); cy.get('[data-cy="co-proposers"]').contains(lastName); }); @@ -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.be.visible'); + cy.get('[data-cy="participant-selector"] .MuiChip-label').should( + 'not.exist' + ); }); it('Should not be able to add duplicate co-proposer in modal', () => { diff --git a/apps/frontend/src/components/proposal/ParticipantSelector.tsx b/apps/frontend/src/components/proposal/ParticipantSelector.tsx index d19dbc0587..890edd7f57 100644 --- a/apps/frontend/src/components/proposal/ParticipantSelector.tsx +++ b/apps/frontend/src/components/proposal/ParticipantSelector.tsx @@ -75,7 +75,6 @@ function ParticipantSelector({ preset = [], multiple = true, }: ParticipantSelectorProps & WithConfirmProps) { - // multiple selection enabled by default const api = useDataApi(); const [query, setQuery] = useState(''); const [loading, setLoading] = useState(false); @@ -312,6 +311,7 @@ function ParticipantSelector({ ) } + data-cy="participant-selector" > Date: Mon, 27 Oct 2025 22:31:59 +0200 Subject: [PATCH 08/10] fix: update invite test to click submit button with specific coordinates --- apps/e2e/cypress/e2e/invites.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index a6a2a41bf3..d8eb74e0e0 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -469,7 +469,7 @@ context('Invites tests', () => { cy.get('[role=presentation]').contains(lastName).click(); cy.get('[data-cy="invite-user-submit-button"]') .should('be.enabled') - .click(); + .click(1, 1); cy.get('[data-cy="add-participant-button"]').click(); From 64554e4409c93ea0b364958c86132f43c4193475 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Tue, 28 Oct 2025 08:10:36 +0200 Subject: [PATCH 09/10] fix: update invite test to use default click for submit button and specific coordinates for add participant button --- apps/e2e/cypress/e2e/invites.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index d8eb74e0e0..b4acc6799d 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -469,9 +469,9 @@ context('Invites tests', () => { cy.get('[role=presentation]').contains(lastName).click(); cy.get('[data-cy="invite-user-submit-button"]') .should('be.enabled') - .click(1, 1); + .click(); - cy.get('[data-cy="add-participant-button"]').click(); + cy.get('[data-cy="add-participant-button"]').click(1, 1); cy.get('[data-cy="invite-user-autocomplete"]').type(email); cy.get('[role=presentation]') From 9a59abbfb5886edc8917a463b564eedef215a946 Mon Sep 17 00:00:00 2001 From: jekabskarklins Date: Tue, 28 Oct 2025 08:33:32 +0200 Subject: [PATCH 10/10] fix: update add participant button click to use force option for reliability --- apps/e2e/cypress/e2e/invites.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index b4acc6799d..4dc4f8e812 100644 --- a/apps/e2e/cypress/e2e/invites.cy.ts +++ b/apps/e2e/cypress/e2e/invites.cy.ts @@ -471,7 +471,7 @@ context('Invites tests', () => { .should('be.enabled') .click(); - cy.get('[data-cy="add-participant-button"]').click(1, 1); + cy.get('[data-cy="add-participant-button"]').click({ force: true }); cy.get('[data-cy="invite-user-autocomplete"]').type(email); cy.get('[role=presentation]')