From 3c6861856a2f19c4055d9064e4f27f2d4ad47561 Mon Sep 17 00:00:00 2001 From: Hamed Valiollahi Date: Thu, 9 Jan 2025 15:42:51 -0800 Subject: [PATCH] feat: add user settings widget to the dashboard --- frontend/src/assets/locales/en/dashboard.json | 12 ++ frontend/src/views/Dashboard/Dashboard.jsx | 19 ++- .../cards/bceid/OrgUserSettingsCard.jsx | 79 +++++++++ .../__tests__/OrgUserSettingsCard.test.jsx | 160 ++++++++++++++++++ .../cards/idir/UserSettingsCard.jsx | 79 +++++++++ .../idir/__tests__/UserSettingsCard.test.jsx | 160 ++++++++++++++++++ .../views/Dashboard/components/cards/index.js | 2 + 7 files changed, 508 insertions(+), 3 deletions(-) create mode 100644 frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx create mode 100644 frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx create mode 100644 frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx create mode 100644 frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx diff --git a/frontend/src/assets/locales/en/dashboard.json b/frontend/src/assets/locales/en/dashboard.json index c88fa945b..7ec7521dc 100644 --- a/frontend/src/assets/locales/en/dashboard.json +++ b/frontend/src/assets/locales/en/dashboard.json @@ -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" } } diff --git a/frontend/src/views/Dashboard/Dashboard.jsx b/frontend/src/views/Dashboard/Dashboard.jsx index 7d6a66052..2708d076e 100644 --- a/frontend/src/views/Dashboard/Dashboard.jsx +++ b/frontend/src/views/Dashboard/Dashboard.jsx @@ -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' @@ -93,6 +98,14 @@ export const Dashboard = () => { + + {/* User settings */} + + + + + + diff --git a/frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx b/frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx new file mode 100644 index 000000000..18a45546d --- /dev/null +++ b/frontend/src/views/Dashboard/components/cards/bceid/OrgUserSettingsCard.jsx @@ -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 }) => ( + + + {children} + + +) + +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 ( + + + {displayName} + + + + navigate(ROUTES.NOTIFICATIONS)}> + {t('dashboard:orgUserSettings.notifications')} + + + navigate(ROUTES.NOTIFICATIONS_SETTINGS)} + > + {t('dashboard:orgUserSettings.configureNotifications')} + + + {/* TODO: Update the link to the help page */} + navigate()}> + {t('dashboard:orgUserSettings.help')} + + + + + } + /> + ) +} + +export default withRole(OrgUserSettingsCard, nonGovRoles) diff --git a/frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx b/frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx new file mode 100644 index 000000000..3a4aa4030 --- /dev/null +++ b/frontend/src/views/Dashboard/components/cards/bceid/__tests__/OrgUserSettingsCard.test.jsx @@ -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 + } +})) + +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(, { 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(, { 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(, { 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(, { 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(, { 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(, { wrapper }) + + const helpLink = screen.getByText('mock__dashboard:orgUserSettings.help') + fireEvent.click(helpLink) + + // By default, it calls navigate() with no args + expect(mockNavigate).toHaveBeenCalled() + }) +}) diff --git a/frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx b/frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx new file mode 100644 index 000000000..76331bb0d --- /dev/null +++ b/frontend/src/views/Dashboard/components/cards/idir/UserSettingsCard.jsx @@ -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 }) => ( + + + {children} + + +) + +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 ( + + + {displayName} + + + + navigate(ROUTES.NOTIFICATIONS)}> + {t('dashboard:userSettings.notifications')} + + + navigate(ROUTES.NOTIFICATIONS_SETTINGS)} + > + {t('dashboard:userSettings.configureNotifications')} + + + {/* TODO: Update the link to the help page */} + navigate()}> + {t('dashboard:userSettings.help')} + + + + + } + /> + ) +} + +export default withRole(UserSettingsCard, govRoles) diff --git a/frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx b/frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx new file mode 100644 index 000000000..cf0b93b21 --- /dev/null +++ b/frontend/src/views/Dashboard/components/cards/idir/__tests__/UserSettingsCard.test.jsx @@ -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 UserSettingsCard from '../UserSettingsCard' +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 + } +})) + +describe('UserSettingsCard', () => { + 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(, { wrapper }) + + expect(screen.getByText('Test User, Developer')).toBeInTheDocument() + + expect( + screen.getByText('mock__dashboard:userSettings.notifications') + ).toBeInTheDocument() + expect( + screen.getByText('mock__dashboard:userSettings.configureNotifications') + ).toBeInTheDocument() + expect( + screen.getByText('mock__dashboard:userSettings.help') + ).toBeInTheDocument() + }) + + it('handles missing title gracefully', () => { + useCurrentUser.mockReturnValue({ + data: { + firstName: 'Test', + lastName: 'User', + title: undefined, + roles: [{ name: 'Government' }] + }, + isLoading: false + }) + + render(, { 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(, { wrapper }) + + // Should show "Test, Developer" + expect(screen.getByText('Test, Developer')).toBeInTheDocument() + }) + + it('navigates to Notifications page when "mock__dashboard:userSettings.notifications" is clicked', () => { + useCurrentUser.mockReturnValue({ + data: { + firstName: 'Test', + lastName: 'User', + title: 'Dev', + roles: [{ name: 'Government' }] + }, + isLoading: false + }) + + render(, { wrapper }) + + const notificationsLink = screen.getByText( + 'mock__dashboard:userSettings.notifications' + ) + fireEvent.click(notificationsLink) + + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS) + }) + + it('navigates to Notifications Settings page when "mock__dashboard:userSettings.configureNotifications" is clicked', () => { + useCurrentUser.mockReturnValue({ + data: { + firstName: 'Test', + lastName: 'User', + title: 'Dev', + roles: [{ name: 'Government' }] + }, + isLoading: false + }) + + render(, { wrapper }) + + const configureLink = screen.getByText( + 'mock__dashboard:userSettings.configureNotifications' + ) + fireEvent.click(configureLink) + + expect(mockNavigate).toHaveBeenCalledWith(ROUTES.NOTIFICATIONS_SETTINGS) + }) + + it('navigates to the help page (placeholder) when "mock__dashboard:userSettings.help" is clicked', () => { + useCurrentUser.mockReturnValue({ + data: { + firstName: 'Test', + lastName: 'User', + title: 'Dev', + roles: [{ name: 'Government' }] + }, + isLoading: false + }) + + render(, { wrapper }) + + const helpLink = screen.getByText('mock__dashboard:userSettings.help') + fireEvent.click(helpLink) + + // By default, it calls navigate() with no args + expect(mockNavigate).toHaveBeenCalled() + }) +}) diff --git a/frontend/src/views/Dashboard/components/cards/index.js b/frontend/src/views/Dashboard/components/cards/index.js index 58aefc825..95f57fb9e 100644 --- a/frontend/src/views/Dashboard/components/cards/index.js +++ b/frontend/src/views/Dashboard/components/cards/index.js @@ -2,6 +2,7 @@ export { default as AdminLinksCard } from './idir/AdminLinksCard' export { default as DirectorReviewCard } from './idir/DirectorReviewCard' export { default as TransactionsCard } from './idir/TransactionsCard' +export { default as UserSettingsCard } from './idir/UserSettingsCard' // BCeID Cards export { default as OrgDetailsCard } from './bceid/OrgDetailsCard' @@ -10,3 +11,4 @@ export { default as FeedbackCard } from './bceid/FeedbackCard' export { default as WebsiteCard } from './bceid/WebsiteCard' export { default as OrgTransactionsCard } from './bceid/OrgTransactionsCard' export { default as OrgComplianceReportsCard } from './bceid/OrgComplianceReportsCard' +export { default as OrgUserSettingsCard } from './bceid/OrgUserSettingsCard'