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

Create the departmental account icon #112

Merged
merged 1 commit into from
Dec 12, 2024
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
66 changes: 66 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,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) && (
<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"
Copy link
Contributor

@JorWo JorWo Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try h-6 w-6 for the width and height, let me know if you think it looks better. The school icon looks off center.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using h-6 and w-6 will make the blue background into more a circular shape. I like this one more than how it looked before with it being more oval-shaped.
image

>
<FontAwesomeIcon
icon={faSchool}
size="sm"
aria-label="Departmental Account Icon"
inverse
/>
</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={[]}
michho8 marked this conversation as resolved.
Show resolved Hide resolved
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.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(<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.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();
});
});
});
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.
});
});
Loading