diff --git a/api/src/api/members.js b/api/src/api/members.js
index 1bdf1ab..ea31023 100644
--- a/api/src/api/members.js
+++ b/api/src/api/members.js
@@ -68,6 +68,8 @@ router.get(
}),
);
+// Create a new member
+// Requires Director Level
router.post(
'/',
requireDirector,
diff --git a/api/src/utils/user-utils.js b/api/src/utils/user-utils.js
index d56fbc1..6afbc0d 100644
--- a/api/src/utils/user-utils.js
+++ b/api/src/utils/user-utils.js
@@ -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) => {
diff --git a/client/src/components/EditableAttribute/DateAttribute.js b/client/src/components/EditableAttribute/DateAttribute.js
index 5918f54..0277435 100644
--- a/client/src/components/EditableAttribute/DateAttribute.js
+++ b/client/src/components/EditableAttribute/DateAttribute.js
@@ -11,6 +11,7 @@ const DateAttribute = ({
isDisabled = false,
className = '',
onChange,
+ isRequired = false,
}) => {
const onValueChange = (date) => {
onChange(date, attributeLabel);
@@ -24,6 +25,7 @@ const DateAttribute = ({
onChange={onValueChange}
selected={value}
disabled={isDisabled}
+ required={isRequired}
/>
);
@@ -35,6 +37,7 @@ DateAttribute.propTypes = {
isDisabled: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
+ isRequired: PropTypes.bool,
};
export default DateAttribute;
diff --git a/client/src/components/EditableAttribute/TextAttribute.js b/client/src/components/EditableAttribute/TextAttribute.js
index 0d7b563..e69afc9 100644
--- a/client/src/components/EditableAttribute/TextAttribute.js
+++ b/client/src/components/EditableAttribute/TextAttribute.js
@@ -10,6 +10,7 @@ const TextAttribute = ({
isDisabled = false,
className = '',
onChange,
+ isRequired = false,
}) => {
const onValueChange = (e) => {
onChange(e.target.value, attributeLabel);
@@ -23,6 +24,7 @@ const TextAttribute = ({
value={value}
onChange={onValueChange}
disabled={isDisabled}
+ required={isRequired}
/>
);
@@ -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,
};
diff --git a/client/src/components/navbar/Navbar.js b/client/src/components/navbar/Navbar.js
index 44c2002..2cc0cf0 100644
--- a/client/src/components/navbar/Navbar.js
+++ b/client/src/components/navbar/Navbar.js
@@ -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';
/**
@@ -19,6 +19,11 @@ const Navbar = ({ user }) => (
+ {levelEnum[user.level] >= levelEnum.DIRECTOR && (
+ -
+ Add Member
+
+ )}
-
Members
diff --git a/client/src/css/Navbar.css b/client/src/css/Navbar.css
index 64d8f04..b0686bf 100644
--- a/client/src/css/Navbar.css
+++ b/client/src/css/Navbar.css
@@ -35,6 +35,7 @@ nav .nav-link {
nav .profile-item p {
margin: 0;
display: flex;
+ margin-left: auto;
align-items: center;
}
diff --git a/client/src/pages/Profile.js b/client/src/pages/Profile.js
index 9c70369..6d65cec 100644
--- a/client/src/pages/Profile.js
+++ b/client/src/pages/Profile.js
@@ -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';
@@ -15,7 +15,9 @@ import {
getMemberPermissionsByID,
getMemberSchemaTypes,
updateMember,
+ createMember,
} from '../utils/apiWrapper';
+import { requiredFields } from '../utils/consts';
/**
* @constant
@@ -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({});
@@ -56,25 +60,32 @@ 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);
@@ -82,7 +93,7 @@ const Profile = () => {
}
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
@@ -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);
}
};
@@ -140,10 +165,12 @@ const Profile = () => {
>
}
>
+ {/* Redirects to the new member page immediately after creating and getting a success response */}
+ {newUserID && }
Profile
-