Skip to content
This repository has been archived by the owner on Sep 12, 2023. It is now read-only.

#730 add user settings #757

Merged
merged 44 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
83067b8
#730 - fix incorrect importing user from Prisma client
jamescd18 Jun 25, 2022
306a4f1
#730 - added user settings to prisma w/ migration
jamescd18 Jun 25, 2022
0a9250b
#730 - create user settings on user creation
jamescd18 Jun 26, 2022
3a2c5e2
#730 - migrate back-end login endpoint to dedicated authenticated use…
jamescd18 Jun 26, 2022
530e186
#730 - merge main into branch to bring it up to date
jamescd18 Jun 27, 2022
5e7c143
Merge branch 'main' into #730-add-user-settings
jamescd18 Jun 30, 2022
6bc1bc7
#698 - fix project risks seed data
jamescd18 Jun 30, 2022
f15bad4
#730 - update services to use AuthenticatedUser interface
jamescd18 Jul 1, 2022
b64aec7
#730 - add get settings back-end route
jamescd18 Jul 1, 2022
0c75dae
#730 - add services for getting user settings
jamescd18 Jul 1, 2022
b99596c
#730 - add user settings to settings page
jamescd18 Jul 1, 2022
3d3bea8
#730 - rename theme provided object to theme utility
jamescd18 Jul 1, 2022
6b2e043
#730 - rename utils theme to theme name
jamescd18 Jul 1, 2022
e6c57da
#730 - rename theme back from theme utility
jamescd18 Jul 1, 2022
b96d5c9
#730 - strict theme name usage
jamescd18 Jul 1, 2022
b261687
#730 - fix theme name usage
jamescd18 Jul 1, 2022
6c2cad8
#730 - add set theme on login, update components
jamescd18 Jul 1, 2022
04c2c0f
#730 - fix wrong user import
jamescd18 Jul 1, 2022
8f8a602
#730 - refactor out into user settings view component
jamescd18 Jul 1, 2022
3980719
#730 - add update user settings endpoint
jamescd18 Jul 1, 2022
903aa8a
#730 - shrink formatting
jamescd18 Jul 1, 2022
63b60e9
#730 - create services for updating user settings
jamescd18 Jul 2, 2022
b4f02d8
#730 - move container into user settings view
jamescd18 Jul 2, 2022
ac440c1
#730 - base update user settings form working
jamescd18 Jul 2, 2022
7e627e1
#730 - shrink button choices
jamescd18 Jul 2, 2022
f3e387c
#730 - improve buttons to simplify form
jamescd18 Jul 2, 2022
1c4cee5
#730 - replace edit button with pencil icon
jamescd18 Jul 2, 2022
6376c8d
#730 - collaspe vairables into output jsx
jamescd18 Jul 2, 2022
a294e86
#730 - simplify on submit wrapper
jamescd18 Jul 2, 2022
011702c
#730 - clean imports
jamescd18 Jul 2, 2022
98177e7
#730 - more import cleaning
jamescd18 Jul 2, 2022
c937057
#730 - nest pencil icon in div for accessibility
jamescd18 Jul 2, 2022
2669131
#730 - enhance user settings component tests
jamescd18 Jul 2, 2022
19f525c
#730 - add tests for user settings edit component
jamescd18 Jul 2, 2022
1a95434
#730 - fix user settings database setup
jamescd18 Jul 13, 2022
f3cca5a
#730 - separate name and className in themes
jamescd18 Jul 14, 2022
f183f33
#730 - remove theme toggle in top bar
jamescd18 Jul 14, 2022
9daf697
#730 - auto-create user settings if user doesn't have one
jamescd18 Jul 14, 2022
40dca05
#730 - should use chosen theme from settings now
anthonybernardi Jul 16, 2022
b8a754f
#730 - tests
anthonybernardi Jul 16, 2022
4254ca8
#730 - switch to upsert
jamescd18 Jul 17, 2022
01b48f4
#730 - remove post-login theme toggle
jamescd18 Jul 18, 2022
55f9d60
#730 - revert to post-login theme toggle
jamescd18 Jul 27, 2022
80b5803
Merge branch 'main' into #730-add-user-settings
jamescd18 Jul 27, 2022
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
73 changes: 68 additions & 5 deletions src/backend/functions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,33 @@ import {
buildNotFoundResponse,
buildSuccessResponse,
routeMatcher,
User
User,
AuthenticatedUser
} from 'utils';

const prisma = new PrismaClient();

const authUserQueryArgs = Prisma.validator<Prisma.UserArgs>()({
include: {
userSettings: true
}
});

const authenticatedUserTransformer = (
user: Prisma.UserGetPayload<typeof authUserQueryArgs>
): AuthenticatedUser => {
return {
userId: user.userId,
firstName: user.firstName,
lastName: user.lastName,
googleAuthId: user.googleAuthId,
email: user.email,
emailId: user.emailId,
role: user.role,
defaultTheme: user.userSettings?.defaultTheme
};
};

const usersTransformer = (user: Prisma.UserGetPayload<null>): User => {
if (user === null) throw new TypeError('User not found');

Expand Down Expand Up @@ -70,7 +92,10 @@ const logUserIn: ApiRouteFunction = async (_params, event) => {
if (!payload) throw new Error('Auth server response payload invalid');
const { sub: userId } = payload; // google user id
// check if user is already in the database via Google ID
let user = await prisma.user.findUnique({ where: { googleAuthId: userId } });
let user = await prisma.user.findUnique({
where: { googleAuthId: userId },
...authUserQueryArgs
});

// if not in database, create user in database
if (user === null) {
Expand All @@ -83,8 +108,10 @@ const logUserIn: ApiRouteFunction = async (_params, event) => {
lastName: payload['family_name']!,
googleAuthId: userId,
email: payload['email']!,
emailId
}
emailId,
userSettings: { create: {} }
},
...authUserQueryArgs
});
user = createdUser;
}
Expand All @@ -97,7 +124,33 @@ const logUserIn: ApiRouteFunction = async (_params, event) => {
}
});

return buildSuccessResponse(usersTransformer(user));
return buildSuccessResponse(authenticatedUserTransformer(user));
};

/** Get settings for the specified user */
const getUserSettings: ApiRouteFunction = async (params: { id: string }) => {
const userId: number = parseInt(params.id);
const settings = await prisma.user_Settings.upsert({
where: { userId },
update: {},
create: { userId }
});
if (!settings) return buildNotFoundResponse('settings for user', `#${params.id}`);
return buildSuccessResponse(settings);
};

/** Update settings for the specified user */
const updateUserSettings: ApiRouteFunction = async (params: { id: string }, event) => {
jamescd18 marked this conversation as resolved.
Show resolved Hide resolved
const userId: number = parseInt(params.id);
if (!event.body) return buildClientFailureResponse('No settings found to update.');
const body = JSON.parse(event.body!);
if (!body.defaultTheme) return buildClientFailureResponse('No defaultTheme found for settings.');
await prisma.user_Settings.upsert({
where: { userId },
update: { defaultTheme: body.defaultTheme },
create: { userId, defaultTheme: body.defaultTheme }
});
return buildSuccessResponse({ message: `Successfully updated settings for user ${userId}.` });
};

// Define all valid routes for the endpoint
Expand All @@ -116,6 +169,16 @@ const routes: ApiRoute[] = [
path: `${API_URL}${apiRoutes.USERS_LOGIN}`,
httpMethod: 'POST',
func: logUserIn
},
{
path: `${API_URL}${apiRoutes.USER_SETTINGS_BY_USER_ID}`,
httpMethod: 'GET',
func: getUserSettings
},
{
path: `${API_URL}${apiRoutes.USER_SETTINGS_BY_USER_ID}`,
httpMethod: 'POST',
func: updateUserSettings
}
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- CreateEnum
CREATE TYPE "Theme" AS ENUM ('LIGHT', 'DARK');

-- CreateTable
CREATE TABLE "User_Settings" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"defaultTheme" "Theme" NOT NULL DEFAULT E'DARK',

PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_Settings.userId_unique" ON "User_Settings"("userId");

-- AddForeignKey
ALTER TABLE "User_Settings" ADD FOREIGN KEY ("userId") REFERENCES "User"("userId") ON DELETE CASCADE ON UPDATE CASCADE;
29 changes: 22 additions & 7 deletions src/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ enum Role {
GUEST
}

enum Theme {
LIGHT
DARK
}

enum Scope_CR_Why_Type {
ESTIMATION
SCHOOL
Expand All @@ -46,13 +51,16 @@ enum Scope_CR_Why_Type {
}

model User {
userId Int @id @default(autoincrement())
firstName String
lastName String
googleAuthId String @unique
email String @unique
emailId String? @unique
role Role @default(GUEST)
userId Int @id @default(autoincrement())
firstName String
lastName String
googleAuthId String @unique
email String @unique
emailId String? @unique
role Role @default(GUEST)
userSettings User_Settings?

// Relation references
submittedChangeRequests Change_Request[] @relation(name: "submittedChangeRequests")
reviewedChangeRequests Change_Request[] @relation(name: "reviewedChangeRequests")
markedAsProjectLead Activation_CR[] @relation(name: "markAsProjectLead")
Expand All @@ -74,6 +82,13 @@ model Session {
deviceInfo String?
}

model User_Settings {
id String @id @default(uuid())
userId Int @unique
user User @relation(fields: [userId], references: [userId])
defaultTheme Theme @default(DARK)
}

model Change_Request {
crId Int @id @default(autoincrement())
submitterId Int
Expand Down
12 changes: 8 additions & 4 deletions src/backend/prisma/seed-data/risks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@

const dbSeedRisk1: any = {
projectId: 1,
detail: 'This one might be a bit too expensive',
createdByUserId: 1
createdByUserId: 1,
fields: {
detail: 'This one might be a bit too expensive'
}
};

const dbSeedRisk2: any = {
projectId: 1,
detail: 'Risky Risk 123',
createdByUserId: 1
createdByUserId: 1,
fields: {
detail: 'Risky Risk 123'
}
};

export const dbSeedAllRisks: any[] = [dbSeedRisk1, dbSeedRisk2];
4 changes: 2 additions & 2 deletions src/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const prisma = new PrismaClient();

const performSeed: () => Promise<void> = async () => {
for (const seedUser of dbSeedAllUsers) {
await prisma.user.create({ data: { ...seedUser } });
await prisma.user.create({ data: { ...seedUser, userSettings: { create: {} } } });
}

for (const seedSession of dbSeedAllSessions) {
Expand Down Expand Up @@ -44,7 +44,7 @@ const performSeed: () => Promise<void> = async () => {
data: {
createdBy: { connect: { userId: seedRisk.createdByUserId } },
project: { connect: { projectId: seedRisk.projectId } },
...seedRisk
...seedRisk.fields
}
});
}
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/app/app-context/app-context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ jest.mock('../app-context-auth/app-context-auth', () => {
};
});

jest.mock('../app-context-theme/app-context-theme', () => {
return {
__esModule: true,
default: (props: any) => {
return <div>app context theme {props.children}</div>;
}
};
});

// Sets up the component under test with the desired values and renders it
const renderComponent = () => {
render(
Expand All @@ -44,6 +53,11 @@ describe('app context', () => {
expect(screen.getByText('app context auth')).toBeInTheDocument();
});

it('renders the app context theme component', () => {
renderComponent();
expect(screen.getByText('app context theme')).toBeInTheDocument();
});

it('renders the app context text', () => {
renderComponent();
expect(screen.getByText('full context')).toBeInTheDocument();
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/app/app-public/app-public.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const AppPublic: React.FC = () => {
document.body.style.backgroundColor = theme.bgColor;

return (
<html className={theme.name}>
<html className={theme.className}>
<Switch>
<Route path={routes.LOGIN}>
<Login
Expand Down
12 changes: 0 additions & 12 deletions src/frontend/layouts/nav-top-bar/nav-top-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@
* See the LICENSE file in the repository root folder for details.
*/

import { useTheme } from '../../../services/theme.hooks';
import themes from '../../../shared/themes';
import { Theme } from '../../../shared/types';
import { render, routerWrapperBuilder, screen } from '../../../test-support/test-utils';
import NavTopBar from './nav-top-bar';

jest.mock('../../../services/theme.hooks');
const mockTheme = useTheme as jest.Mock<Theme>;

const mockHook = () => {
mockTheme.mockReturnValue(themes[0]);
};

/**
* Sets up the component under test with the desired values and renders it.
*/
Expand All @@ -29,8 +19,6 @@ const renderComponent = () => {
};

describe('navigation top bar tests', () => {
beforeEach(() => mockHook());

it('renders site title', () => {
renderComponent();
expect(screen.getByText(/NER PM Dashboard/i)).toBeInTheDocument();
Expand Down
17 changes: 1 addition & 16 deletions src/frontend/layouts/nav-top-bar/nav-top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,17 @@
* See the LICENSE file in the repository root folder for details.
*/

import { Dropdown, Nav, Navbar } from 'react-bootstrap';
import { Nav, Navbar } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { routes } from '../../../shared/routes';
import { useAuth } from '../../../services/auth.hooks';
import { fullNamePipe } from '../../../shared/pipes';
import { useTheme } from '../../../services/theme.hooks';
import themes from '../../../shared/themes';
import NavUserMenu from './nav-user-menu/nav-user-menu';
import NavNotificationsMenu from './nav-notifications-menu/nav-notifications-menu';
import styles from './nav-top-bar.module.css';

const NavTopBar: React.FC = () => {
const auth = useAuth();
const theme = useTheme();

return (
<Navbar className={styles.mainBackground} variant="light" expand="md" fixed="top">
Expand All @@ -33,18 +30,6 @@ const NavTopBar: React.FC = () => {
<Navbar.Toggle aria-controls="nav-top-bar-items" />
<Navbar.Collapse id="nav-top-bar-items">
<Nav className="ml-auto">
<Dropdown className={styles.dropdown}>
<Dropdown.Toggle variant={theme.cardBg}>{theme.name}</Dropdown.Toggle>
<Dropdown.Menu>
{themes
.filter((t) => t.name !== theme.name)
.map((t) => (
<Dropdown.Item key={t.name} onClick={() => theme.toggleTheme!(t.name)}>
{t.name}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
<NavNotificationsMenu />
<div className={styles.username}>{fullNamePipe(auth.user)}</div>
<NavUserMenu />
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/pages/LoginPage/login-page/login-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* See the LICENSE file in the repository root folder for details.
*/

import themes from '../../../../shared/themes';
import { render, screen } from '../../../../test-support/test-utils';
import LoginPage from './login-page';

Expand All @@ -16,6 +17,7 @@ const renderComponent = () => {
devFormSubmit={(e) => e}
prodSuccess={(r) => r}
prodFailure={(r) => r}
theme={themes[0]}
/>
);
};
Expand Down
8 changes: 4 additions & 4 deletions src/frontend/pages/LoginPage/login-page/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import GoogleLogin from 'react-google-login';
import { Card } from 'react-bootstrap';
import LoginDev from '../login-dev/login-dev';
import { useTheme } from '../../../../services/theme.hooks';
import { Theme } from '../../../../shared/types';

const styles = {
card: {
Expand All @@ -19,6 +19,7 @@ interface LoginPageProps {
devFormSubmit: (e: any) => any;
prodSuccess: (res: any) => any;
prodFailure: (res: any) => any;
theme: Theme;
}

/**
Expand All @@ -28,10 +29,9 @@ const LoginPage: React.FC<LoginPageProps> = ({
devSetRole,
devFormSubmit,
prodSuccess,
prodFailure
prodFailure,
theme
}) => {
const theme = useTheme();

return (
<Card bg={theme.cardBg} className={'mx-auto mt-sm-5 '} style={styles.card}>
<Card.Body>
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/pages/LoginPage/login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const renderComponent = () => {
history.listen((loc) => {
pushed.push(loc.pathname);
});
return <Login postLoginRedirect={routes.HOME} />;
return <Login postLoginRedirect={{ url: routes.HOME, search: '' }} />;
};
const RouterWrapper = routerWrapperBuilder({ path: routes.LOGIN, route: routes.LOGIN });
return render(
Expand Down
Loading