Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add User Settings Widgets to Dashboard - 1589 #1651

Merged
merged 2 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions frontend/src/assets/locales/en/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,17 @@
"inProgress": "Compliance report(s) in progress",
"awaitingGovReview": "Compliance report(s) awaiting government review",
"noActionRequired": "There are no reports that require any action."
},
"userSettings": {
"title": "User settings",
"notifications": "Notifications",
"configureNotifications": "Configure your notifications",
"help": "Help"
},
"orgUserSettings": {
"title": "User settings",
"notifications": "Notifications",
"configureNotifications": "Configure your notifications",
"help": "Help"
}
}
19 changes: 16 additions & 3 deletions frontend/src/views/Dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ import { Grid, Box } from '@mui/material'
import { Role } from '@/components/Role'
import { roles, govRoles, nonGovRoles } from '@/constants/roles'
import {
// IDIR Cards
AdminLinksCard,
DirectorReviewCard,
TransactionsCard,
UserSettingsCard,

// BCeID Cards
OrgDetailsCard,
OrgBalanceCard,
FeedbackCard,
WebsiteCard,
DirectorReviewCard,
TransactionsCard,
OrgTransactionsCard,
OrgComplianceReportsCard
OrgComplianceReportsCard,
OrgUserSettingsCard
} from './components/cards'
import OrganizationsSummaryCard from './components/cards/idir/OrganizationsSummaryCard'

Expand Down Expand Up @@ -93,6 +98,14 @@ export const Dashboard = () => {
<Role roles={[govRoles, roles.administrator]}>
<AdminLinksCard />
</Role>

{/* User settings */}
<Role roles={nonGovRoles}>
<OrgUserSettingsCard />
</Role>
<Role roles={govRoles}>
<UserSettingsCard />
</Role>
</Box>
</Grid>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react'
import { List, ListItemButton, Stack, Typography } from '@mui/material'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard'
import BCTypography from '@/components/BCTypography'
import withRole from '@/utils/withRole'
import { nonGovRoles } from '@/constants/roles'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { ROUTES } from '@/constants/routes'

const linkStyle = {
textDecoration: 'underline',
color: 'link.main',
'&:hover': { color: 'info.main' }
}

const UserSettingsLink = ({ onClick, children }) => (
<ListItemButton onClick={onClick}>
<Typography variant="subtitle2" sx={linkStyle}>
{children}
</Typography>
</ListItemButton>
)

const OrgUserSettingsCard = () => {
const { t } = useTranslation(['dashboard'])
const { data: currentUser } = useCurrentUser()
const navigate = useNavigate()

const { title, firstName, lastName } = currentUser || {}
const name = [firstName, lastName].filter(Boolean).join(' ')
const displayName = [name, title].filter(Boolean).join(', ')

return (
<BCWidgetCard
component="div"
disableHover={true}
color="nav"
icon="user"
title={t('dashboard:orgUserSettings.title')}
content={
<Stack spacing={1}>
<BCTypography
variant="body2"
sx={{ fontWeight: 'bold', color: '#003366' }}
>
{displayName}
</BCTypography>

<List component="nav" sx={{ maxWidth: '100%', mt: 1 }}>
<UserSettingsLink onClick={() => navigate(ROUTES.NOTIFICATIONS)}>
{t('dashboard:orgUserSettings.notifications')}
</UserSettingsLink>

<UserSettingsLink
onClick={() => navigate(ROUTES.NOTIFICATIONS_SETTINGS)}
>
{t('dashboard:orgUserSettings.configureNotifications')}
</UserSettingsLink>

{/* TODO: Update the link to the help page */}
<UserSettingsLink onClick={() => navigate()}>
{t('dashboard:orgUserSettings.help')}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
style={{ color: '#578260', marginLeft: 6 }}
/>
</UserSettingsLink>
</List>
</Stack>
}
/>
)
}

export default withRole(OrgUserSettingsCard, nonGovRoles)
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import OrgUserSettingsCard from '../OrgUserSettingsCard'
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { useNavigate } from 'react-router-dom'
import { ROUTES } from '@/constants/routes'
import { wrapper } from '@/tests/utils/wrapper'

vi.mock('@/hooks/useCurrentUser')

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key) => `mock__${key}`
})
}))

vi.mock('react-router-dom', () => ({
...vi.importActual('react-router-dom'),
useNavigate: vi.fn()
}))

vi.mock('@/utils/withRole', () => ({
__esModule: true,
default: (Component) =>
function MockWithRole(props) {
return <Component {...props} />
}
}))

describe('OrgUserSettingsCard', () => {
const mockNavigate = vi.fn()

beforeEach(() => {
mockNavigate.mockClear()
useNavigate.mockReturnValue(mockNavigate)
})

it('renders the user’s full name and title correctly', () => {
useCurrentUser.mockReturnValue({
data: {
firstName: 'Test',
lastName: 'User',
title: 'Developer',
roles: [{ name: 'Government' }]
},
isLoading: false
})

render(<OrgUserSettingsCard />, { wrapper })

expect(screen.getByText('Test User, Developer')).toBeInTheDocument()

expect(
screen.getByText('mock__dashboard:orgUserSettings.notifications')
).toBeInTheDocument()
expect(
screen.getByText('mock__dashboard:orgUserSettings.configureNotifications')
).toBeInTheDocument()
expect(
screen.getByText('mock__dashboard:orgUserSettings.help')
).toBeInTheDocument()
})

it('handles missing title gracefully', () => {
useCurrentUser.mockReturnValue({
data: {
firstName: 'Test',
lastName: 'User',
title: undefined,
roles: [{ name: 'Government' }]
},
isLoading: false
})

render(<OrgUserSettingsCard />, { wrapper })

// Should be "Test User" with no comma
expect(screen.getByText('Test User')).toBeInTheDocument()
})

it('handles missing firstName or lastName gracefully', () => {
useCurrentUser.mockReturnValue({
data: {
firstName: 'Test',
lastName: '',
title: 'Developer',
roles: [{ name: 'Government' }]
},
isLoading: false
})

render(<OrgUserSettingsCard />, { wrapper })

// Should show "Test, Developer"
expect(screen.getByText('Test, Developer')).toBeInTheDocument()
})

it('navigates to Notifications page when "mock__dashboard:orgUserSettings.notifications" is clicked', () => {
useCurrentUser.mockReturnValue({
data: {
firstName: 'Test',
lastName: 'User',
title: 'Dev',
roles: [{ name: 'Government' }]
},
isLoading: false
})

render(<OrgUserSettingsCard />, { wrapper })

const notificationsLink = screen.getByText(
'mock__dashboard:orgUserSettings.notifications'
)
fireEvent.click(notificationsLink)

expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS)
})

it('navigates to Notifications Settings page when "mock__dashboard:orgUserSettings.configureNotifications" is clicked', () => {
useCurrentUser.mockReturnValue({
data: {
firstName: 'Test',
lastName: 'User',
title: 'Dev',
roles: [{ name: 'Government' }]
},
isLoading: false
})

render(<OrgUserSettingsCard />, { wrapper })

const configureLink = screen.getByText(
'mock__dashboard:orgUserSettings.configureNotifications'
)
fireEvent.click(configureLink)

expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS_SETTINGS)
})

it('navigates to the help page (placeholder) when "mock__dashboard:orgUserSettings.help" is clicked', () => {
useCurrentUser.mockReturnValue({
data: {
firstName: 'Test',
lastName: 'User',
title: 'Dev',
roles: [{ name: 'Government' }]
},
isLoading: false
})

render(<OrgUserSettingsCard />, { wrapper })

const helpLink = screen.getByText('mock__dashboard:orgUserSettings.help')
fireEvent.click(helpLink)

// By default, it calls navigate() with no args
expect(mockNavigate).toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react'
import { List, ListItemButton, Stack, Typography } from '@mui/material'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard'
import BCTypography from '@/components/BCTypography'
import withRole from '@/utils/withRole'
import { govRoles } from '@/constants/roles'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import { useCurrentUser } from '@/hooks/useCurrentUser'
import { ROUTES } from '@/constants/routes'

const linkStyle = {
textDecoration: 'underline',
color: 'link.main',
'&:hover': { color: 'info.main' }
}

const UserSettingsLink = ({ onClick, children }) => (
<ListItemButton onClick={onClick}>
<Typography variant="subtitle2" sx={linkStyle}>
{children}
</Typography>
</ListItemButton>
)

const UserSettingsCard = () => {
const { t } = useTranslation(['dashboard'])
const { data: currentUser } = useCurrentUser()
const navigate = useNavigate()

const { title, firstName, lastName } = currentUser || {}
const name = [firstName, lastName].filter(Boolean).join(' ')
const displayName = [name, title].filter(Boolean).join(', ')

return (
<BCWidgetCard
component="div"
disableHover={true}
color="nav"
icon="user"
title={t('dashboard:userSettings.title')}
content={
<Stack spacing={1}>
<BCTypography
variant="body2"
sx={{ fontWeight: 'bold', color: '#003366' }}
>
{displayName}
</BCTypography>

<List component="nav" sx={{ maxWidth: '100%', mt: 1 }}>
<UserSettingsLink onClick={() => navigate(ROUTES.NOTIFICATIONS)}>
{t('dashboard:userSettings.notifications')}
</UserSettingsLink>

<UserSettingsLink
onClick={() => navigate(ROUTES.NOTIFICATIONS_SETTINGS)}
>
{t('dashboard:userSettings.configureNotifications')}
</UserSettingsLink>

{/* TODO: Update the link to the help page */}
<UserSettingsLink onClick={() => navigate()}>
{t('dashboard:userSettings.help')}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
style={{ color: '#578260', marginLeft: 6 }}
/>
</UserSettingsLink>
</List>
</Stack>
}
/>
)
}

export default withRole(UserSettingsCard, govRoles)
Loading
Loading