diff --git a/api/package.json b/api/package.json
index 956f2fd..965fb42 100644
--- a/api/package.json
+++ b/api/package.json
@@ -22,7 +22,7 @@
"http-errors": "~1.6.3",
"if-env": "^1.0.4",
"isomorphic-unfetch": "^3.0.0",
- "lodash": "^4.17.20",
+ "lodash": "^4.17.21",
"mongodb": "^3.3.2",
"mongoose": "^5.7.5",
"morgan": "~1.9.1",
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/api/yarn.lock b/api/yarn.lock
index f61506a..ca2584b 100644
--- a/api/yarn.lock
+++ b/api/yarn.lock
@@ -3309,10 +3309,10 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
- integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.3:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lolex@^5.0.0:
version "5.1.2"
diff --git a/client/src/App.js b/client/src/App.js
index a6b1710..8056346 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -18,11 +18,13 @@ function App() {
useEffect(() => {
const userAuth = async () => {
const resp = await getUserAuth();
- if (!resp.error) setUser(resp.data.result);
+ if (!resp.error) setUser(resp?.data?.result);
};
userAuth();
}, [location]);
+ // TODO: Create user context and remove prop drilling
+
return (
{user &&
}
@@ -33,7 +35,7 @@ function App() {
-
+
{user ? : }
@@ -41,7 +43,7 @@ function App() {
-
+
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/components/table/Table.js b/client/src/components/table/Table.js
index 43f77e1..bd430a8 100644
--- a/client/src/components/table/Table.js
+++ b/client/src/components/table/Table.js
@@ -6,8 +6,11 @@ import '../../css/Table.css';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-alpine.css';
-const Table = ({ data, columns, onRowClick }) => {
+const Table = ({ data, columns, onRowClick, sizeToFit }) => {
const [entries, setEntries] = useState([]);
+
+ const onGridReady = (params) => sizeToFit && params.api.sizeColumnsToFit();
+
useEffect(() => {
setEntries(data);
}, [data]);
@@ -15,6 +18,7 @@ const Table = ({ data, columns, onRowClick }) => {
return (
(
+
+ {
+ // TODO: Remove inline styles in favor of external CSS
+ }{' '}
+ {subList
+ .map((id) => {
+ const idx = parentList.findIndex((m) => m.value === id);
+ return parentList?.[idx]?.text;
+ })
+ .join(', ')}
+
+);
+
+DisplayList.propTypes = {
+ subList: PropTypes.array,
+ parentList: PropTypes.array,
+};
+
+function Note({ user }) {
// note state
const [noteState, setNoteState] = useState(NOTE_STATE.loading);
- const [submitError, setSubmitError] = useState(false);
+ const [submitState, setSubmitState] = useState(SUBMIT_STATE.start);
+
+ // TODO! Merge view/edit mode into noteState enum
+ const [isEditable, setIsEditable] = useState(false);
// routing
const { noteID } = useParams();
@@ -70,6 +111,8 @@ function Note() {
const [members, setMembers] = useState([]);
const [allNoteLabels, setAllNoteLabels] = useState([]);
+ const history = useHistory();
+
useEffect(() => {
const init = async () => {
// if note is not new request template
@@ -102,6 +145,14 @@ function Note() {
setReferencedMembers(currentReferencedMembers.map((m) => m.memberId));
setViewableBy(currentViewableBy.map((m) => m.memberId));
setEditableBy(currentEditableBy.map((m) => m.memberId));
+
+ // check if current user is in editor list
+ if (
+ currentEditableBy.findIndex((m) => m.memberId === user?._id) > -1
+ ) {
+ setIsEditable(true);
+ }
+
setEditorState(
EditorState.createWithContent(convertFromRaw(JSON.parse(content))),
);
@@ -110,6 +161,7 @@ function Note() {
setNoteState(NOTE_STATE.error);
}
} else {
+ setIsEditable(true);
setNoteState(NOTE_STATE.creating);
}
@@ -134,7 +186,7 @@ function Note() {
setAllNoteLabels(cleanedNoteLabels);
};
init();
- }, [noteID]);
+ }, [noteID, user]);
/**
* handles shortcuts to format rich text
@@ -170,6 +222,10 @@ function Note() {
* @returns {undefined}
*/
const submitNote = () => {
+ if (!isEditable) {
+ return;
+ }
+
// check if this note has a valid title, at least one referenced member,
// and a valid editor state
if (
@@ -177,10 +233,12 @@ function Note() {
!noteTitle ||
!referencedMembers
) {
- setSubmitError(true);
+ setSubmitState(SUBMIT_STATE.error);
return;
}
+ setSubmitState(SUBMIT_STATE.start);
+
(noteState === NOTE_STATE.editing ? updateNote : createNote)(
{
content: JSON.stringify(convertToRaw(editorState.getCurrentContent())),
@@ -189,15 +247,24 @@ function Note() {
labels: noteLabels,
referencedMembers,
access: {
- editableBy,
- viewableBy,
+ // remove duplicate of current user id's on existing notes
+ editableBy: [...new Set([...editableBy, user._id])],
+ viewableBy: [...new Set([...viewableBy, user._id])],
},
},
},
noteID,
)
- .then(() => setSubmitError(false))
- .catch(() => setSubmitError(true));
+ .then((res) => {
+ setSubmitState(SUBMIT_STATE.success);
+ return res;
+ })
+ .then(
+ (res) =>
+ res?.data?.result?._id &&
+ history.push(`/notes/${res.data.result._id}`),
+ )
+ .catch(() => setSubmitState(SUBMIT_STATE.error));
};
/**
@@ -235,21 +302,28 @@ function Note() {
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
-
+ {isEditable && (
+
+ )}
Metadata
-
- {submitError && (
+ {isEditable && (
+
+ )}
+ {submitState === SUBMIT_STATE.error && (
{
// TODO: Add specific error messages
@@ -338,6 +443,9 @@ function Note() {
Error with submission.
)}
+ {submitState === SUBMIT_STATE.success && (
+
+ )}
@@ -346,4 +454,8 @@ function Note() {
}
}
+Note.propTypes = {
+ user: PropTypes.object,
+};
+
export default Note;
diff --git a/client/src/pages/Notes.js b/client/src/pages/Notes.js
index 439e714..9b08177 100644
--- a/client/src/pages/Notes.js
+++ b/client/src/pages/Notes.js
@@ -48,6 +48,7 @@ function Notes() {
data={notes}
columns={notesColumnDefs}
onRowClick={(e) => history.push(`/notes/${e.data._id}`)}
+ sizeToFit
/>
) : (
'No notes here!'
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
-