diff --git a/web-ui/src/components/member_selector/MemberSelector.css b/web-ui/src/components/member_selector/MemberSelector.css
index 27080dd161..01e439e4cc 100644
--- a/web-ui/src/components/member_selector/MemberSelector.css
+++ b/web-ui/src/components/member_selector/MemberSelector.css
@@ -15,19 +15,19 @@
}
.member-selector-card .member-selector-card-title {
- font-size: 1.5em;
+ font-size: 1rem;
}
.member-selector-card .member-selector-card-count {
- font-size: 1.25em;
+ font-size: 1rem;
}
@media screen and (max-width: 600px) {
.member-selector-card .member-selector-card-title {
- font-size: 1.25em;
+ font-size: 0.75rem;
}
.member-selector-card .member-selector-card-count {
- font-size: 1em;
+ font-size: 0.75rem;
}
}
diff --git a/web-ui/src/components/member_selector/MemberSelector.jsx b/web-ui/src/components/member_selector/MemberSelector.jsx
index 0c8ad7e2fe..6dc3aba135 100644
--- a/web-ui/src/components/member_selector/MemberSelector.jsx
+++ b/web-ui/src/components/member_selector/MemberSelector.jsx
@@ -4,30 +4,20 @@ import {
Avatar,
Card,
CardHeader,
- Collapse,
Divider,
IconButton,
List,
- ListItem,
- ListItemAvatar,
ListItemIcon,
ListItemText,
- Menu,
- MenuItem,
Tooltip,
Typography
} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
-import RemoveIcon from '@mui/icons-material/Remove';
-import HighlightOffIcon from '@mui/icons-material/HighlightOff';
-import MoreVertIcon from '@mui/icons-material/MoreVert';
+import { Add, FileDownload } from '@mui/icons-material';
import { getAvatarURL } from '../../api/api';
-import ExpandMore from '../expand-more/ExpandMore.jsx';
import MemberSelectorDialog, {
FilterType
} from './member_selector_dialog/MemberSelectorDialog';
-import DownloadIcon from '@mui/icons-material/FileDownload';
import { reportSelectedMembersCsv } from '../../api/member.js';
import { AppContext } from '../../context/AppContext.jsx';
import { selectCsrfToken } from '../../context/selectors.js';
@@ -58,8 +48,6 @@ const propTypes = {
outlined: PropTypes.bool,
/** If true, include a button to export the list of members to a CSV file. False by default. */
exportable: PropTypes.bool,
- /** Adjusts the height of the scrollable list of selected members (in pixels) */
- listHeight: PropTypes.number,
/** If true, members cannot be added to or removed from the current selection. False by default. */
disabled: PropTypes.bool,
/** A custom class name to additionally apply to the top-level card */
@@ -76,7 +64,6 @@ const MemberSelector = ({
expand = true,
outlined = false,
exportable = false,
- listHeight = 400,
disabled = false,
className,
style
@@ -93,7 +80,7 @@ const MemberSelector = ({
filter => filter.type === FilterType.ROLE
);
const roleFilter = filters.find(filter => filter.type === FilterType.ROLE);
- const memberDescriptor = isFilteredByRole ? roleFilter.value : 'members';
+ const memberDescriptor = isFilteredByRole ? roleFilter.value : 'Members';
const handleExpandClick = () => setExpanded(!expanded);
@@ -104,8 +91,8 @@ const MemberSelector = ({
}
}, [disabled]);
- const addMembers = membersToAdd => {
- onChange([...selected, ...membersToAdd]);
+ const replaceSelectedMembers = members => {
+ onChange(members);
setDialogOpen(false);
};
@@ -115,9 +102,7 @@ const MemberSelector = ({
};
const downloadMemberCsv = useCallback(() => {
- if (!exportable) {
- return;
- }
+ if (!exportable) return;
const memberIds = selected.map(member => member.id);
reportSelectedMembersCsv(memberIds, csrf).then(res => {
@@ -152,14 +137,6 @@ const MemberSelector = ({
style={style}
>
- }
title={
- 0
+ 3
selected
renders correctly 1`] = `
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web-ui/src/components/reviews/TeamMemberReview-old.jsx b/web-ui/src/components/reviews/TeamMemberReview-old.jsx
new file mode 100644
index 0000000000..2ecabcc2e1
--- /dev/null
+++ b/web-ui/src/components/reviews/TeamMemberReview-old.jsx
@@ -0,0 +1,367 @@
+import React, { useContext, useState, useCallback } from 'react';
+import { useTheme } from '@mui/material/styles';
+import { styled } from '@mui/material/styles';
+import SwipeableViews from 'react-swipeable-views';
+import PropTypes from 'prop-types';
+import { AppContext } from '../../context/AppContext';
+import { selectCurrentUser, selectProfile } from '../../context/selectors';
+import AppBar from '@mui/material/AppBar';
+import Button from '@mui/material/Button';
+import Tabs from '@mui/material/Tabs';
+import Tab from '@mui/material/Tab';
+import Typography from '@mui/material/Typography';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
+import Box from '@mui/material/Box';
+import {
+ Card,
+ CardActions,
+ CardContent,
+ CardHeader,
+ Modal
+} from '@mui/material';
+import FeedbackSubmitForm from '../feedback_submit_form/FeedbackSubmitForm';
+import SelectUserModal from './SelectUserModal';
+import {
+ cancelFeedbackRequest,
+ updateFeedbackRequest
+} from '../../api/feedback';
+import { selectCsrfToken } from '../../context/selectors';
+import { UPDATE_TOAST } from '../../context/actions';
+
+const propTypes = {
+ selfReview: PropTypes.any,
+ reviews: PropTypes.arrayOf(PropTypes.any),
+ memberProfile: PropTypes.any,
+ reloadReviews: PropTypes.func
+};
+const displayName = 'TeamMemberReview';
+
+const PREFIX = displayName;
+const classes = {
+ actionButtons: `${PREFIX}-actionButtons`,
+ buttonRow: `${PREFIX}-buttonRow`,
+ periodModal: `${PREFIX}-periodModal`
+};
+
+const Root = styled('div')(({ theme }) => ({
+ [`& .${classes.actionButtons}`]: {
+ margin: '0.5em 0 0 1em',
+ ['@media (max-width:820px)']: {
+ // eslint-disable-line no-useless-computed-key
+ padding: '0'
+ }
+ },
+ [`& .${classes.buttonRow}`]: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ margin: '0 0 1em 0'
+ }
+}));
+
+const TabPanel = ({ children, value, index, ...other }) => {
+ return (
+
+ {value === index && (
+
+ {children}
+
+ )}
+
+ );
+};
+
+TabPanel.propTypes = {
+ children: PropTypes.any,
+ dir: PropTypes.string,
+ index: PropTypes.number,
+ value: PropTypes.number
+};
+TabPanel.displayName = 'TabPanel';
+
+const a11yProps = index => ({
+ id: `full-width-tab-${index}`,
+ 'aria-controls': `full-width-tabpanel-${index}`
+});
+
+const TeamMemberReview = ({
+ selfReview,
+ reviews,
+ memberProfile,
+ reloadReviews
+}) => {
+ const { state } = useContext(AppContext);
+ const csrf = selectCsrfToken(state);
+ const currentUser = selectCurrentUser(state);
+ const theme = useTheme();
+ const [value, setValue] = useState(0);
+ const [reassignOpen, setReassignOpen] = useState(false);
+ const [cancelOpen, setCancelOpen] = useState(false);
+
+ const handleOpenReassign = useCallback(
+ () => setReassignOpen(true),
+ [setReassignOpen]
+ );
+ const handleCloseReassign = useCallback(
+ () => setReassignOpen(false),
+ [setReassignOpen]
+ );
+ const handleOpenCancel = useCallback(
+ () => setCancelOpen(true),
+ [setCancelOpen]
+ );
+ const handleCloseCancel = useCallback(
+ () => setCancelOpen(false),
+ [setCancelOpen]
+ );
+
+ const review = reviews && reviews[value - 1];
+ const recipient = selectProfile(state, review?.recipientId);
+
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+
+ const handleChangeIndex = index => {
+ setValue(index);
+ };
+
+ const handleCancelClick = useCallback(() => {
+ const cancelRequest = async () => {
+ const res = await cancelFeedbackRequest(review, csrf);
+ const cancellationResponse =
+ res && res.payload && res.payload.status === 200 && !res.error
+ ? res.payload.data
+ : null;
+ if (!cancellationResponse) {
+ window.snackDispatch({
+ type: UPDATE_TOAST,
+ payload: {
+ severity: 'error',
+ toast:
+ 'There was an error cancelling the review. Please contact your administrator.'
+ }
+ });
+ }
+ return cancellationResponse;
+ };
+
+ handleCloseCancel();
+ if (csrf) {
+ cancelRequest().then(res => {
+ if (res) {
+ reloadReviews();
+ window.snackDispatch({
+ type: UPDATE_TOAST,
+ payload: {
+ severity: 'success',
+ toast: 'Review canceled'
+ }
+ });
+ }
+ });
+ }
+ }, [csrf, handleCloseCancel, review, reloadReviews]);
+
+ const handleReassign = useCallback(
+ assignee => {
+ const reassignRequest = async () => {
+ review.recipientId = assignee.id;
+ const res = await updateFeedbackRequest(review, csrf);
+ const updateResponse =
+ res && res.payload && res.payload.status === 200 && !res.error
+ ? res.payload.data
+ : null;
+ if (!updateResponse) {
+ window.snackDispatch({
+ type: UPDATE_TOAST,
+ payload: {
+ severity: 'error',
+ toast:
+ 'There was an error reassigning the review. Please contact your administrator.'
+ }
+ });
+ }
+ return updateResponse;
+ };
+
+ handleCloseReassign();
+ if (csrf) {
+ reassignRequest().then(res => {
+ if (res) {
+ reloadReviews();
+ window.snackDispatch({
+ type: UPDATE_TOAST,
+ payload: {
+ severity: 'success',
+ toast: 'Review reassigned'
+ }
+ });
+ }
+ });
+ }
+ },
+ [csrf, handleCloseReassign, review, reloadReviews]
+ );
+
+ let selfReviewIcon =
;
+ if (selfReview?.status.toUpperCase() === 'SUBMITTED') {
+ selfReviewIcon =
;
+ }
+
+ return (
+
+
+
+
+
+ {reviews &&
+ reviews.map((review, index) => {
+ const reviewer = selectProfile(state, review?.recipientId);
+ let label = reviewer?.firstName + "'s Review";
+
+ if (reviewer?.id === currentUser?.id) {
+ label = 'Your Review';
+ }
+
+ let icon = ;
+ if (review?.status.toUpperCase() === 'SUBMITTED') {
+ icon = ;
+ }
+
+ return (
+
+ );
+ })}
+
+
+
+
+ {selfReview && selfReview.id ? (
+
+ ) : (
+
+ {memberProfile?.firstName} has not started their self-review.
+
+ )}
+
+ {reviews &&
+ reviews.map((review, index) => {
+ const reviewer = selectProfile(state, review?.recipientId);
+ const requesteeName = memberProfile?.name;
+
+ let readOnly = true;
+ if (
+ reviewer?.id === currentUser?.id &&
+ 'SUBMITTED' !== review?.status?.toUpperCase()
+ ) {
+ readOnly = false;
+ }
+
+ return (
+
+ {review?.status.toUpperCase() !== 'SUBMITTED' && (
+
+
+ Cancel
+
+
+ Reassign
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+ Cancel Review
+
+ }
+ />
+
+
+ Are you sure you want to cancel the review sent to{' '}
+ {recipient?.name} on {review?.sendDate} ? The
+ recipient will not be able to respond to this request once it is
+ canceled.
+
+
+
+
+ No, Keep Feedback Request
+
+
+ Yes, Cancel Feedback Request
+
+
+
+
+
+
+ );
+};
+
+TeamMemberReview.displayName = displayName;
+TeamMemberReview.propTypes = propTypes;
+
+export default TeamMemberReview;
diff --git a/web-ui/src/components/reviews/TeamReviews.jsx b/web-ui/src/components/reviews/TeamReviews.jsx
index 2264e92b3c..6f1689bc27 100644
--- a/web-ui/src/components/reviews/TeamReviews.jsx
+++ b/web-ui/src/components/reviews/TeamReviews.jsx
@@ -1,3 +1,8 @@
+import DateFnsUtils from '@date-io/date-fns';
+const dateUtils = new DateFnsUtils();
+
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
import React, {
useEffect,
useContext,
@@ -5,40 +10,63 @@ import React, {
useState,
useRef
} from 'react';
-import PropTypes from 'prop-types';
import { useLocation, useHistory } from 'react-router-dom';
+
+import {
+ AddCircle,
+ AddComment,
+ Archive,
+ ArrowBack,
+ Delete,
+ Download,
+ ExpandMore,
+ Unarchive
+} from '@mui/icons-material';
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Avatar,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ Divider,
+ FormControlLabel,
+ IconButton,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemSecondaryAction,
+ ListItemText,
+ Skeleton,
+ Switch,
+ Tooltip,
+ Typography
+} from '@mui/material';
import { styled } from '@mui/material/styles';
-import AddCircleIcon from '@mui/icons-material/AddCircle';
-import AddCommentIcon from '@mui/icons-material/AddComment';
-import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
-import Avatar from '@mui/material/Avatar';
-import Button from '@mui/material/Button';
-import IconButton from '@mui/material/IconButton';
-import Divider from '@mui/material/Divider';
-import queryString from 'query-string';
-import Accordion from '@mui/material/Accordion';
-import AccordionSummary from '@mui/material/AccordionSummary';
-import AccordionDetails from '@mui/material/AccordionDetails';
-import FormControlLabel from '@mui/material/FormControlLabel';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import Switch from '@mui/material/Switch';
-import Typography from '@mui/material/Typography';
-import List from '@mui/material/List';
-import ListItem from '@mui/material/ListItem';
-import ListItemText from '@mui/material/ListItemText';
-import ListItemAvatar from '@mui/material/ListItemAvatar';
-import Tooltip from '@mui/material/Tooltip';
-import Skeleton from '@mui/material/Skeleton';
-import TeamMemberReview from './TeamMemberReview';
-import SelectUserModal from './SelectUserModal';
-import { UPDATE_REVIEW_PERIODS, UPDATE_TOAST } from '../../context/actions';
-import { AppContext } from '../../context/AppContext';
-import { getReviewPeriods } from '../../api/reviewperiods.js';
+
+import { resolve } from '../../api/api.js';
+import { getAvatarURL } from '../../api/api.js';
import {
createFeedbackRequest,
findReviewRequestsByPeriodAndTeamMembers,
findSelfReviewRequestsByPeriodAndTeamMembers
} from '../../api/feedback.js';
+import {
+ getReviewPeriods,
+ removeReviewPeriod,
+ updateReviewPeriod
+} from '../../api/reviewperiods.js';
+import {
+ DELETE_REVIEW_PERIOD,
+ UPDATE_REVIEW_PERIOD,
+ UPDATE_REVIEW_PERIODS,
+ UPDATE_TOAST
+} from '../../context/actions';
+import { AppContext } from '../../context/AppContext';
import {
selectCsrfToken,
selectReviewPeriod,
@@ -49,16 +77,22 @@ import {
selectCurrentMembers,
selectSubordinates
} from '../../context/selectors';
-import { getAvatarURL } from '../../api/api.js';
-import DateFnsUtils from '@date-io/date-fns';
-const dateUtils = new DateFnsUtils();
+
+import MemberSelector from '../member_selector/MemberSelector';
+import MemberSelectorDialog, {
+ FilterType
+} from '../member_selector/member_selector_dialog/MemberSelectorDialog';
+import SelectUserModal from './SelectUserModal';
+import TeamMemberReview from './TeamMemberReview';
+
+import DatePickerField from './periods/DatePickerField.jsx';
+import './periods/DatePickerField.css';
const propTypes = {
teamMembers: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
- firstName: PropTypes.string,
- lastName: PropTypes.string
+ onBack: PropTypes.func
})
),
periodId: PropTypes.string
@@ -89,33 +123,48 @@ const Root = styled('div')(({ theme }) => ({
['@media (max-width:800px)']: {
// eslint-disable-line no-useless-computed-key
margin: '0',
- 'justify-content': 'center'
+ justifyContent: 'center'
}
}
}));
-const TeamReviews = ({ periodId }) => {
+const ReviewStatus = {
+ PLANNING: 'PLANNING',
+ AWAITING_APPROVAL: 'AWAITING_APPROVAL',
+ OPEN: 'OPEN',
+ CLOSED: 'CLOSED',
+ UNKNOWN: 'UNKNOWN'
+};
+
+const TeamReviews = ({ onBack, periodId }) => {
const { state, dispatch } = useContext(AppContext);
- const csrf = selectCsrfToken(state);
- const location = useLocation();
const history = useHistory();
- const currentUser = selectCurrentUser(state);
+ const location = useLocation();
+
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [includeAll, setIncludeAll] = useState(false);
+ const [memberFilters, setMemberFilters] = useState([]);
+ const [memberSelectorOpen, setMemberSelectorOpen] = useState(false);
+ const [newRequestOpen, setNewRequestOpen] = useState(false);
+ const [query, setQuery] = useState({});
+ const [reviews, setReviews] = useState(null);
+ const [selectedMember, setSelectedMember] = useState(null);
+ const [selfReviews, setSelfReviews] = useState({});
+ const [teamMembers, setTeamMembers] = useState([]);
+ const [toDelete, setToDelete] = useState(null);
+
+ const creatingReview = useRef(false);
+ const loadedReviews = useRef(false);
+ const loadingReviews = useRef(false);
+
+ const csrf = selectCsrfToken(state);
const currentMembers = selectCurrentMembers(state);
- const myTeam = selectMyTeam(state);
- const subordinates = selectSubordinates(state, currentUser?.id);
+ const currentUser = selectCurrentUser(state);
const isAdmin = selectIsAdmin(state);
+ const myTeam = selectMyTeam(state);
const period = selectReviewPeriod(state, periodId);
- const [teamMembers, setTeamMembers] = useState(null);
- const [selfReviews, setSelfReviews] = useState({});
- const [reviews, setReviews] = useState(null);
- const [query, setQuery] = useState({});
- const [selectedTeamMember, setSelectedTeamMember] = useState(null);
- const selectedMemberProfile = selectProfile(state, selectedTeamMember);
- const [newRequestOpen, setNewRequestOpen] = useState(false);
- const [includeAll, setIncludeAll] = useState(false);
- const loadingReviews = useRef(false);
- const loadedReviews = useRef(false);
- const creatingReview = useRef(false);
+ const selectedMemberProfile = selectProfile(state, selectedMember);
+ const subordinates = selectSubordinates(state, currentUser?.id);
const handleOpenNewRequest = useCallback(
() => setNewRequestOpen(true),
@@ -126,24 +175,58 @@ const TeamReviews = ({ periodId }) => {
[setNewRequestOpen]
);
+ const reviewAssignmentsUrl = '/services/review-assignments';
+
useEffect(() => {
- if (currentMembers && currentMembers.length > 0) {
- isAdmin && includeAll
- ? setTeamMembers(
- currentMembers.filter(member => member?.id !== currentUser?.id)
- )
- : includeAll
- ? setTeamMembers(subordinates)
- : setTeamMembers(myTeam);
+ loadTeamMembers();
+ }, [currentMembers]);
+
+ const loadTeamMembers = async () => {
+ const myId = currentUser?.id;
+ try {
+ const res = await resolve({
+ method: 'GET',
+ url: `${reviewAssignmentsUrl}/period/${periodId}`,
+ headers: {
+ 'X-CSRF-Header': csrf,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json;charset=UTF-8'
+ }
+ });
+ if (res.error) throw new Error(res.error.message);
+ const assignments = res.payload.data;
+ const memberIds = assignments.map(a => a.revieweeId);
+ const members = currentMembers.filter(m => memberIds.includes(m.id));
+ setTeamMembers(members);
+ } catch (err) {
+ console.error('TeamReviews.jsx loadTeamMembers:', err);
}
- }, [
- isAdmin,
- includeAll,
- subordinates,
- currentMembers,
- myTeam,
- currentUser?.id
- ]);
+ };
+
+ const updateTeamMembers = async teamMembers => {
+ const data = teamMembers.map(tm => ({
+ revieweeId: tm.id,
+ reviewerId: tm.supervisorid,
+ reviewPeriodId: periodId,
+ approved: true
+ }));
+
+ try {
+ const res = await resolve({
+ method: 'POST',
+ url: reviewAssignmentsUrl + '/' + periodId,
+ data,
+ headers: {
+ 'X-CSRF-Header': csrf,
+ Accept: 'application/json',
+ 'Content-Type': 'application/json;charset=UTF-8'
+ }
+ });
+ setTeamMembers(teamMembers);
+ } catch (err) {
+ console.error('TeamReviews.jsx updateTeamMembers:', err);
+ }
+ };
const getReviewStatus = useCallback(
teamMemberId => {
@@ -219,7 +302,7 @@ const TeamReviews = ({ periodId }) => {
}, [query.teamMember, hasTeamMember]);
useEffect(() => {
- setSelectedTeamMember(getTeamMember());
+ setSelectedMember(getTeamMember());
}, [getTeamMember]);
useEffect(() => {
@@ -288,6 +371,115 @@ const TeamReviews = ({ periodId }) => {
}
}, [csrf, reviews, currentUser, period, selectedMemberProfile]);
+ const confirmDelete = useCallback(() => {
+ setToDelete(period.id);
+ setConfirmOpen(true);
+ }, [period, setToDelete, setConfirmOpen]);
+
+ const handleConfirmClose = useCallback(() => {
+ setToDelete(null);
+ setConfirmOpen(false);
+ }, [setToDelete, setConfirmOpen]);
+
+ const deleteReviewPeriod = useCallback(async () => {
+ if (!csrf) return;
+
+ await removeReviewPeriod(toDelete, csrf);
+ dispatch({
+ type: DELETE_REVIEW_PERIOD,
+ payload: toDelete
+ });
+ handleConfirmClose();
+ history.goBack();
+ }, [csrf, dispatch, toDelete, handleConfirmClose]);
+
+ const toggleReviewPeriod = useCallback(async () => {
+ if (!csrf) return;
+
+ period.reviewStatus =
+ period?.reviewStatus === ReviewStatus.CLOSED
+ ? ReviewStatus.OPEN
+ : ReviewStatus.CLOSED;
+ const res = await updateReviewPeriod(period, csrf);
+ const data = res?.payload?.data ? res.payload.data : null;
+ if (data) {
+ dispatch({ type: UPDATE_REVIEW_PERIOD, payload: period });
+ } else {
+ console.error(res?.error);
+ window.snackDispatch({
+ type: UPDATE_TOAST,
+ payload: {
+ severity: 'error',
+ toast: 'Error selecting review period'
+ }
+ });
+ }
+ }, [csrf, period, state, dispatch]);
+
+ const updateReviewPeriodDates = useCallback(
+ async period => {
+ if (!csrf) return;
+
+ const res = await updateReviewPeriod(period, csrf);
+ const data = res?.payload?.data ?? null;
+ if (data) {
+ dispatch({ type: UPDATE_REVIEW_PERIODS, payload: [period] });
+ } else {
+ console.error('Error updating review period:', res?.error);
+ window.snackDispatch({
+ type: UPDATE_TOAST,
+ payload: {
+ severity: 'error',
+ toast: 'Error updating review period'
+ }
+ });
+ }
+ },
+ [csrf, dispatch, period, state]
+ );
+
+ const handleLaunchDateChange = (val, period) => {
+ const newDate = val?.$d;
+ const isoDate = newDate.toISOString() ?? null;
+ const newPeriod = { ...period, launchDate: isoDate };
+
+ // Clear dates that are not correctly ordered.
+ const selfReviewCloseDate = new Date(period.selfReviewCloseDate);
+ const closeDate = new Date(period.closeDate);
+ if (selfReviewCloseDate <= newDate) newPeriod.selfReviewCloseDate = null;
+ if (closeDate <= newDate) newPeriod.closeDate = null;
+
+ updateReviewPeriodDates(newPeriod);
+ };
+
+ const handleSelfReviewDateChange = (val, period) => {
+ const newDate = val?.$d;
+ const isoDate = newDate.toISOString() ?? null;
+ const newPeriod = { ...period, selfReviewCloseDate: isoDate };
+
+ // Clear dates that are not correctly ordered.
+ const launchDate = new Date(period.launchDate);
+ const closeDate = new Date(period.closeDate);
+ if (launchDate >= newDate) newPeriod.launchDate = null;
+ if (closeDate <= newDate) newPeriod.closeDate = null;
+
+ updateReviewPeriodDates(newPeriod);
+ };
+
+ const handleCloseDateChange = (val, period) => {
+ const newDate = val?.$d;
+ const isoDate = newDate.toISOString() ?? null;
+ const newPeriod = { ...period, closeDate: isoDate };
+
+ // Clear dates that are not correctly ordered.
+ const launchDate = new Date(period.launchDate);
+ const selfReviewCloseDate = new Date(period.selfReviewCloseDate);
+ if (launchDate >= newDate) newPeriod.launchDate = null;
+ if (selfReviewCloseDate >= newDate) newPeriod.selfReviewCloseDate = null;
+
+ updateReviewPeriodDates(newPeriod);
+ };
+
const handleQueryChange = useCallback(
(key, value) => {
let newQuery = {
@@ -478,11 +670,11 @@ const TeamReviews = ({ periodId }) => {
},
[
csrf,
- period,
- selectedMemberProfile,
dispatch,
handleCloseNewRequest,
- reviews
+ period,
+ reviews,
+ selectedMemberProfile
]
);
@@ -493,9 +685,51 @@ const TeamReviews = ({ periodId }) => {
return (
+
+
+ Back
+
+
+
-
Team Reviews
- {!selectedTeamMember && (
+
{period?.name ?? ''} Team Reviews
+
+ {period && isAdmin && (
+
+
+
+ {period.reviewStatus === ReviewStatus.OPEN ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+ {!selectedMember && (
@@ -503,11 +737,11 @@ const TeamReviews = ({ periodId }) => {
label="Show All"
/>
)}
- {selectedTeamMember && (
+ {selectedMember && (
}
+ endIcon={
}
variant="contained"
color="primary"
>
@@ -515,142 +749,36 @@ const TeamReviews = ({ periodId }) => {
)}
- {!selectedTeamMember && loadedReviews.current && (
- <>
-
- {teamMembers && teamMembers.length > 0
- ? teamMembers
- .sort((a, b) => {
- return ('' + a?.lastName)
- .toUpperCase()
- .localeCompare(b?.lastName.toUpperCase());
- })
- .filter(teamMember => {
- return (
- reviews &&
- (!reviews[teamMember.id] ||
- reviews[teamMember.id].length === 0 ||
- !reviews[teamMember.id]?.reduce(
- (status, review) =>
- status && review.status === 'submitted',
- true
- ))
- );
- })
- .map((teamMember, i) => (
- <>
- onTeamMemberSelected(teamMember?.id)}
- key={`teamMember-${teamMember?.id}`}
- >
-
-
-
-
-
-
-
- {
- e.stopPropagation();
- history.push(
- `/feedback/request?for=${teamMember?.id}`
- );
- }}
- />
-
-
-
-
-
- >
- ))
- : null}
-
-
- }
- aria-controls="panel1a-content"
- id="panel1a-header"
- >
- Completed Reviews
-
-
-
- {teamMembers && teamMembers.length > 0
- ? teamMembers
- .sort((a, b) => {
- return ('' + a?.lastName)
- .toUpperCase()
- .localeCompare(b?.lastName.toUpperCase());
- })
- .filter(teamMember => {
- return (
- reviews &&
- reviews[teamMember.id] &&
- reviews[teamMember.id].length !== 0 &&
- reviews[teamMember.id]?.reduce(
- (status, review) =>
- status && review.status === 'submitted',
- true
- )
- );
- })
- .map((teamMember, i) => (
- <>
- onTeamMemberSelected(teamMember?.id)}
- key={`teamMember-${teamMember?.id}`}
- >
-
-
-
-
-
-
-
- {
- e.stopPropagation();
- history.push(
- `/feedback/request?for=${teamMember?.id}`
- );
- }}
- />
-
-
-
-
-
- >
- ))
- : null}
-
-
-
- >
+ {period && (
+
+ handleLaunchDateChange(val, period)}
+ label="Launch Date"
+ disabled={!isAdmin}
+ open={period?.reviewStatus === ReviewStatus?.PLANNING}
+ />
+ handleSelfReviewDateChange(val, period)}
+ label="Self-Review Date"
+ disabled={!isAdmin}
+ />
+ handleCloseDateChange(val, period)}
+ label="Close Date"
+ disabled={!isAdmin}
+ />
+
)}
- {!selectedTeamMember && loadingReviews.current && (
+
+ {!selectedMember && loadingReviews.current && (
<>
@@ -668,12 +796,12 @@ const TeamReviews = ({ periodId }) => {
>
)}
- {!!selectedTeamMember && reviews && (
+ {!!selectedMember && reviews && (
)}
{
onSelect={handleNewRequest}
onClose={handleCloseNewRequest}
/>
+ setDialogOpen(false)}
+ onSubmit={membersToAdd => setTeamMembers(membersToAdd)}
+ />
+
+
+ {'Delete this review period?'}
+
+
+
+ Are you sure that you would like to delete period{' '}
+ {selectReviewPeriod(state, toDelete)?.name}?
+
+
+
+ No
+
+ Yes
+
+
+
);
};
diff --git a/web-ui/src/components/reviews/periods/DatePickerField.jsx b/web-ui/src/components/reviews/periods/DatePickerField.jsx
index 3e29f847cf..a430f2c46a 100644
--- a/web-ui/src/components/reviews/periods/DatePickerField.jsx
+++ b/web-ui/src/components/reviews/periods/DatePickerField.jsx
@@ -1,7 +1,8 @@
+import dayjs from 'dayjs';
import React, { useRef, useEffect, useState } from 'react';
-import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import './DatePickerField.css';
export default function DatePickerField({
@@ -36,7 +37,7 @@ export default function DatePickerField({
{
const { state, dispatch } = useContext(AppContext);
const [canSave, setCanSave] = useState(false);
- const [confirmOpen, setConfirmOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [reviewStatus, setReviewStatus] = useState(ReviewStatus.CLOSED);
@@ -154,7 +141,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
});
const [selfReviews, setSelfReviews] = useState(null);
const [templates, setTemplates] = useState([]);
- const [toDelete, setToDelete] = useState(null);
const currentUserId = selectCurrentUserId(state);
const csrf = selectCsrfToken(state);
@@ -234,34 +220,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
]
);
- const toggleReviewPeriod = useCallback(
- async id => {
- if (!csrf) {
- return;
- }
- const toUpdate = selectReviewPeriod(state, id);
- toUpdate.reviewStatus =
- toUpdate?.reviewStatus === ReviewStatus.CLOSED
- ? ReviewStatus.OPEN
- : ReviewStatus.CLOSED;
- const res = await updateReviewPeriod(toUpdate, csrf);
- const data = res?.payload?.data ? res.payload.data : null;
- if (data) {
- dispatch({ type: UPDATE_REVIEW_PERIODS, payload: [...periods] });
- } else {
- console.error(res?.error);
- window.snackDispatch({
- type: UPDATE_TOAST,
- payload: {
- severity: 'error',
- toast: 'Error selecting review period'
- }
- });
- }
- },
- [csrf, state, periods, dispatch]
- );
-
const getSecondaryLabel = useCallback(
periodId => {
if (mode === 'self') {
@@ -290,48 +248,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
[selfReviews, state, mode]
);
- const handleConfirmClose = useCallback(() => {
- setToDelete(null);
- setConfirmOpen(false);
- }, [setToDelete, setConfirmOpen]);
-
- const deleteReviewPeriod = useCallback(async () => {
- if (!csrf) {
- return;
- }
-
- await removeReviewPeriod(toDelete, csrf);
- dispatch({
- type: UPDATE_REVIEW_PERIODS,
- payload: periods.filter(period => period?.id !== toDelete)
- });
- handleConfirmClose();
- }, [csrf, periods, dispatch, toDelete, handleConfirmClose]);
-
- const updateReviewPeriodDates = useCallback(
- async period => {
- if (!csrf) {
- return;
- }
- const res = await updateReviewPeriod(period, csrf);
- const data = res?.payload?.data ?? null;
- if (data) {
- dispatch({ type: UPDATE_REVIEW_PERIODS, payload: [...periods] });
- } else {
- console.error(res?.error);
- window.snackDispatch({
- type: UPDATE_TOAST,
- payload: {
- severity: 'error',
- toast: 'Error updating review period'
- }
- });
- }
- setPeriodToAdd(period);
- },
- [csrf, state, periods, dispatch]
- );
-
const loadFeedbackTemplates = useCallback(async () => {
const res = await getAllFeedbackTemplates(csrf);
const templates = res?.payload?.data;
@@ -438,14 +354,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
[state, onPeriodSelected]
);
- const confirmDelete = useCallback(
- id => {
- setToDelete(id);
- setConfirmOpen(true);
- },
- [setToDelete, setConfirmOpen]
- );
-
const handleReviewTemplateChange = event => {
const templateId = event.target.value;
setPeriodToAdd({
@@ -462,30 +370,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
});
};
- const handleLaunchDateChange = (val, period) => {
- const isoDate = val?.$d.toISOString() ?? null;
- updateReviewPeriodDates({
- ...period,
- launchDate: isoDate
- });
- };
-
- const handleSelfReviewDateChange = (val, period) => {
- const isoDate = val?.$d.toISOString() ?? null;
- updateReviewPeriodDates({
- ...period,
- selfReviewCloseDate: isoDate
- });
- };
-
- const handleCloseDateChange = (val, period) => {
- const isoDate = val?.$d.toISOString() ?? null;
- updateReviewPeriodDates({
- ...period,
- closeDate: isoDate
- });
- };
-
return (
@@ -543,46 +427,7 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
i
) => (
-
-
- toggleReviewPeriod(id)}
- aria-label={
- reviewStatus === ReviewStatus.OPEN
- ? 'Archive'
- : 'Unarchive'
- }
- >
- {reviewStatus === ReviewStatus.OPEN ? (
-
- ) : (
-
- )}
-
-
-
- confirmDelete(id)}
- edge="end"
- aria-label="Delete"
- >
-
-
-
- >
- )
- }
- key={`period-${id}`}
- >
+
onPeriodClick(id)}
@@ -595,54 +440,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
primary={`${name} - ${titleCase(reviewStatus)}`}
secondary={getSecondaryLabel(id)}
/>
-
-
- handleLaunchDateChange(val, {
- id,
- name,
- reviewStatus,
- launchDate,
- selfReviewCloseDate,
- closeDate
- })
- }
- label="Launch Date"
- disabled={!isAdmin}
- open={reviewStatus === ReviewStatus.PLANNING}
- />
-
- handleSelfReviewDateChange(val, {
- id,
- name,
- reviewStatus,
- launchDate,
- selfReviewCloseDate,
- closeDate
- })
- }
- label="Self-Review Date"
- disabled={!isAdmin}
- />
-
- handleCloseDateChange(val, {
- id,
- name,
- reviewStatus,
- launchDate,
- selfReviewCloseDate,
- closeDate
- })
- }
- label="Close Date"
- disabled={!isAdmin}
- />
-
)
@@ -732,28 +529,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
-
-
- {'Delete this review period?'}
-
-
-
- Are you sure that you would like to delete period{' '}
- {selectReviewPeriod(state, toDelete)?.name}?
-
-
-
- No
-
- Yes
-
-
-
);
};
diff --git a/web-ui/src/context/actions.js b/web-ui/src/context/actions.js
index a4b8d5f24b..80d90b160b 100644
--- a/web-ui/src/context/actions.js
+++ b/web-ui/src/context/actions.js
@@ -5,6 +5,7 @@ export const ADD_SKILL = '@@check-ins/add-skill';
export const ADD_TEAM = '@@check-ins/add_team';
export const DELETE_MEMBER_PROFILE = '@@check-ins/delete_member_profile';
export const DELETE_MEMBER_SKILL = '@@check-ins/delete_member_skill';
+export const DELETE_REVIEW_PERIOD = '@@check-ins/delete_review_period';
export const DELETE_SKILL = '@@check-ins/delete-skill';
export const DELETE_ROLE = '@@check-ins/delete-role';
export const MY_PROFILE_UPDATE = '@@check-ins/update_profile';
@@ -31,5 +32,6 @@ export const UPDATE_FEEEDBACK_SUGGESTIONS =
'@@check-ins/update_feedback_suggestions';
export const UPDATE_PEOPLE_LOADING = '@@check-ins/update_people_loading';
export const UPDATE_TEAMS_LOADING = '@@check-ins/update_teams_loading';
+export const UPDATE_REVIEW_PERIOD = '@@check-ins/update_review_period';
export const UPDATE_REVIEW_PERIODS = '@@check-ins/update_review_periods';
export const ADD_REVIEW_PERIOD = '@@check-ins/add_review_period';
diff --git a/web-ui/src/context/reducer.js b/web-ui/src/context/reducer.js
index ab9d5204e6..081d0f144b 100644
--- a/web-ui/src/context/reducer.js
+++ b/web-ui/src/context/reducer.js
@@ -7,6 +7,7 @@ import {
DELETE_MEMBER_PROFILE,
DELETE_MEMBER_SKILL,
DELETE_ROLE,
+ DELETE_REVIEW_PERIOD,
DELETE_SKILL,
MY_PROFILE_UPDATE,
SET_CSRF,
@@ -28,6 +29,7 @@ import {
UPDATE_USER_BIO,
UPDATE_PEOPLE_LOADING,
UPDATE_TEAMS_LOADING,
+ UPDATE_REVIEW_PERIOD,
UPDATE_REVIEW_PERIODS,
ADD_REVIEW_PERIOD
} from './actions';
@@ -225,12 +227,23 @@ export const reducer = (state, action) => {
state.guilds.sort((a, b) => a.name.localeCompare(b.name));
state.guilds = [...state.guilds];
break;
- case UPDATE_REVIEW_PERIODS:
- state.reviewPeriods = action.payload;
- break;
case ADD_REVIEW_PERIOD:
state.reviewPeriods = [...state.reviewPeriods, action.payload];
break;
+ case DELETE_REVIEW_PERIOD:
+ const periodId = action.payload;
+ state.reviewPeriods = state.reviewPeriods.filter(p => p.id !== periodId);
+ break;
+ case UPDATE_REVIEW_PERIOD:
+ const period = action.payload;
+ const periodIndex = state.reviewPeriods.findIndex(
+ p => p.id === period.id
+ );
+ if (periodIndex !== -1) state.reviewPeriods[periodIndex] = period;
+ break;
+ case UPDATE_REVIEW_PERIODS:
+ state.reviewPeriods = action.payload;
+ break;
default:
}
return { ...state };
diff --git a/web-ui/src/pages/CheckinsReportPage.jsx b/web-ui/src/pages/CheckinsReportPage.jsx
index 750e3d96af..5e2dcfed9b 100644
--- a/web-ui/src/pages/CheckinsReportPage.jsx
+++ b/web-ui/src/pages/CheckinsReportPage.jsx
@@ -136,7 +136,6 @@ const CheckinsReportPage = () => {
title="Select PDLs"
selected={selectedPdls}
onChange={setSelectedPdls}
- listHeight={180}
exportable
expand={false}
/>
diff --git a/web-ui/src/pages/ReviewsPage.jsx b/web-ui/src/pages/ReviewsPage.jsx
index 0f9029db0d..113c2e3fd9 100644
--- a/web-ui/src/pages/ReviewsPage.jsx
+++ b/web-ui/src/pages/ReviewsPage.jsx
@@ -91,7 +91,10 @@ const ReviewPage = () => {
{selectedPeriod === null ? (
) : (
-
+ onPeriodSelected(null)}
+ periodId={selectedPeriod}
+ />
)}
diff --git a/web-ui/src/pages/TeamSkillReportPage.jsx b/web-ui/src/pages/TeamSkillReportPage.jsx
index 01ce7936ba..611a72a60c 100644
--- a/web-ui/src/pages/TeamSkillReportPage.jsx
+++ b/web-ui/src/pages/TeamSkillReportPage.jsx
@@ -162,7 +162,6 @@ const TeamSkillReportPage = () => {