Skip to content

Commit

Permalink
Create the departmental account icon
Browse files Browse the repository at this point in the history
  • Loading branch information
Michelle Ho committed Dec 6, 2024
1 parent c88af0e commit fcd19c6
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 20 deletions.
65 changes: 65 additions & 0 deletions ui/src/components/layout/navbar/dept-account-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import { useState } from 'react';

import { School } from 'lucide-react';
import DynamicModal from '@/components/modal/dynamic-modal';
import { faUser } 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) && (
<div
onClick={openDepartmentalModal}
className="flex justify-center items-center rounded-full
h-[45px] w-[45px] bg-seafoam mx-auto relative lg:ml-24"
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<FontAwesomeIcon aria-label="user" icon={faUser} width={14} height={16} />
<div
className="bg-blue-background rounded-full flex justify-center
items-center h-[20px] w-[25px] absolute left-7 bottom-0"
>
<School
className="fill-white stroke-1 p-0.5"
aria-label="Departmental Account Icon"
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>You are not in your personal account</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
{isModalOpen && (
<DynamicModal
open={isModalOpen}
title={'Warning'}
body={'You are not in your personal account.'}
buttons={[]}
onClose={closeDepartmentalModal}
/>
)}
</>
);
};

export default DeptAccountIcon;
2 changes: 1 addition & 1 deletion ui/src/components/layout/navbar/navbar-menu.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
3 changes: 3 additions & 0 deletions ui/src/components/layout/navbar/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<TimeoutModal currentUser={currentUser} />
Expand All @@ -34,6 +36,7 @@ const Navbar = async () => {
/>
</Link>
</div>
<DeptAccountIcon currentUser={currentUser} />
<div className="text-lg text-uh-black my-auto lg:space-x-5">
{NavbarLinks.filter(
(navbarLink) =>
Expand Down
21 changes: 7 additions & 14 deletions ui/src/components/modal/dynamic-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AlertDialog open={openDynamicModal}>
<AlertDialog open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{body}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => close()}>OK</AlertDialogCancel>
<AlertDialogCancel onClick={onClose}>OK</AlertDialogCancel>
{/*Any buttons that should lead the user to a different page.*/}
{buttons?.map((button, index) => (
<Button key={index} onClick={() => close()}>
<Button key={index} onClick={onClose}>
{button}
</Button>
))}
Expand Down
7 changes: 7 additions & 0 deletions ui/src/lib/access/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const setRoles = async (user: User): Promise<void> => {
if (await isAdmin(user.uhUuid)) {
user.roles.push(Role.ADMIN);
}
if (isDepartmental(user)) {
user.roles.push(Role.DEPARTMENTAL);
}
};

/**
Expand Down Expand Up @@ -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;
};
3 changes: 2 additions & 1 deletion ui/src/lib/access/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ enum Role {
ADMIN = 'ADMIN',
ANONYMOUS = 'ANONYMOUS',
OWNER = 'OWNER',
UH = 'UH'
UH = 'UH',
DEPARTMENTAL = 'DEPARTMENTAL'
}

export default Role;
31 changes: 31 additions & 0 deletions ui/tests/components/layout/navbar/dept-account-icon.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DeptAccountIcon currentUser={testUser} />);

fireEvent.focus(document);
expect(screen.getByRole('button', { name: 'Departmental Account Icon' })).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: '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(<DeptAccountIcon currentUser={testUser} />);

fireEvent.focus(document);
expect(screen.queryByRole('button', { name: 'Departmental Account Icon' })).not.toBeInTheDocument();
});
});
19 changes: 19 additions & 0 deletions ui/tests/components/layout/navbar/navbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.getByRole('button', { name: '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();
});
});
});
27 changes: 23 additions & 4 deletions ui/tests/components/modal/dynamic-modal.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<DynamicModal open={true} title="A Dynamic Title" body="Some dynamic message here." />);
const onClose = jest.fn();
render(
<DynamicModal open={true} title="A Dynamic Title" onClose={onClose} body="Some dynamic message here." />
);
fireEvent.focus(document);

expect(screen.getByRole('alertdialog', { name: 'A Dynamic Title' })).toBeInTheDocument();
Expand All @@ -14,11 +18,13 @@ describe('DynamicModal', () => {
});

it('should open an informational modal with test contents and extra buttons', () => {
const onClose = jest.fn();
render(
<DynamicModal
open={true}
title="A Dynamic Title"
body="Some dynamic message here."
onClose={onClose}
buttons={[<>Button1</>, <>Button2</>]}
/>
);
Expand All @@ -33,7 +39,16 @@ describe('DynamicModal', () => {
});

it('should close the modal upon clicking the OK button', () => {
render(<DynamicModal open={true} title="A Dynamic Title" body="Some dynamic message here." buttons={[]} />);
const onClose = jest.fn();
render(
<DynamicModal
open={true}
title="A Dynamic Title"
body="Some dynamic message here."
onClose={onClose}
buttons={[]}
/>
);
fireEvent.focus(document);

expect(screen.getByRole('alertdialog', { name: 'A Dynamic Title' })).toBeInTheDocument();
Expand All @@ -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(
<DynamicModal
open={true}
title="A Modal to the Feedback Page"
body="Click Feedback to go to the Feedback Page."
onClose={onClose}
buttons={[
<Link key={'feedbackButton'} href={'/feedback'}>
Feedback
Expand All @@ -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.
});
});

0 comments on commit fcd19c6

Please sign in to comment.