Skip to content

Commit

Permalink
Add Members UI (#70)
Browse files Browse the repository at this point in the history
* add button for new user

* add user created on frontend

* add user created on frontend

* formatting

* linting

* fixed add members

* linting

* comments

* comments

* linting

* add required fields

* removed css

* linting

* fixed year validation

* generalized digit regex
  • Loading branch information
Leundai authored May 5, 2021
1 parent 51df289 commit db7a031
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 24 deletions.
2 changes: 2 additions & 0 deletions api/src/api/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ router.get(
}),
);

// Create a new member
// Requires Director Level
router.post(
'/',
requireDirector,
Expand Down
4 changes: 2 additions & 2 deletions api/src/utils/user-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const nonEditableFields = [
const validationFields = {
email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
phone: /^[0-9]{10}$/,
gradYear: /^\d{4}/,
generationYear: /^\d{4}/,
gradYear: /^\d{4}$/,
generationYear: /^\d{4}$/,
};

const getViewableFields = (currentUser, memberId) => {
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/EditableAttribute/DateAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const DateAttribute = ({
isDisabled = false,
className = '',
onChange,
isRequired = false,
}) => {
const onValueChange = (date) => {
onChange(date, attributeLabel);
Expand All @@ -24,6 +25,7 @@ const DateAttribute = ({
onChange={onValueChange}
selected={value}
disabled={isDisabled}
required={isRequired}
/>
</div>
);
Expand All @@ -35,6 +37,7 @@ DateAttribute.propTypes = {
isDisabled: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
isRequired: PropTypes.bool,
};

export default DateAttribute;
3 changes: 3 additions & 0 deletions client/src/components/EditableAttribute/TextAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const TextAttribute = ({
isDisabled = false,
className = '',
onChange,
isRequired = false,
}) => {
const onValueChange = (e) => {
onChange(e.target.value, attributeLabel);
Expand All @@ -23,6 +24,7 @@ const TextAttribute = ({
value={value}
onChange={onValueChange}
disabled={isDisabled}
required={isRequired}
/>
</div>
);
Expand All @@ -33,6 +35,7 @@ TextAttribute.propTypes = {
type: PropTypes.string,
attributeLabel: PropTypes.string,
isDisabled: PropTypes.bool,
isRequired: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
};
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/navbar/Navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Link, NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';

import '../../css/Navbar.css';

import ProfileDropdown from '../ProfileDropdown/ProfileDropdown';
import { levelEnum } from '../../utils/consts';
import * as Routes from '../../routes';

/**
Expand All @@ -19,6 +19,11 @@ const Navbar = ({ user }) => (
</Link>
</h2>
<ul>
{levelEnum[user.level] >= levelEnum.DIRECTOR && (
<li>
<NavLink to="/member/new">Add Member</NavLink>
</li>
)}
<li>
<NavLink to="/">Members</NavLink>
</li>
Expand Down
1 change: 1 addition & 0 deletions client/src/css/Navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ nav .nav-link {
nav .profile-item p {
margin: 0;
display: flex;
margin-left: auto;
align-items: center;
}

Expand Down
76 changes: 55 additions & 21 deletions client/src/pages/Profile.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import { Link, useParams, Redirect } from 'react-router-dom';
import { Form, Message, Icon, Button, Card } from 'semantic-ui-react';
import _ from 'lodash';

Expand All @@ -15,7 +15,9 @@ import {
getMemberPermissionsByID,
getMemberSchemaTypes,
updateMember,
createMember,
} from '../utils/apiWrapper';
import { requiredFields } from '../utils/consts';

/**
* @constant
Expand All @@ -39,6 +41,8 @@ const areResponsesSuccessful = (...responses) => {

const Profile = () => {
const { memberID } = useParams();
const newUser = memberID === 'new';
const [newUserID, setNewUserID] = useState(false);

// Upstream user is the DB version. Local user captures local changes made to the user.
const [upstreamUser, setUpstreamUser] = useState({});
Expand All @@ -56,33 +60,40 @@ const Profile = () => {
async function getUserData() {
if (memberID == null) return;

const memberDataResponse = await getMemberByID(memberID);
const responses = [];

let memberDataResponse;
if (!newUser) {
memberDataResponse = await getMemberByID(memberID);
responses.push(memberDataResponse);
}

const memberPermissionResponse = await getMemberPermissionsByID(memberID);
const memberSchemaResponse = await getMemberSchemaTypes();
const enumOptionsResponse = await getMemberEnumOptions();
responses.push(
enumOptionsResponse,
memberSchemaResponse,
memberPermissionResponse,
);

if (
!areResponsesSuccessful(
memberDataResponse,
memberPermissionResponse,
memberSchemaResponse,
enumOptionsResponse,
)
) {
if (!areResponsesSuccessful(...responses)) {
setErrorMessage('An error occurred while retrieving member data.');
return;
}

setUpstreamUser(memberDataResponse.data.result);
setLocalUser(memberDataResponse.data.result);
if (!newUser) {
setUpstreamUser(memberDataResponse.data.result);
setLocalUser(memberDataResponse.data.result);
}
setUserPermissions(memberPermissionResponse.data.result);
setSchemaTypes(memberSchemaResponse.data.result);
setEnumOptions(enumOptionsResponse.data.result);
setErrorMessage(null);
}

getUserData();
}, [memberID]);
}, [memberID, newUser]);

// Returns true if the member attribute is of the given type.
// Type is a string defined by mongoose. See https://mongoosejs.com/docs/schematypes.html
Expand Down Expand Up @@ -114,20 +125,34 @@ const Profile = () => {
};

const submitChanges = async () => {
const result = await updateMember(createUpdatedUser(), upstreamUser._id);
let missingFields = false;
requiredFields.forEach((field) => {
if (!localUser[field]) {
missingFields = true;
}
});
if (missingFields) return;

const result = newUser
? await createMember(createUpdatedUser())
: await updateMember(createUpdatedUser(), upstreamUser._id);
if (!areResponsesSuccessful(result)) {
setErrorMessage(
`An error occured${
result && result.data && result.data.message
? `: ${result.data.message}`
result &&
result.error &&
result.error.response &&
result.error.response.data
? `: ${result.error.response.data.message}`
: '.'
}`,
);
setSuccessMessage(null);
} else {
setTemporarySuccessMessage('User updated');
setTemporarySuccessMessage(newUser ? 'User Created' : 'User updated');
setErrorMessage(null);
setUpstreamUser(result.data.result);
if (newUser) setNewUserID(result.data.result._id);
}
};

Expand All @@ -140,10 +165,12 @@ const Profile = () => {
</>
}
>
{/* Redirects to the new member page immediately after creating and getting a success response */}
{newUserID && <Redirect to={`/member/${newUserID}`} />}
<Card fluid>
<Card.Content>
<Card.Header>Profile</Card.Header>
<Form fluid className="profile-form">
<Form fluid className="profile-form" onSubmit={submitChanges}>
<div className="form-grid">
{
// Main content
Expand All @@ -158,6 +185,7 @@ const Profile = () => {
className="attribute"
onChange={onAttributeChange}
isDisabled={!userPermissions.edit.includes(attribute)}
isRequired={requiredFields.includes(attribute)}
/>
);
}
Expand Down Expand Up @@ -198,6 +226,7 @@ const Profile = () => {
onChange={onAttributeChange}
className="attribute"
isDisabled={!userPermissions.edit.includes(attribute)}
isRequired={requiredFields.includes(attribute)}
/>
);
}
Expand All @@ -212,6 +241,7 @@ const Profile = () => {
key={attribute}
onChange={onAttributeChange}
isDisabled={!userPermissions.edit.includes(attribute)}
isRequired={requiredFields.includes(attribute)}
/>
);
}
Expand All @@ -227,7 +257,9 @@ const Profile = () => {
<Message icon big positive>
<Icon name="thumbs up" />
<Message.Content>
<Message.Header>Update Succeeded!</Message.Header>
<Message.Header>
{newUser ? 'Create User' : 'Update'} Succeeded!
</Message.Header>
{successMessage}
</Message.Content>
</Message>
Expand All @@ -244,7 +276,9 @@ const Profile = () => {
<Message className="profile-alert" icon big negative>
<Icon name="warning circle" />
<Message.Content>
<Message.Header>Update Failed!</Message.Header>
<Message.Header>
{newUser ? 'Create User' : 'Update'} Failed!
</Message.Header>
{errorMessage}
</Message.Content>
</Message>
Expand All @@ -263,7 +297,7 @@ const Profile = () => {
type="large"
onClick={submitChanges}
>
Update
{newUser ? 'Create User' : 'Update'}
</Button>
</>
) : (
Expand Down
13 changes: 13 additions & 0 deletions client/src/utils/apiWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,16 @@ export const getNoteLabels = () => {
error,
}));
};

// Creates a new member
export const createMember = (member) => {
const requestString = `${BACKEND_BASE_URL}/members/`;
return axios
.post(requestString, {
...member,
})
.catch((error) => ({
type: 'GET_MEMBER_SCHEMA_TYPES_FAIL',
error,
}));
};
20 changes: 20 additions & 0 deletions client/src/utils/consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Useful for waterfall permissions where directors have all permissions
// Except for Admin permissions
export const levelEnum = {
ADMIN: 4,
DIRECTOR: 3,
LEAD: 2,
MEMBER: 1,
TBD: 0,
};

export const requiredFields = [
'email',
'firstName',
'lastName',
'gradYear',
'generationYear',
'phone',
];

export default { levelEnum, requiredFields };

1 comment on commit db7a031

@vercel
Copy link

@vercel vercel bot commented on db7a031 May 5, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.