Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions web-ui/src/components/reviews/periods/DatePickerField.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.date-picker-field {
display: flex;
margin: 0.7rem;

.MuiFormControl-root {
width: auto;
}
Expand All @@ -17,16 +17,3 @@
align-items: center;
margin-bottom: 1rem;
}

.reviewPeriodSection {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This CSS class is no longer used.

border: 1px solid var(--primary-link-dark);
border-radius: 0.5rem;
margin-bottom: 1rem;
background-color: var(--checkins-palette-gray-light);
cursor: pointer;

&:hover {
background-color: transparent;
transition: all 0.4s ease-out;
}
}
22 changes: 22 additions & 0 deletions web-ui/src/components/reviews/periods/ReviewPeriodCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.review-period-card {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The UI that renders each review period in ReviewPeriods.jsx was factored out to create the new component ReviewPeriodCard. This is the CSS for the new component.

.MuiListItemAvatar-root {
padding: 0.7rem 0;
}

.bottom-row {
padding: 0.5rem;
}

.row {
display: flex;
align-items: center;
}

.top-row {
padding-left: 0.7rem;

&:hover {
cursor: pointer;
}
}
}
210 changes: 210 additions & 0 deletions web-ui/src/components/reviews/periods/ReviewPeriodCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import PropTypes from 'prop-types';
Copy link
Collaborator Author

@mvolkmann mvolkmann May 15, 2024

Choose a reason for hiding this comment

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

This is the new component that was factored out of ReviewPeriod.jsx.
Most of the code stayed the same, but new code was added to get and display approval percentages.

import React, { useCallback, useContext, useEffect, useState } from 'react';

import {
BorderColor,
DoorFront,
HourglassTop,
MeetingRoom,
QuestionMark
} from '@mui/icons-material';

import {
Avatar,
Card,
CardActions,
CardContent,
Collapse,
ListItemAvatar,
ListItemText
} from '@mui/material';

import ExpandMore from '../../expand-more/ExpandMore';
import { resolve } from '../../../api/api.js';
import { AppContext } from '../../../context/AppContext';
import {
selectCsrfToken,
selectCurrentMembers,
selectHasUpdateReviewAssignmentsPermission,
selectReviewPeriods
} from '../../../context/selectors';
import { titleCase } from '../../../helpers/strings.js';

import './ReviewPeriodCard.css';

const propTypes = {
mode: PropTypes.string,
onSelect: PropTypes.func.isRequired,
periodId: PropTypes.string.isRequired,
selfReviews: PropTypes.object.isRequired
};
const displayName = 'ReviewPeriodCard';

const ReviewStatus = {
PLANNING: 'PLANNING',
AWAITING_APPROVAL: 'AWAITING_APPROVAL',
OPEN: 'OPEN',
CLOSED: 'CLOSED',
UNKNOWN: 'UNKNOWN'
};

const reviewStatusIconMap = {
[ReviewStatus.PLANNING]: <BorderColor />,
[ReviewStatus.AWAITING_APPROVAL]: <HourglassTop />,
[ReviewStatus.OPEN]: <MeetingRoom />,
[ReviewStatus.CLOSED]: <DoorFront />,
[ReviewStatus.UNKNOWN]: <QuestionMark />
};

const ReviewPeriodCard = ({ mode, onSelect, periodId, selfReviews }) => {
const { state } = useContext(AppContext);
const [approvalStats, setApprovalStats] = useState([]);
const [overallApprovalPercentage, setOverallApprovalPercentage] = useState(0);
const [expanded, setExpanded] = useState(false);

const csrf = selectCsrfToken(state);
const currentMembers = selectCurrentMembers(state);
const canUpdateReviewAssignments =
selectHasUpdateReviewAssignmentsPermission(state);
const periods = selectReviewPeriods(state);

const period = periods.find(period => period.id === periodId);
const showPercentages =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This determines whether we should show percentages. The user must have a certain permission AND the review period must have a status of "AWAITING APPROVAL".

canUpdateReviewAssignments &&
period.reviewStatus === ReviewStatus.AWAITING_APPROVAL;
const { name, reviewStatus } = period;

const handleExpandClick = () => setExpanded(!expanded);

const loadApprovalStats = async () => {
try {
// Get all the review assignments for this period.
const res = await resolve({
method: 'GET',
url: `/services/review-assignments/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 approvedCount = assignments.filter(a => a.approved).length;
setOverallApprovalPercentage((100 * approvedCount) / assignments.length);

// Get a list of all the reviewers in this period.
const reviewerIds = new Set();
for (const assignment of assignments) {
reviewerIds.add(assignment.reviewerId);
}
const reviewers = [...reviewerIds].map(id =>
currentMembers.find(m => m.id === id)
);
reviewers.sort((a, b) => a.name.localeCompare(b.name));

// Build an array containing statistics for each reviewer.
const stats = reviewers.map(reviewer => {
const { id } = reviewer;
const assignmentsForReviewer = assignments.filter(
assignment => assignment.reviewerId === id
);
const approved = assignmentsForReviewer.filter(
assignment => assignment.approved
).length;
return {
name: reviewer.name,
percent:
((100 * approved) / assignmentsForReviewer.length).toFixed(0) + '%'
};
});

setApprovalStats(stats);
} catch (err) {
console.error('ReviewPeriods.jsx getApprovalStats:', err);
}
};

useEffect(() => {
if (csrf && currentMembers.length) loadApprovalStats();
}, [csrf, currentMembers]);

const getSecondaryLabel = useCallback(
periodId => {
if (mode === 'self') {
if (
selectReviewPeriod(state, periodId)?.reviewStatus ===
ReviewStatus.OPEN
) {
if (!selfReviews[periodId] || selfReviews[periodId] === null) {
return 'Click to start your review.';
} else {
if (selfReviews[periodId].status.toUpperCase() === 'SUBMITTED') {
return 'Your review has been submitted. Thank you!';
} else {
return 'Click to finish your review.';
}
}
} else {
return 'This review period is closed.';
}
}
},
[mode, selfReviews, state]
);

return (
<Card className="review-period-card" key={`period-${periodId}`}>
<div className="row top-row">
<ListItemAvatar
key={`period-lia-${periodId}`}
onClick={() => onSelect(periodId)}
>
<Avatar>{reviewStatusIconMap[reviewStatus]}</Avatar>
</ListItemAvatar>
<ListItemText
key={`period-lit-${periodId}`}
onClick={() => onSelect(periodId)}
primary={`${name} - ${titleCase(ReviewStatus[reviewStatus])}`}
secondary={getSecondaryLabel(periodId)}
/>
{showPercentages && (
<div className="row">
<ListItemText
key={`period-percent-${periodId}`}
primary={overallApprovalPercentage.toFixed(0) + '%'}
/>
<CardActions disableSpacing>
<ExpandMore
expand={expanded}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label={expanded ? 'show less' : 'show more'}
size="large"
/>
</CardActions>
</div>
)}
</div>
{showPercentages && (
<Collapse
className="bottom-row"
in={expanded}
timeout="auto"
unmountOnExit
>
{approvalStats.map(stats => (
<div key={stats.name}>
{stats.name} - {stats.percent}
</div>
))}
</Collapse>
)}
</Card>
);
};

ReviewPeriodCard.propTypes = propTypes;
ReviewPeriodCard.displayName = displayName;

export default ReviewPeriodCard;
73 changes: 10 additions & 63 deletions web-ui/src/components/reviews/periods/ReviewPeriods.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';

import {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All the deleted code was moved to the new component ReviewPeriodCard.

BorderColor,
DoorFront,
HourglassTop,
MeetingRoom,
QuestionMark
} from '@mui/icons-material';

import {
Avatar,
Box,
Expand Down Expand Up @@ -51,7 +43,7 @@ import {
selectUserProfile
} from '../../../context/selectors';

import { titleCase } from '../../../helpers/strings.js';
import ReviewPeriodCard from './ReviewPeriodCard.jsx';

const propTypes = {
message: PropTypes.string,
Expand Down Expand Up @@ -114,14 +106,6 @@ const ReviewStatus = {
UNKNOWN: 'UNKNOWN'
};

const reviewStatusIconMap = {
[ReviewStatus.PLANNING]: <BorderColor />,
[ReviewStatus.AWAITING_APPROVAL]: <HourglassTop />,
[ReviewStatus.OPEN]: <MeetingRoom />,
[ReviewStatus.CLOSED]: <DoorFront />,
[ReviewStatus.UNKNOWN]: <QuestionMark />
};

const ReviewPeriods = ({ onPeriodSelected, mode }) => {
const { state, dispatch } = useContext(AppContext);

Expand All @@ -135,7 +119,7 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
closeDate: null
});
const [reviewStatus, setReviewStatus] = useState(ReviewStatus.CLOSED);
const [selfReviews, setSelfReviews] = useState(null);
const [selfReviews, setSelfReviews] = useState({});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This holds a map of review data. Initializing it to an empty object simplifies the code a bit.

const [templates, setTemplates] = useState([]);

const currentUserId = selectCurrentUserId(state);
Expand Down Expand Up @@ -216,34 +200,6 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
]
);

const getSecondaryLabel = useCallback(
periodId => {
if (mode === 'self') {
if (
selectReviewPeriod(state, periodId)?.reviewStatus ===
ReviewStatus.OPEN
) {
if (
!selfReviews ||
!selfReviews[periodId] ||
selfReviews[periodId] === null
) {
return 'Click to start your review.';
} else {
if (selfReviews[periodId].status.toUpperCase() === 'SUBMITTED') {
return 'Your review has been submitted. Thank you!';
} else {
return 'Click to finish your review.';
}
}
} else {
return 'This review period is closed.';
}
}
},
[selfReviews, state, mode]
);

const loadFeedbackTemplates = useCallback(async () => {
const res = await getAllFeedbackTemplates(csrf);
const templates = res?.payload?.data;
Expand Down Expand Up @@ -335,7 +291,7 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
periods &&
periods.length > 0 &&
currentUserId &&
selfReviews == null
Object.keys(selfReviews).length === 0
) {
getSelfReviews();
}
Expand Down Expand Up @@ -426,22 +382,13 @@ const ReviewPeriods = ({ onPeriodSelected, mode }) => {
: 1;
})
.map(({ name, reviewStatus, id }, i) => (
<div key={i} className="reviewPeriodSection">
<ListItem key={`period-${id}`}>
<ListItemAvatar
key={`period-lia-${id}`}
onClick={() => onPeriodClick(id)}
>
<Avatar>{reviewStatusIconMap[reviewStatus]}</Avatar>
</ListItemAvatar>
<ListItemText
key={`period-lit-${id}`}
onClick={() => onPeriodClick(id)}
primary={`${name} - ${titleCase(ReviewStatus[reviewStatus])}`}
secondary={getSecondaryLabel(id)}
/>
</ListItem>
</div>
<ReviewPeriodCard
key={`review-period-card-${id}`}
mode={mode}
onSelect={onPeriodClick}
periodId={id}
selfReviews={selfReviews}
/>
))
) : (
<Typography variant="body1">
Expand Down
10 changes: 10 additions & 0 deletions web-ui/src/context/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ export const selectHasTeamSkillsReportPermission = createSelector(
)
);

export const selectHasUpdateReviewAssignmentsPermission = createSelector(
selectUserProfile,
userProfile =>
userProfile &&
userProfile.role &&
userProfile.permissions.some(p =>
p?.permission?.includes('CAN_UPDATE_REVIEW_ASSIGNMENTS')
)
);

export const selectIsPDL = createSelector(
selectUserProfile,
userProfile =>
Expand Down