@@ -161,29 +148,27 @@ const TimeEntry = (props) => {
{hours}h {minutes}m
Project/Task:
-
- {projectName}
+
+ {projectName}
- {taskName && `\u2003 ↳ ${taskName}`}
+ {taskName && `\u2003 ↳ ${taskName}`}
-
- {
- canEditTangibility
- ? (
- <>
-
Tangible:
-
- {isProcessing ?
Processing... : null}
- >
- )
- :
{isTangible ? 'Tangible' : 'Intangible'}
- }
+
+ {canEditTangibility ? (
+ <>
+ Tangible:
+
+ {isProcessing ? Processing... : null}
+ >
+ ) : (
+ {isTangible ? 'Tangible' : 'Intangible'}
+ )}
@@ -193,13 +178,14 @@ const TimeEntry = (props) => {
{ReactHtmlParser(notes)}
- {((hasATimeEntryEditPermission || isAuthUserAndSameDayEntry) && !cantEditJaeRelatedRecord) && (
-
-
-
- )}
+ {(hasATimeEntryEditPermission || isAuthUserAndSameDayEntry) &&
+ !cantEditJaeRelatedRecord && (
+
+
+
+ )}
{canDelete && (
-
+
)}
@@ -211,7 +197,7 @@ const TimeEntry = (props) => {
{/* this TimeEntryForm could be rendered from either weekly tab or task tab */}
{
/>
);
-};
+}
-const mapStateToProps = (state) => ({
+const mapStateToProps = state => ({
authUser: state.auth.user,
-})
+});
const mapDispatchToProps = dispatch => ({
hasPermission: permission => dispatch(hasPermission(permission)),
diff --git a/src/components/Timelog/TimeEntryForm/AboutModal.jsx b/src/components/Timelog/TimeEntryForm/AboutModal.jsx
index 6a0a157a4f..95ae5a4992 100644
--- a/src/components/Timelog/TimeEntryForm/AboutModal.jsx
+++ b/src/components/Timelog/TimeEntryForm/AboutModal.jsx
@@ -6,7 +6,7 @@ import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap';
* @param {Boolean} props.visible
* @param {Func} props.setVisible
*/
-const AboutModal = props => {
+function AboutModal(props) {
return (
Info
@@ -72,6 +72,6 @@ const AboutModal = props => {
);
-};
+}
export default AboutModal;
diff --git a/src/components/Timelog/TimeEntryForm/ReminderModal.jsx b/src/components/Timelog/TimeEntryForm/ReminderModal.jsx
index 8305f49a7f..f730101752 100644
--- a/src/components/Timelog/TimeEntryForm/ReminderModal.jsx
+++ b/src/components/Timelog/TimeEntryForm/ReminderModal.jsx
@@ -10,7 +10,7 @@ import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap';
* @param {*} props.inputs
* @param {Func} cancelChange
*/
-const ReminderModal = props => {
+function ReminderModal(props) {
const { edit, visible, data, inputs, reminder, cancelChange, setVisible, darkMode } = props;
return (
@@ -20,16 +20,14 @@ const ReminderModal = props => {
setVisible(false)} color="danger">
Continue
- {edit &&
- (data.hours !== inputs.hours ||
- data.minutes !== inputs.minutes) && (
-
- Cancel
-
- )}
+ {edit && (data.hours !== inputs.hours || data.minutes !== inputs.minutes) && (
+
+ Cancel
+
+ )}
);
-};
+}
export default ReminderModal;
diff --git a/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx b/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx
index aedfc869cb..ba205aa313 100644
--- a/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx
+++ b/src/components/Timelog/TimeEntryForm/TangibleInfoModal.jsx
@@ -7,7 +7,7 @@ import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap';
* @param {Func} props.setVisible
* @param {Boolean} props.darkMode
*/
-const TangibleInfoModal = props => {
+function TangibleInfoModal(props) {
return (
Info
@@ -26,6 +26,6 @@ const TangibleInfoModal = props => {
);
-};
+}
export default TangibleInfoModal;
diff --git a/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx b/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx
index c4206c6895..6862ccc96f 100644
--- a/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx
+++ b/src/components/Timelog/TimeEntryForm/TimeEntryForm.jsx
@@ -1,4 +1,8 @@
-import React, { useState, useEffect } from 'react';
+/* eslint-disable react/require-default-props */
+/* eslint-disable react/no-unused-prop-types */
+/* eslint-disable react/forbid-prop-types */
+/* eslint-disable no-param-reassign */
+import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import {
@@ -15,21 +19,21 @@ import {
ModalFooter,
} from 'reactstrap';
import moment from 'moment-timezone';
-import { isEmpty, isEqual, set } from 'lodash';
+import { isEmpty, isEqual } from 'lodash';
import { Editor } from '@tinymce/tinymce-react';
import { toast } from 'react-toastify';
import ReactTooltip from 'react-tooltip';
-import { postTimeEntry, editTimeEntry, getTimeEntriesForWeek } from '../../../actions/timeEntries';
import { getUserProfile } from 'actions/userProfile';
+import axios from 'axios';
+import hasPermission from 'utils/permissions';
+import { boxStyle, boxStyleDark } from 'styles';
+import { postTimeEntry, editTimeEntry, getTimeEntriesForWeek } from '../../../actions/timeEntries';
import AboutModal from './AboutModal';
import TangibleInfoModal from './TangibleInfoModal';
import ReminderModal from './ReminderModal';
import TimeLogConfirmationModal from './TimeLogConfirmationModal';
-import axios from 'axios';
import { ENDPOINTS } from '../../../utils/URL';
-import hasPermission from 'utils/permissions';
-import { boxStyle, boxStyleDark } from 'styles';
-import '../../Header/DarkMode.css'
+import '../../Header/DarkMode.css';
// Images are not allowed in timelog
const customImageUploadHandler = () =>
@@ -44,6 +48,7 @@ const TINY_MCE_INIT_OPTIONS = {
placeholder: 'Description (10-word minimum) and reference link',
plugins: 'advlist autolink autoresize lists link charmap table paste help wordcount',
toolbar:
+ // eslint-disable-next-line no-multi-str
'bold italic underline link removeformat | bullist numlist outdent indent |\
styleselect fontsizeselect | table| strikethrough forecolor backcolor |\
subscript superscript charmap | help',
@@ -74,36 +79,32 @@ const TINY_MCE_INIT_OPTIONS = {
* @returns
*/
-const TimeEntryForm = props => {
- /*---------------- variables -------------- */
+function TimeEntryForm(props) {
+ /* ---------------- variables -------------- */
// props from parent
- const { from, sendStop, edit, data, toggle, isOpen, tab, userProfile, darkMode } = props;
+ const { from, sendStop, edit, data, toggle, isOpen, tab, darkMode } = props;
// props from store
const { authUser } = props;
const viewingUser = JSON.parse(sessionStorage.getItem('viewingUser') ?? '{}');
- const initialFormValues = Object.assign(
- {
- dateOfWork: moment()
- .tz('America/Los_Angeles')
- .format('YYYY-MM-DD'),
- personId: viewingUser.userId ?? authUser.userid,
- projectId: '',
- wbsId: '',
- taskId: '',
- hours: 0,
- minutes: 0,
- notes: '',
- isTangible: from === 'Timer' ? true : false,
- entryType: 'default',
- },
- data,
- );
+ const initialFormValues = {
+ dateOfWork: moment()
+ .tz('America/Los_Angeles')
+ .format('YYYY-MM-DD'),
+ personId: viewingUser.userId ?? authUser.userid,
+ projectId: '',
+ wbsId: '',
+ taskId: '',
+ hours: 0,
+ minutes: 0,
+ notes: '',
+ isTangible: from === 'Timer',
+ entryType: 'default',
+ ...data,
+ };
- const timeEntryUserId = from === 'Timer'
- ? (viewingUser.userId ?? authUser.userid)
- : data.personId;
+ const timeEntryUserId = from === 'Timer' ? viewingUser.userId ?? authUser.userid : data.personId;
const {
dateOfWork: initialDateOfWork,
@@ -118,8 +119,8 @@ const TimeEntryForm = props => {
const timeEntryInitialProjectOrTaskId = edit
? initialProjectId +
- (!!initialwbsId ? '/' + initialwbsId : '') +
- (!!initialTaskId ? '/' + initialTaskId : '')
+ (initialwbsId ? `/${initialwbsId}` : '') +
+ (initialTaskId ? `/${initialTaskId}` : '')
: 'defaultProject';
const initialReminder = {
@@ -133,7 +134,6 @@ const TimeEntryForm = props => {
const [formValues, setFormValues] = useState(initialFormValues);
const [timeEntryFormUserProfile, setTimeEntryFormUserProfile] = useState(null);
const [timeEntryFormUserProjects, setTimeEntryFormUserProjects] = useState([]);
- const [timeEntryFormUserWBSs, setTimeEntryFormUserWBSs] = useState([]);
const [timeEntryFormUserTasks, setTimeEntryFormUserTasks] = useState([]);
const [projectOrTaskId, setProjectOrTaskId] = useState(timeEntryInitialProjectOrTaskId);
const [isAsyncDataLoaded, setIsAsyncDataLoaded] = useState(false);
@@ -163,17 +163,17 @@ const TimeEntryForm = props => {
const canChangeTime =
from !== 'Timer' && (from === 'TimeLog' || canEditTimeEntryTime || isSameDayAuthUserEdit);
- /*---------------- methods -------------- */
+ /* ---------------- methods -------------- */
const toggleRemainder = () =>
- setReminder(reminder => ({
- ...reminder,
- openModal: !reminder.openModal,
+ setReminder(r => ({
+ ...r,
+ openModal: !r.openModal,
}));
const cancelChange = () => {
setReminder(initialReminder);
- setFormValues(formValues => ({
- ...formValues,
+ setFormValues(fv => ({
+ ...fv,
hours: initialHours,
minutes: initialMinutes,
}));
@@ -194,18 +194,23 @@ const TimeEntryForm = props => {
const handleInputChange = event => {
event.persist();
- const target = event.target;
- switch (target.name) {
- case 'hours':
- if (+target.value < 0 || +target.value > 40) return;
- return setFormValues(formValues => ({ ...formValues, hours: +target.value }));
- case 'minutes':
- if (+target.value < 0 || +target.value > 59) return;
- return setFormValues(formValues => ({ ...formValues, minutes: +target.value }));
- case 'isTangible':
- return setFormValues(formValues => ({ ...formValues, isTangible: target.checked }));
- default:
- return setFormValues(formValues => ({ ...formValues, [target.name]: target.value }));
+ const { name, value, checked } = event.target;
+
+ const updateFormValues = (key, val) => {
+ setFormValues(fv => ({ ...fv, [key]: val }));
+ };
+
+ if (name === 'hours' || name === 'minutes') {
+ const numValue = +value;
+ const isValid =
+ name === 'hours' ? numValue >= 0 && numValue <= 40 : numValue >= 0 && numValue <= 59;
+ if (isValid) {
+ updateFormValues(name, numValue);
+ }
+ } else if (name === 'isTangible') {
+ updateFormValues(name, checked);
+ } else {
+ updateFormValues(name, value);
}
};
@@ -213,8 +218,8 @@ const TimeEntryForm = props => {
const optionValue = event.target.value;
const ids = optionValue.split('/');
const [projectId, wbsId, taskId] = ids.length > 1 ? ids : [ids[0], null, null];
- setFormValues(formValues => ({
- ...formValues,
+ setFormValues(fv => ({
+ ...fv,
projectId,
wbsId,
taskId,
@@ -226,15 +231,14 @@ const TimeEntryForm = props => {
const { wordcount } = editor.plugins;
const hasLink = content.indexOf('http://') > -1 || content.indexOf('https://') > -1;
const enoughWords = wordcount.body.getWordCount() > 10;
- setFormValues(formValues => ({ ...formValues, [editor.id]: content }));
- setReminder(reminder => ({
- ...reminder,
+ setFormValues(fv => ({ ...fv, [editor.id]: content }));
+ setReminder(r => ({
+ ...r,
enoughWords,
hasLink,
}));
};
-
const validateForm = isTimeModified => {
const errorObj = {};
const remindObj = { ...initialReminder };
@@ -295,63 +299,42 @@ const TimeEntryForm = props => {
if (closed === true && isOpen) toggle();
};
- const handleSubmit = async (event) => {
- if (event) {
- event.preventDefault();
- }
- setSubmitting(true);
-
- if (edit && isEqual(formValues, initialFormValues)) {
- toast.info(`Nothing is changed for this time entry`);
- setSubmitting(false);
- return;
- }
-
- if (!edit && !formValues.isTangible) {
- setTimelogConfirmationModalVisible(true);
- setSubmitting(false);
- return;
- }
-
- await submitTimeEntry();
- };
-
const submitTimeEntry = async () => {
const { hours: formHours, minutes: formMinutes } = formValues;
const timeEntry = { ...formValues };
const isTimeModified = edit && (initialHours !== formHours || initialMinutes !== formMinutes);
-
+
if (!validateForm(isTimeModified)) {
setSubmitting(false);
return;
}
-
- try {
- if (edit) {
- await props.editTimeEntry(data._id, timeEntry, initialDateOfWork);
- } else {
- await props.postTimeEntry(timeEntry);
- }
+ const handleFormReset = () => {
setFormValues(initialFormValues);
+ setReminder(initialReminder);
+ if (isOpen) toggle();
+ setSubmitting(false);
+ };
- //Clear the form and clean up.
+ const handleError = error => {
+ toast.error(`An error occurred while attempting to submit your time entry. Error: ${error}`);
+ setSubmitting(false);
+ };
+
+ const handlePostSubmitActions = async () => {
switch (from) {
- case 'Timer': // log time entry from Timer
+ case 'Timer':
sendStop();
clearForm();
break;
- case 'TimeLog': // add intangible time entry
+ case 'TimeLog': {
const date = moment(formValues.dateOfWork);
const today = moment().tz('America/Los_Angeles');
const offset = today.week() - date.week();
- if (offset < 3) {
- props.getTimeEntriesForWeek(timeEntryUserId, offset);
- } else {
- props.getTimeEntriesForWeek(timeEntryUserId, 3);
- }
+ props.getTimeEntriesForWeek(timeEntryUserId, Math.min(offset, 3));
clearForm();
break;
+ }
case 'WeeklyTab':
await Promise.all([
props.getUserProfile(timeEntryUserId),
@@ -363,26 +346,53 @@ const TimeEntryForm = props => {
}
if (from !== 'Timer' && !reminder.editLimitNotification) {
- setReminder(reminder => ({
- ...reminder,
- editLimitNotification: !reminder.editLimitNotification,
+ setReminder(r => ({
+ ...r,
+ editLimitNotification: !r.editLimitNotification,
}));
}
+ };
- setReminder(initialReminder);
- if (isOpen) toggle();
- setSubmitting(false);
+ try {
+ if (edit) {
+ await props.editTimeEntry(data._id, timeEntry, initialDateOfWork);
+ } else {
+ await props.postTimeEntry(timeEntry);
+ }
+
+ await handlePostSubmitActions();
+ handleFormReset();
} catch (error) {
- toast.error(`An error occurred while attempting to submit your time entry. Error: ${error}`);
+ handleError(error);
+ }
+ };
+
+ const handleSubmit = async event => {
+ if (event) {
+ event.preventDefault();
+ }
+ setSubmitting(true);
+
+ if (edit && isEqual(formValues, initialFormValues)) {
+ toast.info(`Nothing is changed for this time entry`);
setSubmitting(false);
- };
+ return;
+ }
+
+ if (!edit && !formValues.isTangible) {
+ setTimelogConfirmationModalVisible(true);
+ setSubmitting(false);
+ return;
+ }
+
+ await submitTimeEntry();
};
const handleTangibleTimelogConfirm = async () => {
setTimelogConfirmationModalVisible(false);
await submitTimeEntry();
};
-
+
const handleTangibleTimelogCancel = () => {
setTimelogConfirmationModalVisible(false);
};
@@ -399,80 +409,99 @@ const TimeEntryForm = props => {
const buildOptions = () => {
const projectsObject = {};
+
+ // Initialize default option
const options = [
Select Project/Task
,
];
- timeEntryFormUserProjects.forEach(project => {
- const { projectId } = project;
- project.WBSObject = {};
- projectsObject[projectId] = project;
- });
- timeEntryFormUserTasks.forEach(task => {
- const { projectId, wbsId, _id: taskId, wbsName, projectName } = task;
- if (!projectsObject[projectId]) {
- projectsObject[projectId] = {
- projectName,
- WBSObject: {
- [wbsId]: {
- wbsName,
- taskObject: {
- [taskId]: task,
+
+ // Build projectsObject with WBS and tasks
+ const buildProjectsObject = () => {
+ timeEntryFormUserProjects.forEach(project => {
+ const { projectId } = project;
+ project.WBSObject = {};
+ projectsObject[projectId] = project;
+ });
+
+ timeEntryFormUserTasks.forEach(task => {
+ const { projectId, wbsId, _id: taskId, wbsName, projectName } = task;
+ if (!projectsObject[projectId]) {
+ projectsObject[projectId] = {
+ projectName,
+ WBSObject: {
+ [wbsId]: {
+ wbsName,
+ taskObject: { [taskId]: task },
},
},
- },
- };
- } else if (!projectsObject[projectId].WBSObject[wbsId]) {
- projectsObject[projectId].WBSObject[wbsId] = {
- wbsName,
- taskObject: {
- [taskId]: task,
- },
- };
- } else {
- projectsObject[projectId].WBSObject[wbsId].taskObject[taskId] = task;
- }
- });
-
- for (const [projectId, project] of Object.entries(projectsObject)) {
- const { projectName, WBSObject } = project;
- options.push(
-
- {projectName}
- ,
- );
- for (const [wbsId, WBS] of Object.entries(WBSObject)) {
- const { wbsName, taskObject } = WBS;
- if (Object.keys(taskObject).length) {
- options.push(
-
- {`\u2003WBS: ${wbsName}`}
- ,
- );
- for (const [taskId, task] of Object.entries(taskObject)) {
- const { taskName } = task;
+ };
+ } else if (!projectsObject[projectId].WBSObject[wbsId]) {
+ projectsObject[projectId].WBSObject[wbsId] = {
+ wbsName,
+ taskObject: { [taskId]: task },
+ };
+ } else {
+ projectsObject[projectId].WBSObject[wbsId].taskObject[taskId] = task;
+ }
+ });
+ };
+
+ // Add options for tasks, WBS, and projects
+ const buildOptionsFromProjects = () => {
+ Object.entries(projectsObject).forEach(([projectId, project]) => {
+ const { projectName, WBSObject } = project;
+
+ // Add project option
+ options.push(
+
+ {projectName}
+ ,
+ );
+
+ Object.entries(WBSObject).forEach(([wbsId, WBS]) => {
+ const { wbsName, taskObject } = WBS;
+
+ // Add WBS option if it has tasks
+ if (Object.keys(taskObject).length) {
options.push(
-
- {`\u2003\u2003 ↳ ${taskName}`}
+
+ {`\u2003WBS: ${wbsName}`}
,
);
+
+ Object.entries(taskObject).forEach(([taskId, task]) => {
+ const { taskName } = task;
+
+ // Add task option
+ options.push(
+
+ {`\u2003\u2003 ↳ ${taskName}`}
+ ,
+ );
+ });
}
- }
- }
- }
+ });
+ });
+ };
+
+ // Build the projects object and options
+ buildProjectsObject();
+ buildOptionsFromProjects();
+
return options;
};
/**
* Rectify: This will run whenever TimeEntryForm is opened, since time entry data does not bound to store states (e.g., userProfile, userProjects, userTasks..)
* */
- const loadAsyncData = async timeEntryUserId => {
+ const loadAsyncData = async tuid => {
setIsAsyncDataLoaded(false);
try {
- const profileURL = ENDPOINTS.USER_PROFILE(timeEntryUserId);
- const projectURL = ENDPOINTS.USER_PROJECTS(timeEntryUserId);
- const taskURL = ENDPOINTS.TASKS_BY_USERID(timeEntryUserId);
+ const profileURL = ENDPOINTS.USER_PROFILE(tuid);
+ const projectURL = ENDPOINTS.USER_PROJECTS(tuid);
+ const taskURL = ENDPOINTS.TASKS_BY_USERID(tuid);
const profilePromise = axios.get(profileURL);
const projectPromise = axios.get(projectURL);
@@ -488,12 +517,11 @@ const TimeEntryForm = props => {
setTimeEntryFormUserTasks(userTasksRes.data);
setIsAsyncDataLoaded(true);
} catch (e) {
- console.log(e);
toast.error('An error occurred while loading the form data. Please try again later.');
}
};
- /*---------------- useEffects -------------- */
+ /* ---------------- useEffects -------------- */
useEffect(() => {
if (isAsyncDataLoaded) {
const options = buildOptions();
@@ -501,7 +529,7 @@ const TimeEntryForm = props => {
}
}, [isAsyncDataLoaded]);
- //grab form data before editing
+ // grab form data before editing
useEffect(() => {
if (isOpen) {
loadAsyncData(timeEntryUserId);
@@ -533,7 +561,8 @@ const TimeEntryForm = props => {
) : (
Intangible
)}
- Time Entry{viewingUser.userId ? ` for ${viewingUser.firstName} ${viewingUser.lastName} ` : ' '}
+ Time Entry
+ {viewingUser.userId ? ` for ${viewingUser.firstName} ${viewingUser.lastName} ` : ' '}
{
style={darkMode ? boxStyleDark : boxStyle}
disabled={submitting}
>
- {edit ? (submitting ? 'Saving...' : 'Save') : submitting ? 'Submitting...' : 'Submit'}
+ {(() => {
+ if (edit) {
+ return submitting ? 'Saving...' : 'Save';
+ }
+ return submitting ? 'Submitting...' : 'Submit';
+ })()}
@@ -711,7 +745,7 @@ const TimeEntryForm = props => {
cancelChange={cancelChange}
darkMode={darkMode}
/>
- {
/>
>
);
-};
+}
TimeEntryForm.propTypes = {
edit: PropTypes.bool.isRequired,
diff --git a/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx b/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx
index 385a49e0c5..574d891f35 100644
--- a/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx
+++ b/src/components/Timelog/TimeEntryForm/TimeLogConfirmationModal.jsx
@@ -1,43 +1,62 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
-function TimeLogConfirmationModal({ isOpen, toggleModal, onConfirm, onReject, onIntangible, darkMode }) {
- const [userChoice, setUserChoice] = useState('');
+function TimeLogConfirmationModal({
+ isOpen,
+ toggleModal,
+ onConfirm,
+ onReject,
+ onIntangible,
+ darkMode,
+}) {
+ const [, setUserChoice] = useState('');
- const handleUserAction = (choice) => {
- setUserChoice(choice);
- toggleModal();
+ const handleUserAction = choice => {
+ setUserChoice(choice);
+ toggleModal();
- // Call corresponding action based on user's choice
- if (choice === 'confirmed') {
- onConfirm();
- } else if (choice === 'rejected') {
- onReject();
- } else if (choice === 'intangible') {
- onIntangible();
- }
- };
+ // Call corresponding action based on user's choice
+ if (choice === 'confirmed') {
+ onConfirm();
+ } else if (choice === 'rejected') {
+ onReject();
+ } else if (choice === 'intangible') {
+ onIntangible();
+ }
+ };
- return (
-
- HOLD ON TIGER!
-
- If you are logging Intangible Time that you want converted to Tangible, you must include the reason why you didn’t use the required timer.
-
-
- handleUserAction('confirmed')} style={{ flex: 1, marginRight: '10px' }}>
- Yep, totally did that! Please proceed.
-
- handleUserAction('rejected')} style={{ flex: 1, marginRight: '10px' }}>
- Oops, didn’t do that! Take me back.
-
- handleUserAction('intangible')} style={{ flex: 1, marginRight: '10px' }}>
- Time is meant as Intangible. Please proceed.
-
-
-
- );
+ return (
+
+ HOLD ON TIGER!
+
+ If you are logging Intangible Time that you want converted to Tangible, you must include the
+ reason why you didn’t use the required timer.
+
+
+ handleUserAction('confirmed')}
+ style={{ flex: 1, marginRight: '10px' }}
+ >
+ Yep, totally did that! Please proceed.
+
+ handleUserAction('rejected')}
+ style={{ flex: 1, marginRight: '10px' }}
+ >
+ Oops, didn’t do that! Take me back.
+
+ handleUserAction('intangible')}
+ style={{ flex: 1, marginRight: '10px' }}
+ >
+ Time is meant as Intangible. Please proceed.
+
+
+
+ );
}
export default TimeLogConfirmationModal;
-
diff --git a/src/components/Timelog/Timelog.jsx b/src/components/Timelog/Timelog.jsx
index 3f4c421c88..08ee8cfa7f 100644
--- a/src/components/Timelog/Timelog.jsx
+++ b/src/components/Timelog/Timelog.jsx
@@ -1,4 +1,7 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-alert */
+/* eslint-disable no-console */
+import { useState, useEffect } from 'react';
import { useParams, useLocation } from 'react-router-dom';
import {
Container,
@@ -32,6 +35,17 @@ import ReactTooltip from 'react-tooltip';
import ActiveCell from 'components/UserManagement/ActiveCell';
import { ProfileNavDot } from 'components/UserManagement/ProfileNavDot';
import TeamMemberTasks from 'components/TeamMemberTasks';
+import { boxStyle, boxStyleDark } from 'styles';
+import { formatDate } from 'utils/formatDate';
+import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal';
+import { cantUpdateDevAdminDetails } from 'utils/permissions';
+import axios from 'axios';
+import {
+ DEV_ADMIN_ACCOUNT_EMAIL_DEV_ENV_ONLY,
+ DEV_ADMIN_ACCOUNT_CUSTOM_WARNING_MESSAGE_DEV_ENV_ONLY,
+ PROTECTED_ACCOUNT_MODIFICATION_WARNING_MESSAGE,
+} from 'utils/constants';
+import PropTypes from 'prop-types';
import { getTimeEntriesForWeek, getTimeEntriesForPeriod } from '../../actions/timeEntries';
import { getUserProfile, updateUserProfile, getUserTasks } from '../../actions/userProfile';
import { getUserProjects } from '../../actions/userProjects';
@@ -45,17 +59,6 @@ import WeeklySummary from '../WeeklySummary/WeeklySummary';
import LoadingSkeleton from '../common/SkeletonLoading';
import hasPermission from '../../utils/permissions';
import WeeklySummaries from './WeeklySummaries';
-import { boxStyle, boxStyleDark } from 'styles';
-import { formatDate } from 'utils/formatDate';
-import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal';
-import { cantUpdateDevAdminDetails } from 'utils/permissions';
-import axios from 'axios';
-import {
- DEV_ADMIN_ACCOUNT_EMAIL_DEV_ENV_ONLY,
- DEV_ADMIN_ACCOUNT_CUSTOM_WARNING_MESSAGE_DEV_ENV_ONLY,
- PROTECTED_ACCOUNT_MODIFICATION_WARNING_MESSAGE,
-} from 'utils/constants';
-import PropTypes from 'prop-types';
import Badge from '../Badge';
import { ENDPOINTS } from '../../utils/URL';
@@ -79,7 +82,7 @@ const endOfWeek = offset => {
.format('YYYY-MM-DD');
};
-const Timelog = props => {
+function Timelog(props) {
const darkMode = useSelector(state => state.theme.darkMode);
const location = useLocation();
@@ -129,25 +132,14 @@ const Timelog = props => {
const { userId: urlId } = useParams();
const [userprofileId, setUserProfileId] = useState(urlId || authUser.userid);
- const doesUserHaveTaskWithWBS = userHaveTask => {
- return userHaveTask.reduce((acc, item) => {
- const hasIncompleteTask = item.resources.some(
- val =>
- (viewingUser.userId === val.userID || val.userID === userprofileId) &&
- val.completedTask === false,
- );
- if (hasIncompleteTask) acc.push(item);
- return acc;
- }, []);
- };
-
const checkSessionStorage = () => JSON.parse(sessionStorage.getItem('viewingUser')) ?? false;
const [viewingUser, setViewingUser] = useState(checkSessionStorage());
const getUserId = () => {
try {
if (viewingUser) {
return viewingUser.userId;
- } else if (userId != null) {
+ }
+ if (userId != null) {
return userId;
}
return authUser.userid;
@@ -156,28 +148,53 @@ const Timelog = props => {
}
};
+ const doesUserHaveTaskWithWBS = userHaveTask => {
+ return userHaveTask.reduce((acc, item) => {
+ const hasIncompleteTask = item.resources.some(
+ val =>
+ (viewingUser.userId === val.userID || val.userID === userprofileId) &&
+ val.completedTask === false,
+ );
+ if (hasIncompleteTask) acc.push(item);
+ return acc;
+ }, []);
+ };
+
const [displayUserId, setDisplayUserId] = useState(getUserId());
const isAuthUser = authUser.userid === displayUserId;
const fullName = `${displayUserProfile.firstName} ${displayUserProfile.lastName}`;
+ const tabMapping = {
+ '#tasks': 0,
+ '#currentWeek': 1,
+ '#lastWeek': 2,
+ '#beforeLastWeek': 3,
+ '#dateRange': 4,
+ '#weeklySummaries': 5,
+ '#badgesearned': 6,
+ };
+
const defaultTab = data => {
const userHaveTask = doesUserHaveTaskWithWBS(data);
- //change default to time log tab(1) in the following cases:
- const role = authUser.role;
+ // change default to time log tab(1) in the following cases:
+ const { role } = authUser;
let tab = 0;
/* To set the Task tab as defatult this.userTask is being watched.
Accounts with no tasks assigned to it return an empty array.
Accounts assigned with tasks with no wbs return and empty array.
Accounts assigned with tasks with wbs return an array with that wbs data.
The problem: even after unassigning tasks the array keeps the wbs data.
- That breaks this feature. Necessary to check if this array should keep data or be reset when unassinging tasks.*/
-
- //if user role is volunteer or core team and they don't have tasks assigned, then default tab is timelog.
- role === 'Volunteer' && userHaveTask.length > 0
- ? (tab = 0)
- : role === 'Volunteer' && userHaveTask.length === 0
- ? (tab = 1)
- : null;
+ That breaks this feature. Necessary to check if this array should keep data or be reset when unassinging tasks. */
+
+ // if user role is volunteer or core team and they don't have tasks assigned, then default tab is timelog.
+ if (role === 'Volunteer' && userHaveTask.length > 0) {
+ tab = 0;
+ } else if (role === 'Volunteer' && userHaveTask.length === 0) {
+ tab = 1;
+ } else {
+ tab = null;
+ }
+
// Sets active tab to "Current Week Timelog" when the Progress bar in Leaderboard is clicked
if (!props.isDashboard) {
tab = 1;
@@ -192,42 +209,11 @@ const Timelog = props => {
return tab;
};
- const tabMapping = {
- '#tasks': 0,
- '#currentWeek': 1,
- '#lastWeek': 2,
- '#beforeLastWeek': 3,
- '#dateRange': 4,
- '#weeklySummaries': 5,
- '#badgesearned': 6,
- };
-
- useEffect(() => {
- const tab = tabMapping[location.hash];
- if (tab !== undefined) {
- changeTab(tab);
- }
- }, [location.hash]); // This effect will run whenever the hash changes
-
- /*---------------- methods -------------- */
- const updateTimeEntryItems = () => {
- const allTimeEntryItems = generateAllTimeEntryItems();
- setCurrentWeekEntries(allTimeEntryItems[0]);
- setLastWeekEntries(allTimeEntryItems[1]);
- setBeforeLastEntries(allTimeEntryItems[2]);
- setPeriodEntries(allTimeEntryItems[3]);
- };
-
- const generateAllTimeEntryItems = () => {
- const currentWeekEntries = generateTimeEntries(timeEntries.weeks[0], 0);
- const lastWeekEntries = generateTimeEntries(timeEntries.weeks[1], 1);
- const beforeLastEntries = generateTimeEntries(timeEntries.weeks[2], 2);
- const periodEntries = generateTimeEntries(timeEntries.period, 3);
- return [currentWeekEntries, lastWeekEntries, beforeLastEntries, periodEntries];
- };
+ /* ---------------- methods -------------- */
const generateTimeEntries = (data, tab) => {
if (!timeLogState.projectsSelected.includes('all')) {
+ // eslint-disable-next-line no-param-reassign
data = data.filter(
entry =>
timeLogState.projectsSelected.includes(entry.projectId) ||
@@ -255,22 +241,38 @@ const Timelog = props => {
));
};
- const loadAsyncData = async userId => {
- //load the timelog data
+ const generateAllTimeEntryItems = () => {
+ const currentWeekEntry = generateTimeEntries(timeEntries.weeks[0], 0);
+ const lastWeekEntry = generateTimeEntries(timeEntries.weeks[1], 1);
+ const beforeLastEntry = generateTimeEntries(timeEntries.weeks[2], 2);
+ const periodEntry = generateTimeEntries(timeEntries.period, 3);
+ return [currentWeekEntry, lastWeekEntry, beforeLastEntry, periodEntry];
+ };
+
+ const updateTimeEntryItems = () => {
+ const allTimeEntryItems = generateAllTimeEntryItems();
+ setCurrentWeekEntries(allTimeEntryItems[0]);
+ setLastWeekEntries(allTimeEntryItems[1]);
+ setBeforeLastEntries(allTimeEntryItems[2]);
+ setPeriodEntries(allTimeEntryItems[3]);
+ };
+
+ const loadAsyncData = async uid => {
+ // load the timelog data
setTimeLogState({ ...timeLogState, isTimeEntriesLoading: true });
try {
await Promise.all([
- props.getUserProfile(userId),
- props.getTimeEntriesForWeek(userId, 0),
- props.getTimeEntriesForWeek(userId, 1),
- props.getTimeEntriesForWeek(userId, 2),
- props.getTimeEntriesForPeriod(userId, timeLogState.fromDate, timeLogState.toDate),
+ props.getUserProfile(uid),
+ props.getTimeEntriesForWeek(uid, 0),
+ props.getTimeEntriesForWeek(uid, 1),
+ props.getTimeEntriesForWeek(uid, 2),
+ props.getTimeEntriesForPeriod(uid, timeLogState.fromDate, timeLogState.toDate),
props.getAllRoles(),
- props.getUserProjects(userId),
- props.getUserTasks(userId),
+ props.getUserProjects(uid),
+ props.getUserTasks(uid),
]);
- const url = ENDPOINTS.TASKS_BY_USERID(userId);
+ const url = ENDPOINTS.TASKS_BY_USERID(uid);
const res = await axios.get(url);
const data = res.data.length > 0 ? res.data : [];
@@ -294,8 +296,8 @@ const Timelog = props => {
setTimeLogState({ ...timeLogState, timeEntryFormModal: !timeLogState.timeEntryFormModal });
};
- const showSummary = isAuthUser => {
- if (isAuthUser) {
+ const showSummary = isAuth => {
+ if (isAuth) {
setTimeLogState({ ...timeLogState, summary: true });
setTimeout(() => {
const elem = document.getElementById('weeklySum');
@@ -323,7 +325,7 @@ const Timelog = props => {
setTimeLogState({
...timeLogState,
infoModal: !timeLogState.infoModal,
- information: str.split('\n').map((item, i) => {item}
),
+ information: str.split('\n').map(item => {item}
),
});
};
@@ -343,12 +345,19 @@ const Timelog = props => {
});
};
+ useEffect(() => {
+ const tab = tabMapping[location.hash];
+ if (tab !== undefined) {
+ changeTab(tab);
+ }
+ }, [location.hash]); // This effect will run whenever the hash changes
+
const handleInputChange = e => {
setTimeLogState({ ...timeLogState, [e.target.name]: e.target.value });
};
const handleSearch = e => {
- //check if the toDate is before the fromDate
+ // check if the toDate is before the fromDate
if (moment(timeLogState.fromDate).isAfter(moment(timeLogState.toDate))) {
alert('Invalid Date Range: the From Date must be before the To Date');
} else {
@@ -369,32 +378,32 @@ const Timelog = props => {
timeLogState.activeTab === 5 ||
timeLogState.activeTab === 6
) {
- return <>>;
- } else if (timeLogState.activeTab === 4) {
+ return null;
+ }
+ if (timeLogState.activeTab === 4) {
return (
Viewing time Entries from {formatDate(timeLogState.fromDate)} to{' '}
{formatDate(timeLogState.toDate)}
);
- } else {
- return (
-
- Viewing time Entries from {formatDate(startOfWeek(timeLogState.activeTab - 1))} to{' '}
- {formatDate(endOfWeek(timeLogState.activeTab - 1))}
-
- );
}
+ return (
+
+ Viewing time Entries from {formatDate(startOfWeek(timeLogState.activeTab - 1))} to{' '}
+ {formatDate(endOfWeek(timeLogState.activeTab - 1))}
+
+ );
};
- const makeBarData = userId => {
- //pass the data to summary bar
+ const makeBarData = uid => {
+ // pass the data to summary bar
const weekEffort = calculateTotalTime(timeEntries.weeks[0], true);
setTimeLogState({ ...timeLogState, currentWeekEffort: weekEffort });
if (props.isDashboard) {
- props.passSummaryBarData({ personId: userId, tangibletime: weekEffort });
+ props.passSummaryBarData({ personId: uid, tangibletime: weekEffort });
} else {
- setSummaryBarData({ personId: userId, tangibletime: weekEffort });
+ setSummaryBarData({ personId: uid, tangibletime: weekEffort });
}
};
@@ -405,11 +414,16 @@ const Timelog = props => {
Select Project/Task (all)
,
];
+
+ // Build the projectsObject structure
displayUserProjects.forEach(project => {
const { projectId } = project;
- project.WBSObject = {};
- projectsObject[projectId] = project;
+ projectsObject[projectId] = {
+ ...project,
+ WBSObject: {},
+ };
});
+
disPlayUserTasks.forEach(task => {
const { projectId, wbsId, _id: taskId, wbsName, projectName } = task;
if (!projectsObject[projectId]) {
@@ -418,33 +432,35 @@ const Timelog = props => {
WBSObject: {
[wbsId]: {
wbsName,
- taskObject: {
- [taskId]: task,
- },
+ taskObject: { [taskId]: task },
},
},
};
} else if (!projectsObject[projectId].WBSObject[wbsId]) {
projectsObject[projectId].WBSObject[wbsId] = {
wbsName,
- taskObject: {
- [taskId]: task,
- },
+ taskObject: { [taskId]: task },
};
} else {
projectsObject[projectId].WBSObject[wbsId].taskObject[taskId] = task;
}
});
- for (const [projectId, project] of Object.entries(projectsObject)) {
+ // Convert projectsObject to options
+ Object.entries(projectsObject).forEach(([projectId, project]) => {
const { projectName, WBSObject } = project;
+
+ // Add project option
options.push(
{projectName}
,
);
- for (const [wbsId, WBS] of Object.entries(WBSObject)) {
+
+ Object.entries(WBSObject).forEach(([wbsId, WBS]) => {
const { wbsName, taskObject } = WBS;
+
+ // Add WBS option
options.push(
{
{`\u2003WBS: ${wbsName}`}
,
);
- for (const [taskId, task] of Object.entries(taskObject)) {
+
+ Object.entries(taskObject).forEach(([taskId, task]) => {
const { taskName } = task;
+
+ // Add task option
options.push(
{`\u2003\u2003 ↳ ${taskName}`}
,
);
- }
- }
- }
+ });
+ });
+ });
+
return options;
};
- const generateTimeLogItems = userId => {
- //build the time log component
+ const generateTimeLogItems = uid => {
+ // build the time log component
const options = buildOptions();
setProjectOrTaskOptions(options);
updateTimeEntryItems();
- makeBarData(userId);
+ makeBarData(uid);
};
- const handleUpdateTask = useCallback(() => {
- setShouldFetchData(true);
- }, []);
-
const handleStorageEvent = () => {
const sessionStorageData = checkSessionStorage();
setViewingUser(sessionStorageData || false);
- if (sessionStorageData && sessionStorageData.userId != authUser.userId) {
+ if (sessionStorageData && sessionStorageData.userId !== authUser.userId) {
setDisplayUserId(sessionStorageData.userId);
}
};
- /*---------------- useEffects -------------- */
+ /* ---------------- useEffects -------------- */
// Update user ID if it changes in the URL
useEffect(() => {
@@ -536,10 +552,17 @@ const Timelog = props => {
};
}, []);
+ const containerStyle = () => {
+ if (darkMode) {
+ return props.isDashboard ? {} : { padding: '0 15px 300px 15px' };
+ }
+ return {};
+ };
+
return (
{!props.isDashboard ? (
@@ -553,14 +576,12 @@ const Timelog = props => {
) : (
- {props.isDashboard ? (
- <>>
- ) : (
+ {props.isDashboard ? null : (
@@ -604,7 +625,7 @@ const Timelog = props => {
areaName="TasksAndTimelogInfoPoint"
areaTitle="Tasks and Timelogs"
fontSize={24}
- isPermissionPage={true}
+ isPermissionPage
role={authUser.role} // Pass the 'role' prop to EditableInfoModal
darkMode={darkMode}
/>
@@ -906,14 +927,12 @@ const Timelog = props => {
)}
{timeLogState.activeTab === 0 ||
timeLogState.activeTab === 5 ||
- timeLogState.activeTab === 6 ? (
- <>>
- ) : (
+ timeLogState.activeTab === 6 ? null : (
);
-};
+}
Timelog.prototype = {
userId: PropTypes.string,
diff --git a/src/components/Timelog/TimelogNavbar.jsx b/src/components/Timelog/TimelogNavbar.jsx
index 3535fcc3ab..e9f6b5c0b4 100644
--- a/src/components/Timelog/TimelogNavbar.jsx
+++ b/src/components/Timelog/TimelogNavbar.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Progress,
@@ -13,14 +13,15 @@ import {
import { useSelector } from 'react-redux';
import { getProgressColor, getProgressValue } from '../../utils/effortColors';
-const TimelogNavbar = ({ userId }) => {
+function TimelogNavbar({ userId }) {
const { firstName, lastName } = useSelector(state => state.userProfile);
const [collapsed, setCollapsed] = useState(true);
const toggleNavbar = () => setCollapsed(!collapsed);
const timeEntries = useSelector(state => state.timeEntries.weeks[0]);
- const reducer = (total, entry) => total + parseInt(entry.hours) + parseInt(entry.minutes) / 60;
+ const reducer = (total, entry) =>
+ total + parseInt(entry.hours, 10) + parseInt(entry.minutes, 10) / 60;
const totalEffort = timeEntries.reduce(reducer, 0);
const weeklycommittedHours = useSelector(state => state.userProfile.weeklycommittedHours);
@@ -79,6 +80,6 @@ const TimelogNavbar = ({ userId }) => {
);
-};
+}
export default TimelogNavbar;
diff --git a/src/components/Timelog/WeeklySummaries.jsx b/src/components/Timelog/WeeklySummaries.jsx
index 982fadd1b3..b002cbe552 100644
--- a/src/components/Timelog/WeeklySummaries.jsx
+++ b/src/components/Timelog/WeeklySummaries.jsx
@@ -1,29 +1,18 @@
-import React, { useState, useEffect} from 'react';
+import { useState, useEffect } from 'react';
import parse from 'html-react-parser';
-import './Timelog.css'
-import {updateWeeklySummaries} from '../../actions/weeklySummaries';
+import './Timelog.css';
import { getUserProfile, updateUserProfile } from 'actions/userProfile';
import hasPermission from 'utils/permissions';
-import { connect, useDispatch, useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { Editor } from '@tinymce/tinymce-react';
-import { userProfileByIdReducer } from 'reducers/userProfileByIdReducer';
import Spinner from 'react-bootstrap/Spinner';
+import { updateWeeklySummaries } from '../../actions/weeklySummaries';
-const WeeklySummaries = ({ userProfile }) => {
-
- useEffect(() => {
- setEditedSummaries([
- userProfile.weeklySummaries[0]?.summary || '',
- userProfile.weeklySummaries[1]?.summary || '',
- userProfile.weeklySummaries[2]?.summary || '',
- ]);
-
- }, [userProfile]);
-
- const darkMode = useSelector(state => state.theme.darkMode)
+function WeeklySummaries({ userProfile }) {
+ const darkMode = useSelector(state => state.theme.darkMode);
// Initialize state variables for editing and original summaries
-
+
const [editing, setEditing] = useState([false, false, false]);
const [editedSummaries, setEditedSummaries] = useState([
@@ -31,7 +20,6 @@ const WeeklySummaries = ({ userProfile }) => {
userProfile.weeklySummaries[1]?.summary || '',
userProfile.weeklySummaries[2]?.summary || '',
]);
- const [originalSummaries, setOriginalSummaries] = useState([...editedSummaries]);
const [LoadingHandleSave, setLoadingHandleSave] = useState(null);
@@ -40,6 +28,14 @@ const WeeklySummaries = ({ userProfile }) => {
const dispatch = useDispatch();
const canEdit = dispatch(hasPermission('putUserProfile'));
+ useEffect(() => {
+ setEditedSummaries([
+ userProfile.weeklySummaries[0]?.summary || '',
+ userProfile.weeklySummaries[1]?.summary || '',
+ userProfile.weeklySummaries[2]?.summary || '',
+ ]);
+ }, [userProfile]);
+
const currentUserID = userProfile._id;
const { user } = useSelector(state => state.auth);
const loggedInUserId = user.userid;
@@ -48,58 +44,57 @@ const WeeklySummaries = ({ userProfile }) => {
return
@@ -164,28 +173,27 @@ const WeeklySummaries = ({ userProfile }) => {
{parse(editedSummaries[index])}
);
- } else {
- // Display a message when there's no summary
- return (
-
{renderSummary("This week's summary", userProfile.weeklySummaries[0]?.summary, 0)}
{renderSummary("Last week's summary", userProfile.weeklySummaries[1]?.summary, 1)}
- {renderSummary("The week before last's summary",userProfile.weeklySummaries[2]?.summary,2)}
+ {renderSummary("The week before last's summary", userProfile.weeklySummaries[2]?.summary, 2)}
);
-};
+}
// const mapStateToProps = state => state;
// export default connect(mapStateToProps, { hasPermission })(WeeklySummaries);
-export default WeeklySummaries;
\ No newline at end of file
+export default WeeklySummaries;
diff --git a/src/components/Timelog/index.js b/src/components/Timelog/index.js
index ea234d0999..8fcf6cdc69 100644
--- a/src/components/Timelog/index.js
+++ b/src/components/Timelog/index.js
@@ -1 +1,3 @@
-export { default } from './Timelog';
+import Timelog from './Timelog';
+
+export default Timelog;
diff --git a/src/components/Timer/__test__/Countdown.test.js b/src/components/Timer/__test__/Countdown.test.js
new file mode 100644
index 0000000000..202a04546c
--- /dev/null
+++ b/src/components/Timer/__test__/Countdown.test.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import Countdown from '../Countdown';
+
+describe('Countdown Component', () => {
+ const defaultProps = {
+ message: {
+ started: false,
+ goal: 3600000, // 1 hour in milliseconds
+ initialGoal: 3600000,
+ },
+ timerRange: {
+ MAX_HOURS: 24,
+ MIN_MINS: 1,
+ },
+ running: false,
+ wsMessageHandler: {
+ sendPause: jest.fn(),
+ sendSetGoal: jest.fn(),
+ },
+ remaining: 1800000, // 30 minutes in milliseconds
+ setConfirmationResetModal: jest.fn(),
+ checkBtnAvail: jest.fn(() => true),
+ handleStartButton: jest.fn(),
+ handleAddButton: jest.fn(),
+ handleSubtractButton: jest.fn(),
+ handleStopButton: jest.fn(),
+ toggleTimer: jest.fn(),
+ };
+
+ it('renders the countdown component with the correct initial values', () => {
+ render(
{EMAIL}
diff --git a/src/components/UserManagement/UserTableSearchHeader.jsx b/src/components/UserManagement/UserTableSearchHeader.jsx
index 8d6679c535..5c77e54993 100644
--- a/src/components/UserManagement/UserTableSearchHeader.jsx
+++ b/src/components/UserManagement/UserTableSearchHeader.jsx
@@ -21,6 +21,10 @@ const UserTableSearchHeader = React.memo(props => {
props.onRoleSearch(text);
};
+ const onTitleSearch = text => {
+ props.onTitleSearch(text);
+ };
+
const onEmailSearch = text => {
props.onEmailSearch(text);
};
@@ -51,6 +55,14 @@ const UserTableSearchHeader = React.memo(props => {
+
+
+
{
let onFirstNameSearch;
let onLastNameSearch;
let onRoleSearch;
+ let onTitleSearch;
let onEmailSearch;
let onWeeklyHrsSearch;
beforeEach(() => {
onFirstNameSearch = jest.fn();
onLastNameSearch = jest.fn();
onRoleSearch = jest.fn();
+ onTitleSearch = jest.fn();
onEmailSearch = jest.fn();
onWeeklyHrsSearch = jest.fn();
render(
@@ -22,6 +24,7 @@ describe('user table search header row', () => {
onFirstNameSearch={onFirstNameSearch}
onLastNameSearch={onLastNameSearch}
onRoleSearch={onRoleSearch}
+ onTitleSearch={onTitleSearch}
onEmailSearch={onEmailSearch}
onWeeklyHrsSearch={onWeeklyHrsSearch}
roles={['1', '2', '3', '4', 'Volunteer', 'Owner', 'Manager']}
@@ -35,7 +38,7 @@ describe('user table search header row', () => {
expect(screen.getByRole('row')).toBeInTheDocument();
});
it('should render 4 text field', () => {
- expect(screen.getAllByRole('textbox')).toHaveLength(4);
+ expect(screen.getAllByRole('textbox')).toHaveLength(5);
});
it('should render one dropdown box', () => {
expect(screen.getByRole('combobox')).toBeInTheDocument();
@@ -70,22 +73,26 @@ describe('user table search header row', () => {
expect(onRoleSearch).toHaveBeenCalled();
expect(onRoleSearch).toHaveBeenCalledWith('Manager');
});
- it('should fire Email search once the user type something in the email search box', async () => {
+ it('should fire Title search once the user type something in the title search box', async () => {
await userEvent.type(screen.getAllByRole('textbox')[2], 'test', { allAtOnce: false });
+ expect(onTitleSearch).toHaveBeenCalledTimes(4);
+ });
+ it('should fire Email search once the user type something in the email search box', async () => {
+ await userEvent.type(screen.getAllByRole('textbox')[3], 'test', { allAtOnce: false });
expect(onEmailSearch).toHaveBeenCalledTimes(4);
});
it('should fire Email search once the user type something in the email search box', async () => {
- await userEvent.type(screen.getAllByRole('textbox')[2], 'Jhon.wick@email.com', {
+ await userEvent.type(screen.getAllByRole('textbox')[3], 'Jhon.wick@email.com', {
allAtOnce: true,
});
expect(onEmailSearch).toHaveBeenCalled();
});
it('should fire onWeeklyHrsSearch once the user type something in the weeklycommitted hrs search box', async () => {
- await userEvent.type(screen.getAllByRole('textbox')[3], 'test', { allAtOnce: false });
+ await userEvent.type(screen.getAllByRole('textbox')[4], 'test', { allAtOnce: false });
expect(onWeeklyHrsSearch).toHaveBeenCalledTimes(4);
});
it('should fire onWeeklyHrsSearch once the user type something in the weeklycommitted hrs search box', async () => {
- await userEvent.type(screen.getAllByRole('textbox')[3], '15', { allAtOnce: true });
+ await userEvent.type(screen.getAllByRole('textbox')[4], '15', { allAtOnce: true });
expect(onWeeklyHrsSearch).toHaveBeenCalled();
});
});
diff --git a/src/components/UserManagement/usermanagement.css b/src/components/UserManagement/usermanagement.css
index b2acccfe17..c15348f1ec 100644
--- a/src/components/UserManagement/usermanagement.css
+++ b/src/components/UserManagement/usermanagement.css
@@ -89,6 +89,16 @@ thead {
}
+#usermanagement_role,
+#user_role,
+td#usermanagement_role {
+ width: 100px;
+ max-width: 180px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.copy_icon {
cursor: pointer;
position: absolute;
diff --git a/src/components/UserProfile/Badges.jsx b/src/components/UserProfile/Badges.jsx
index a8381b981c..2cce7ca814 100644
--- a/src/components/UserProfile/Badges.jsx
+++ b/src/components/UserProfile/Badges.jsx
@@ -63,29 +63,37 @@ export const Badges = (props) => {
useEffect(() => {
try {
if (props.userProfile.badgeCollection && props.userProfile.badgeCollection.length) {
- const sortBadges = [...props.userProfile.badgeCollection].sort((a, b) => {
- if (a?.badge?.ranking === 0) return 1;
- if (b?.badge?.ranking === 0) return -1;
- if (a?.badge?.ranking > b?.badge?.ranking) return 1;
- if (a?.badge?.ranking < b?.badge?.ranking) return -1;
- if (a?.badge?.badgeName > b?.badge?.badgeName) return 1;
- if (a?.badge?.badgeName < b?.badge?.badgeName) return -1;
- return 0;
- });
+ const sortBadges = [...props.userProfile.badgeCollection]
+ .filter(badge => badge && badge.badge) // Filter out any null or undefined badges
+ .sort((a, b) => {
+ const rankingA = a.badge?.ranking ?? Infinity;
+ const rankingB = b.badge?.ranking ?? Infinity;
+ const nameA = a.badge?.badgeName ?? '';
+ const nameB = b.badge?.badgeName ?? '';
+
+ if (rankingA === 0) return 1;
+ if (rankingB === 0) return -1;
+ if (rankingA > rankingB) return 1;
+ if (rankingA < rankingB) return -1;
+ return nameA.localeCompare(nameB);
+ });
setSortedBadges(sortBadges);
+ } else {
+ setSortedBadges([]);
}
} catch (error) {
- console.log(error);
+ console.error("Error sorting badges:", error);
+ setSortedBadges([]);
}
-
}, [props.userProfile.badgeCollection]);
// Determines what congratulatory text should displayed.
const badgesEarned = props.userProfile.badgeCollection.reduce((acc, badge) => {
- if (badge?.badge?.badgeName === 'Personal Max' || badge?.badge?.type === 'Personal Max') {
+ if (!badge || !badge.badge) return acc;
+ if (badge.badge.badgeName === 'Personal Max' || badge.badge.type === 'Personal Max') {
return acc + 1;
}
- return acc + Math.round(Number(badge.count));
+ return acc + (Math.round(Number(badge.count)) || 0);
}, 0);
const subject = props.isUserSelf ? 'You have' : 'This person has';
@@ -225,7 +233,7 @@ export const Badges = (props) => {
{props.userProfile.badgeCollection && props.userProfile.badgeCollection.length>0 ? (
sortedBadges &&
- sortedBadges.map(value => value &&(
+ sortedBadges.map(value => value && value.badge &&(
{' '}
diff --git a/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx b/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx
index 5d2322a836..c84a525432 100644
--- a/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx
+++ b/src/components/UserProfile/FeaturedBadges/FeaturedBadges.jsx
@@ -2,22 +2,31 @@ import React, { useEffect, useState } from 'react';
import BadgeImage from '../BadgeImage';
const FeaturedBadges = props => {
- let [filteredBadges, setFilteredBadges] = useState([]);
+ const [filteredBadges, setFilteredBadges] = useState([]);
+
const filterBadges = allBadges => {
- let filteredList = allBadges || [];
-
- filteredList = filteredList.sort((a, b) => {
- if (a.featured > b.featured) return -1;
- if (a.featured < b.featured) return 1;
- if (a.badge.ranking > b.badge.ranking) return 1;
- if (a.badge.ranking < b.badge.ranking) return -1;
- if (a.badge.badgeName > b.badge.badgeName) return 1;
- if (a.badge.badgeName < b.badge.badgeName) return -1;
- return 0;
+ if (!Array.isArray(allBadges)) return [];
+
+ let filteredList = allBadges.filter(badge => badge && badge.badge);
+
+ filteredList.sort((a, b) => {
+ const featuredA = a.featured ?? false;
+ const featuredB = b.featured ?? false;
+ const rankingA = a.badge?.ranking ?? 0;
+ const rankingB = b.badge?.ranking ?? 0;
+ const nameA = a.badge?.badgeName ?? '';
+ const nameB = b.badge?.badgeName ?? '';
+
+ if (featuredA > featuredB) return -1;
+ if (featuredA < featuredB) return 1;
+ if (rankingA > rankingB) return 1;
+ if (rankingA < rankingB) return -1;
+ return nameA.localeCompare(nameB);
});
return filteredList.slice(0, 5);
};
+
useEffect(() => {
setFilteredBadges(filterBadges(props.badges));
}, [props.badges]);
@@ -25,10 +34,16 @@ const FeaturedBadges = props => {
return (
{filteredBadges.map((value, index) => (
-
+
))}
);
};
-export default FeaturedBadges;
+export default FeaturedBadges;
\ No newline at end of file
diff --git a/src/components/UserProfile/UserProfile.jsx b/src/components/UserProfile/UserProfile.jsx
index fbcc082160..c99db62d23 100644
--- a/src/components/UserProfile/UserProfile.jsx
+++ b/src/components/UserProfile/UserProfile.jsx
@@ -1055,7 +1055,7 @@ function UserProfile(props) {
>
{showSelect ? 'Hide Team Weekly Summaries' : 'Show Team Weekly Summaries'}
- {canGetProjectMembers && teams.length !== 0 ? (
+ {(canGetProjectMembers && teams.length !== 0) || ['Owner','Administrator','Manager'].includes(requestorRole) ? (
{
navigator.clipboard.writeText(summaryIntro);
diff --git a/src/components/UserProfile/__tests__/Badges.test.jsx b/src/components/UserProfile/__tests__/Badges.test.jsx
index f131350548..8cee0128ea 100644
--- a/src/components/UserProfile/__tests__/Badges.test.jsx
+++ b/src/components/UserProfile/__tests__/Badges.test.jsx
@@ -52,26 +52,30 @@ describe('Badges Component', () => {
});
expect(renderedBadges.find('.card-footer').text()).toBe('You have no badges.');
});
-
+ // Modified this test case to match improved badge structure, see PR3098 for details
it('should display the correct text when you have exactly 1 badge', () => {
const props = {
...badgeProps,
isUserSelf: true,
- userProfile: { ...badgeProps.userProfile, badgeCollection: [{ count: 1 }] },
+ userProfile: { ...badgeProps.userProfile, badgeCollection: [{ badge: { badgeName: 'Test Badge', type: 'Normal' }, count: 1 }] },
};
const renderedBadges = renderWithProvider( , {
store,
});
expect(renderedBadges.find('.card-footer').text()).toBe('Bravo! You have earned 1 badge!');
});
-
+ // Modified this test case to match improved badge structure, see PR3098 for details
it('should display the correct text when you have amount of badges > 1', () => {
const props = {
...badgeProps,
isUserSelf: true,
userProfile: {
...badgeProps.userProfile,
- badgeCollection: [{ count: 1 }, { count: 2 }, { count: 3 }],
+ badgeCollection: [
+ { badge: { badgeName: 'Badge 1', type: 'Normal' }, count: 1 },
+ { badge: { badgeName: 'Badge 2', type: 'Normal' }, count: 2 },
+ { badge: { badgeName: 'Badge 3', type: 'Normal' }, count: 3 }
+ ],
},
};
const renderedBadges = renderWithProvider( , {
@@ -90,11 +94,11 @@ describe('Badges Component', () => {
});
expect(renderedBadges.find('.card-footer').text()).toBe('This person has no badges.');
});
-
+ // Modified this test case to match improved badge structure, see PR3098 for details
it('should display the correct text when they have exactly 1 badge', () => {
const props = {
...badgeProps,
- userProfile: { ...badgeProps.userProfile, badgeCollection: [{ count: 1 }] },
+ userProfile: { ...badgeProps.userProfile, badgeCollection: [{ badge: { badgeName: 'Test Badge', type: 'Normal' }, count: 1 }] },
};
const renderedBadges = renderWithProvider( , {
store,
@@ -103,13 +107,17 @@ describe('Badges Component', () => {
'Bravo! This person has earned 1 badge!'
);
});
-
+ // Modified this test case to match improved badge structure, see PR3098 for details
it('should display the correct text when they have amount of badges > 1', () => {
const props = {
...badgeProps,
userProfile: {
...badgeProps.userProfile,
- badgeCollection: [{ count: 1 }, { count: 2 }, { count: 3 }],
+ badgeCollection: [
+ { badge: { badgeName: 'Badge 1', type: 'Normal' }, count: 1 },
+ { badge: { badgeName: 'Badge 2', type: 'Normal' }, count: 2 },
+ { badge: { badgeName: 'Badge 3', type: 'Normal' }, count: 3 }
+ ],
},
};
const renderedBadges = renderWithProvider( , {
diff --git a/src/languages/en/ui.js b/src/languages/en/ui.js
index 14162e5fd0..ab0d25cccb 100644
--- a/src/languages/en/ui.js
+++ b/src/languages/en/ui.js
@@ -3,6 +3,7 @@
* Stores all variables that display texts on the UI
******************************************************************************* */
export const ACTIVE = 'Active';
+export const TITLE = 'Title'
export const INACTIVE = 'InActive';
export const ACTIVE_PROJECTS = 'Active Projects';
export const BM_DASHBOARD = 'BM Dashboard';
diff --git a/src/reducers/allUserProfilesReducer.js b/src/reducers/allUserProfilesReducer.js
index d580980158..7a5239a49a 100644
--- a/src/reducers/allUserProfilesReducer.js
+++ b/src/reducers/allUserProfilesReducer.js
@@ -5,7 +5,7 @@ const userProfilesInitial = {
fetching: false,
fetched: false,
userProfiles: [],
- editable: { 'first': 1, 'last': 1, 'role': 1, 'email': 1, 'weeklycommittedHours': 1 ,'startDate':1,'endDate':1},
+ editable: { 'first': 1, 'last': 1, 'role': 1, 'jobTitle': 1, 'email': 1, 'weeklycommittedHours': 1 ,'startDate':1,'endDate':1},
pagestats: { pageSize: 10, selectedPage: 1 },
status: 100,
updating:false,