Skip to content

Commit

Permalink
Merge pull request #1651 from bcgov/feat/hamed-user-settings-widget-1589
Browse files Browse the repository at this point in the history
Feat: Add User Settings Widgets to Dashboard - 1589
  • Loading branch information
hamed-valiollahi authored Jan 10, 2025
2 parents 7d4134f + 34610a3 commit fd27caf
Show file tree
Hide file tree
Showing 7 changed files with 508 additions and 3 deletions.
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

0 comments on commit fd27caf

Please sign in to comment.