diff --git a/apps/e2e/cypress/e2e/invites.cy.ts b/apps/e2e/cypress/e2e/invites.cy.ts index 329da96baa..4dc4f8e812 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('.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 () { @@ -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' + ); }); }); @@ -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); }); @@ -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', () => { @@ -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]') 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/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 diff --git a/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx b/apps/frontend/src/components/AppToolbar/AccountActionButton.tsx index 33694e99cf..f749967f7c 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 +169,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; 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" >