Skip to content

Commit

Permalink
User profile (#514)
Browse files Browse the repository at this point in the history
* move rcraprofile to core app

* filter RcraSitePermissions by user

* HaztrakUserView and HaztrakUserSerializer

* add HaztrakUser interface to UserState

* add selectUserState selector

* add asyncThunk for HaztrakUser and dispatch in app root

* user profile form

* add placeholder input for user avatar

* test suite for user profile
  • Loading branch information
dpgraham4401 authored Jun 13, 2023
1 parent 00af8d6 commit 3898050
Show file tree
Hide file tree
Showing 47 changed files with 687 additions and 377 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: ':bug: Bug Report'
name: '🐞 Bug Report'
about: 'Report an issue.'
title: ''
labels: 'bug'
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/other.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
name: ':nut_and_bolt: Something else'
name: 'Something Else'
about: 'Questions, discussions, chores, or other'
title: ''
labels: 'chore'
Expand Down
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import './App.scss';
import { getProfile } from 'store/rcraProfileSlice';
import { selectRcraProfile } from 'store/rcraProfileSlice';
import { selectUserName } from 'store/userSlice';
import { getHaztrakUser } from 'store/userSlice/user.slice';

function App(): ReactElement {
const userName = useAppSelector(selectUserName);
Expand All @@ -28,6 +29,7 @@ function App(): ReactElement {
useEffect(() => {
if (userName) {
dispatch(getProfile());
dispatch(getHaztrakUser());
}
}, [profile.user]);

Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Ht/HtForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ HtForm.InputGroup = function (props: InputGroupProps): ReactElement {

HtForm.Label = function (props: FormLabelProps): ReactElement {
return (
<Form.Label {...props} className="mb-0 fw-bold">
<Form.Label {...props} className={`mb-0 fw-bold ${props.className}`}>
{props.children}
</Form.Label>
);
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/Nav/Sidebar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ afterEach(() => {

describe('Sidebar', () => {
test('renders when use is logged in', () => {
const userName = 'testuser1';
const username = 'testuser1';
renderWithProviders(<Sidebar />, {
preloadedState: {
user: {
user: userName,
user: { username: username, isLoading: false },
token: 'fakeToken',
loading: false,
},
},
});
expect(screen.getByText(userName)).toBeInTheDocument();
expect(screen.getByText(username)).toBeInTheDocument();
});
test('returns nothing when user not logged in', () => {
const userName = 'testuser1';
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Nav/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function Sidebar(): ReactElement | null {
</div>
<div className="sb-sidenav-footer">
<div className="small">Logged in as:</div>
{authUser}
{authUser.username}
</div>
</nav>
</div>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Nav/TopNav.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ afterEach(() => {

describe('TopNav', () => {
test('renders when user is logged in', () => {
const userName = 'testuser1';
const username = 'testuser1';
renderWithProviders(<TopNav />, {
preloadedState: {
user: {
user: userName,
user: { username: username, isLoading: false },
token: 'fakeToken',
loading: false,
},
Expand Down
2 changes: 1 addition & 1 deletion client/src/features/home/Home.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('Home', () => {
renderWithProviders(<Home />, {
preloadedState: {
user: {
user: username,
user: { username: username, isLoading: false },
token: 'fake_token',
loading: false,
error: undefined,
Expand Down
12 changes: 6 additions & 6 deletions client/src/features/login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
import { login, useAppDispatch, useAppSelector } from 'store';
import { useNavigate } from 'react-router-dom';
import { useTitle } from 'hooks';
import { selectUser } from 'store/userSlice';
import { selectUserState } from 'store/userSlice';
import { z } from 'zod';
import { Col, Container, Form, Row } from 'react-bootstrap';
import logo from 'assets/haztrak-logos/low-resolution/svg/haztrak-low-resolution-logo-black-on-transparent-background.svg';
Expand All @@ -24,7 +24,7 @@ type LoginSchema = z.infer<typeof loginSchema>;
export function Login(): ReactElement {
useTitle('Login');
const dispatch = useAppDispatch();
const user = useAppSelector(selectUser);
const userState = useAppSelector(selectUserState);
const navigation = useNavigate();
const {
register,
Expand All @@ -34,10 +34,10 @@ export function Login(): ReactElement {

useEffect(() => {
// redirect to home if already logged in
if (user.user) {
if (userState.user?.username) {
navigation('/');
}
}, [user.user]);
}, [userState.user?.username]);

function onSubmit({ username, password }: LoginSchema) {
return dispatch(login({ username, password }));
Expand Down Expand Up @@ -86,8 +86,8 @@ export function Login(): ReactElement {
{isSubmitting && <span className="spinner-border spinner-border-sm mr-1" />}
Login
</button>
{user.error && (
<div className="alert alert-danger mt-3 mb-0">{String(user.error)}</div>
{userState.error && (
<div className="alert alert-danger mt-3 mb-0">{String(userState.error)}</div>
)}
</HtForm>
</HtCard.Body>
Expand Down
8 changes: 7 additions & 1 deletion client/src/features/profile/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HtCard } from 'components/Ht';
import { HaztrakUser, selectUser } from 'store/userSlice/user.slice';
import { RcraProfile } from './RcraProfile';
import { UserProfile } from './UserProfile';
import { useTitle } from 'hooks';
Expand All @@ -14,8 +15,13 @@ import { getProfile, selectRcraProfile } from 'store/rcraProfileSlice';
export function Profile(): ReactElement {
const dispatch = useAppDispatch();
const profile = useAppSelector(selectRcraProfile);
const user: HaztrakUser | undefined = useAppSelector(selectUser);
useTitle('Profile');

if (!user) {
return <div>loading...</div>;
}

useEffect(() => {
dispatch(getProfile());
}, [profile.user]);
Expand All @@ -31,7 +37,7 @@ export function Profile(): ReactElement {
<HtCard>
<HtCard.Header title="User Profile" />
<HtCard.Body>
<UserProfile profile={profile} />
<UserProfile user={user} />
</HtCard.Body>
</HtCard>
<HtCard>
Expand Down
3 changes: 1 addition & 2 deletions client/src/features/profile/RcraProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ interface ProfileViewProps {
profile: RcraProfileState;
}

// ToDo: Each field should be empty or meet the min length requirements
// ToDo: Either rcraAPIId & rcraAPIID should both be empty or both be non-empty
const rcraProfileForm = z.object({
rcraAPIID: z.string().min(36).optional(),
Expand All @@ -28,7 +27,7 @@ type RcraProfileForm = z.infer<typeof rcraProfileForm>;
export function RcraProfile({ profile }: ProfileViewProps) {
const [editable, setEditable] = useState(false);
const [profileLoading, setProfileLoading] = useState(false);
const { error, rcraSites, loading, ...formValues } = profile;
const { rcraSites, loading, ...formValues } = profile;
const dispatch = useAppDispatch();

const {
Expand Down
65 changes: 65 additions & 0 deletions client/src/features/profile/UserProfile.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from 'features/profile/UserProfile';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { HaztrakUser } from 'store/userSlice/user.slice';
import { renderWithProviders, screen } from 'test-utils';
import { API_BASE_URL } from 'test-utils/mock/handlers';
import { vi } from 'vitest';

const DEFAULT_USER: HaztrakUser = {
username: 'test',
firstName: 'David',
lastName: 'smith',
email: '[email protected]',
};

const server = setupServer(
rest.put(`${API_BASE_URL}/api/user/`, (req, res, ctx) => {
const user: HaztrakUser = { ...DEFAULT_USER };
// @ts-ignore
return res(ctx.status(200), ctx.json({ ...user, ...req.body }));
})
);

// pre-/post-test hooks
beforeAll(() => server.listen());
afterEach(() => {
server.resetHandlers();
cleanup();
vi.resetAllMocks();
});
afterAll(() => server.close()); // Disable API mocking after the tests are done.

describe('UserProfile', () => {
test('renders', () => {
const user: HaztrakUser = {
...DEFAULT_USER,
username: 'test',
firstName: 'David',
};
renderWithProviders(<UserProfile user={user} />, {});
expect(screen.getByRole('textbox', { name: 'First Name' })).toHaveValue(user.firstName);
expect(screen.getByText(user.username)).toBeInTheDocument();
});
test('update profile fields', async () => {
// Arrange
const newEmail = '[email protected]';
const user: HaztrakUser = {
...DEFAULT_USER,
};
renderWithProviders(<UserProfile user={user} />, {});
const editButton = screen.getByRole('button', { name: 'Edit' });
const emailTextBox = screen.getByRole('textbox', { name: 'Email' });
// Act
await userEvent.click(editButton);
await userEvent.clear(emailTextBox);
await userEvent.type(emailTextBox, newEmail);
const saveButton = screen.getByRole('button', { name: 'Save' });
await userEvent.click(saveButton);
// Assert
expect(await screen.findByRole('textbox', { name: 'Email' })).toHaveValue(newEmail);
});
});
Loading

0 comments on commit 3898050

Please sign in to comment.