From 554f5caf4c99ece08747cf7eb4cfa52b45282e7f Mon Sep 17 00:00:00 2001 From: Michelle Ho <89873670+michho8@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:54:43 -1000 Subject: [PATCH] Create the departmental account icon (#112) --- .../layout/navbar/dept-account-icon.tsx | 66 +++++++++++++++++++ .../components/layout/navbar/navbar-menu.tsx | 2 +- ui/src/components/layout/navbar/navbar.tsx | 3 + ui/src/components/modal/dynamic-modal.tsx | 21 ++---- ui/src/lib/access/authorization.ts | 7 ++ ui/src/lib/access/role.ts | 3 +- .../layout/navbar/dept-account-icon.test.tsx | 31 +++++++++ .../components/layout/navbar/navbar.test.tsx | 19 ++++++ .../components/modal/dynamic-modal.test.tsx | 27 ++++++-- 9 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 ui/src/components/layout/navbar/dept-account-icon.tsx create mode 100644 ui/tests/components/layout/navbar/dept-account-icon.test.tsx diff --git a/ui/src/components/layout/navbar/dept-account-icon.tsx b/ui/src/components/layout/navbar/dept-account-icon.tsx new file mode 100644 index 00000000..7fbdc5f6 --- /dev/null +++ b/ui/src/components/layout/navbar/dept-account-icon.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useState } from 'react'; + +import DynamicModal from '@/components/modal/dynamic-modal'; +import { faUser, faSchool } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import User from '@/lib/access/user'; +import Role from '@/lib/access/role'; + +const DeptAccountIcon = ({ currentUser }: { currentUser: User }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const openDepartmentalModal = () => { + setIsModalOpen(true); + }; + + const closeDepartmentalModal = () => { + setIsModalOpen(false); + }; + + return ( + <> + {currentUser.roles.includes(Role.DEPARTMENTAL) && ( +
+ + + + +
+ +
+
+ +

You are not in your personal account

+
+
+
+
+ )} + {isModalOpen && ( + + )} + + ); +}; + +export default DeptAccountIcon; diff --git a/ui/src/components/layout/navbar/navbar-menu.tsx b/ui/src/components/layout/navbar/navbar-menu.tsx index 483e605b..d7d4ec50 100644 --- a/ui/src/components/layout/navbar/navbar-menu.tsx +++ b/ui/src/components/layout/navbar/navbar-menu.tsx @@ -1,7 +1,7 @@ 'use client'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; -import { User } from 'next-cas-client'; +import User from '@/lib/access/user'; import Link from 'next/link'; import { NavbarLinks } from './navbar-links'; import { useState } from 'react'; diff --git a/ui/src/components/layout/navbar/navbar.tsx b/ui/src/components/layout/navbar/navbar.tsx index 8382e4bc..e5a0e11d 100644 --- a/ui/src/components/layout/navbar/navbar.tsx +++ b/ui/src/components/layout/navbar/navbar.tsx @@ -6,9 +6,11 @@ import NavbarMenu from './navbar-menu'; import TimeoutModal from '@/components/modal/timeout-modal'; import { getUser } from '@/lib/access/user'; import Role from '@/lib/access/role'; +import DeptAccountIcon from '@/components/layout/navbar/dept-account-icon'; const Navbar = async () => { const currentUser = await getUser(); + return ( <> @@ -34,6 +36,7 @@ const Navbar = async () => { /> +
{NavbarLinks.filter( (navbarLink) => diff --git a/ui/src/components/modal/dynamic-modal.tsx b/ui/src/components/modal/dynamic-modal.tsx index d8933183..9db92515 100644 --- a/ui/src/components/modal/dynamic-modal.tsx +++ b/ui/src/components/modal/dynamic-modal.tsx @@ -9,41 +9,34 @@ import { AlertDialogFooter, AlertDialogCancel } from '@/components/ui/alert-dialog'; -import { ReactNode, useState } from 'react'; +import { ReactNode } from 'react'; import { Button } from '@/components/ui/button'; const DynamicModal = ({ open, title, body, - buttons + buttons, + onClose }: { open: boolean; title: string; body: string; buttons?: ReactNode[]; + onClose: () => void; }) => { - const [openDynamicModal, setOpenDynamicModal] = useState(open); - - /** - * Closes the modal. - */ - const close = () => { - setOpenDynamicModal(false); - }; - return ( - + {title} {body} - close()}>OK + OK {/*Any buttons that should lead the user to a different page.*/} {buttons?.map((button, index) => ( - ))} diff --git a/ui/src/lib/access/authorization.ts b/ui/src/lib/access/authorization.ts index 532bcf16..593893c2 100644 --- a/ui/src/lib/access/authorization.ts +++ b/ui/src/lib/access/authorization.ts @@ -21,6 +21,9 @@ export const setRoles = async (user: User): Promise => { if (await isAdmin(user.uhUuid)) { user.roles.push(Role.ADMIN); } + if (isDepartmental(user)) { + user.roles.push(Role.DEPARTMENTAL); + } }; /** @@ -58,3 +61,7 @@ const isValidUhUuid = (uhUuid: string): boolean => { const uhUuidPattern = new RegExp(/^[0-9]{8}$/); return uhUuidPattern.test(uhUuid); }; + +const isDepartmental = (user: User): boolean => { + return user.uid === user.uhUuid || !user.uhUuid; +}; diff --git a/ui/src/lib/access/role.ts b/ui/src/lib/access/role.ts index 58c32d8f..b58b8c53 100644 --- a/ui/src/lib/access/role.ts +++ b/ui/src/lib/access/role.ts @@ -2,7 +2,8 @@ enum Role { ADMIN = 'ADMIN', ANONYMOUS = 'ANONYMOUS', OWNER = 'OWNER', - UH = 'UH' + UH = 'UH', + DEPARTMENTAL = 'DEPARTMENTAL' } export default Role; diff --git a/ui/tests/components/layout/navbar/dept-account-icon.test.tsx b/ui/tests/components/layout/navbar/dept-account-icon.test.tsx new file mode 100644 index 00000000..53610fb7 --- /dev/null +++ b/ui/tests/components/layout/navbar/dept-account-icon.test.tsx @@ -0,0 +1,31 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import DeptAccountIcon from '@/components/layout/navbar/dept-account-icon'; +import User, { AnonymousUser } from '@/lib/access/user'; +import * as NextCasClient from 'next-cas-client/app'; +import Role from '@/lib/access/role'; + +const testUser: User = JSON.parse(process.env.TEST_USER_A as string); +jest.mock('next-cas-client/app'); + +describe('Dept Account Icon', () => { + it('should render the Departmental Account icon and open warning modal when clicked on', () => { + testUser.roles = [Role.DEPARTMENTAL]; + render(); + + fireEvent.focus(document); + expect(screen.getByLabelText('Departmental Account Icon')).toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText('Departmental Account Icon')); + expect(screen.getByRole('alertdialog', { name: 'Warning' })).toBeInTheDocument(); + expect(screen.getByRole('alertdialog')).toHaveTextContent('You are not in your personal account'); + }); + + it('should not render the Departmental Account icon for other roles', () => { + jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(AnonymousUser); + testUser.roles = [Role.ANONYMOUS, Role.ADMIN, Role.UH, Role.OWNER]; + render(); + + fireEvent.focus(document); + expect(screen.queryByRole('button', { name: 'Departmental Account Icon' })).not.toBeInTheDocument(); + }); +}); diff --git a/ui/tests/components/layout/navbar/navbar.test.tsx b/ui/tests/components/layout/navbar/navbar.test.tsx index 5fab47d7..2b49161c 100644 --- a/ui/tests/components/layout/navbar/navbar.test.tsx +++ b/ui/tests/components/layout/navbar/navbar.test.tsx @@ -90,5 +90,24 @@ describe('Navbar', () => { expect(screen.getByRole('link', { name: 'Feedback' })).toHaveAttribute('href', '/feedback'); expect(screen.getByRole('button', { name: `Logout (${testUser.uid})` })).toBeInTheDocument(); }); + + it('should render the departmental icon for a Departmental Account without Admin or Groupings links', async () => { + testUser.roles.push(Role.DEPARTMENTAL, Role.UH); + jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + render(await Navbar()); + + expect(screen.getByRole('navigation')).toBeInTheDocument(); + expect(screen.getAllByRole('img', { name: 'UH Groupings Logo' })[0]).toHaveAttribute( + 'src', + '/uhgroupings/uh-groupings-logo.svg' + ); + + expect(screen.getAllByRole('link', { name: 'UH Groupings Logo' })[0]).toHaveAttribute('href', '/'); + expect(screen.getByLabelText('Departmental Account Icon')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Memberships' })).toHaveAttribute('href', '/memberships'); + expect(screen.getByRole('link', { name: 'About' })).toHaveAttribute('href', '/about'); + expect(screen.getByRole('link', { name: 'Feedback' })).toHaveAttribute('href', '/feedback'); + expect(screen.getByRole('button', { name: `Logout (${testUser.uid})` })).toBeInTheDocument(); + }); }); }); diff --git a/ui/tests/components/modal/dynamic-modal.test.tsx b/ui/tests/components/modal/dynamic-modal.test.tsx index f52cef0b..2df95ced 100644 --- a/ui/tests/components/modal/dynamic-modal.test.tsx +++ b/ui/tests/components/modal/dynamic-modal.test.tsx @@ -1,10 +1,14 @@ import { fireEvent, render, screen } from '@testing-library/react'; import DynamicModal from '@/components/modal/dynamic-modal'; import Link from 'next/link'; +import { useState } from 'react'; describe('DynamicModal', () => { it('should open an informational modal with test contents and no extra buttons', () => { - render(); + const onClose = jest.fn(); + render( + + ); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'A Dynamic Title' })).toBeInTheDocument(); @@ -14,11 +18,13 @@ describe('DynamicModal', () => { }); it('should open an informational modal with test contents and extra buttons', () => { + const onClose = jest.fn(); render( Button1, <>Button2]} /> ); @@ -33,7 +39,16 @@ describe('DynamicModal', () => { }); it('should close the modal upon clicking the OK button', () => { - render(); + const onClose = jest.fn(); + render( + + ); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'A Dynamic Title' })).toBeInTheDocument(); @@ -43,15 +58,18 @@ describe('DynamicModal', () => { expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'OK' })); - expect(screen.queryByRole('alertdialog', { name: 'A Dynamic Title' })).not.toBeInTheDocument(); + // Cannot use useState in the test environment so expect the onClose function to be called once. + expect(onClose).toHaveBeenCalledTimes(1); // Assumes the onClose function toggles a useState variable. }); it('should close the modal and route to the provided link (Feedback)', () => { + const onClose = jest.fn(); render( Feedback @@ -69,6 +87,7 @@ describe('DynamicModal', () => { expect(screen.getByRole('link', { name: 'Feedback' })).toHaveAttribute('href', '/feedback'); fireEvent.click(screen.getByRole('button', { name: 'Feedback' })); - expect(screen.queryByRole('alertdialog', { name: 'A Modal to the Feedback Page' })).not.toBeInTheDocument(); + // Cannot use useState in the test environment so expect the onClose function to be called once. + expect(onClose).toHaveBeenCalledTimes(1); // Assumes the onClose function toggles a useState variable. }); });