Skip to content

Commit

Permalink
Dashboard overview of pending reviews.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Oct 30, 2022
1 parent 42b946a commit bfe7a42
Show file tree
Hide file tree
Showing 13 changed files with 393 additions and 14 deletions.
159 changes: 159 additions & 0 deletions src/components/Solutions/PendingReviewsList/PendingReviewsList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';

import GroupsNameContainer from '../../../containers/GroupsNameContainer';
import UsersNameContainer from '../../../containers/UsersNameContainer';
import AssignmentNameContainer from '../../../containers/AssignmentNameContainer';
import DateTime from '../../widgets/DateTime';
import Button from '../../widgets/TheButton';
import Box from '../../widgets/Box';
import Icon, { AssignmentIcon, GroupIcon, LoadingIcon, RefreshIcon, WarningIcon } from '../../icons';
import { resourceStatus } from '../../../redux/helpers/resourceManager';
import withLinks from '../../../helpers/withLinks';

class PendingReviewsList extends Component {
state = { allPending: false };

closeAll = () => {
const { solutions, closeReview, refresh } = this.props;

this.setState({ allPending: true });

const params = [];
Object.keys(solutions).forEach(groupId =>
Object.keys(solutions[groupId]).forEach(assignmentId =>
solutions[groupId][assignmentId].forEach(solution =>
params.push({ groupId, assignmentId, solutionId: solution.id })
)
)
);

return Promise.all(params.map(closeReview)).then(() => {
this.setState({ allPending: false });
refresh && refresh();
}, refresh);
};

render() {
const {
state = resourceStatus.PENDING,
solutions = {},
updatingSelector,
closeReview = null,
refresh = null,
links: { SOLUTION_SOURCE_CODES_URI_FACTORY },
} = this.props;

return state === resourceStatus.FULFILLED && (!solutions || Object.keys(solutions).length === 0) ? null : (
<Box
title={<FormattedMessage id="app.pendingReviewsList.title" defaultMessage="All open reviews" />}
footer={
state === resourceStatus.FULFILLED ? (
<div className="text-center">
<Button variant="success" onClick={this.closeAll} disabled={this.state.allPending}>
{this.state.allPending ? <LoadingIcon gapRight /> : <Icon icon="boxes-packing" gapRight />}
<FormattedMessage id="app.reviewSolutionButtons.closeAll" defaultMessage="Close All Open Reviews" />
</Button>
</div>
) : null
}
noPadding
isOpen
customIcons={
state !== resourceStatus.PENDING &&
refresh && <RefreshIcon className="text-primary" timid onClick={refresh} gapRight />
}>
<>
{state === resourceStatus.PENDING && (
<div className="text-center my-3">
<LoadingIcon />
</div>
)}

{state === resourceStatus.FAILED && (
<div className="text-center my-3">
<WarningIcon className="text-danger" gapRight />
<FormattedMessage
id="app.pendingReviewsList.failed"
defaultMessage="Loading of the open reviews failed. Please try refreshing this component later."
/>
</div>
)}

{state === resourceStatus.FULFILLED &&
Object.keys(solutions).map(groupId => (
<div key={groupId} className="m-3">
<GroupIcon className="text-muted" gapRight />
<GroupsNameContainer groupId={groupId} fullName translations links />

{Object.keys(solutions[groupId]).map(assignmentId => (
<div key={assignmentId} className="ml-4 my-1">
<AssignmentIcon className="text-muted" gapRight />
<AssignmentNameContainer assignmentId={assignmentId} />

{solutions[groupId][assignmentId].map((solution, idx) => (
<div key={solution ? solution.id : `loading-${idx}`} className="ml-4">
{solution ? (
<>
<Link to={SOLUTION_SOURCE_CODES_URI_FACTORY(assignmentId, solution.id)}>
<span>
<UsersNameContainer userId={solution.authorId} isSimple noAutoload />
</span>
#{solution.attemptIndex}{' '}
<span className="px-1 text-muted">
<FormattedMessage id="app.pendingReviewsList.submitted" defaultMessage="submitted" />
</span>{' '}
<DateTime unixts={solution.createdAt} showTime={false} /> (
<DateTime unixts={solution.createdAt} showDate={false} />){' '}
</Link>
<span className="px-1 text-muted">
<FormattedMessage
id="app.pendingReviewsList.reviewOpenedAt"
defaultMessage="review opened at"
/>{' '}
</span>
<DateTime unixts={solution.review.startedAt} showTime={false} /> (
<DateTime unixts={solution.review.startedAt} showDate={false} />)
{closeReview && (
<Button
size="xs"
variant="success"
className="ml-3"
disabled={updatingSelector(solution.id)}
onClick={() => closeReview({ groupId, assignmentId, solutionId: solution.id })}>
{updatingSelector(solution.id) ? (
<LoadingIcon gapRight />
) : (
<Icon icon="boxes-packing" gapRight />
)}
<FormattedMessage id="app.reviewSolutionButtons.close" defaultMessage="Close Review" />
</Button>
)}
</>
) : (
<LoadingIcon />
)}
</div>
))}
</div>
))}
</div>
))}
</>
</Box>
);
}
}

PendingReviewsList.propTypes = {
solutions: PropTypes.object,
state: PropTypes.string,
updatingSelector: PropTypes.func.isRequired,
closeReview: PropTypes.func,
refresh: PropTypes.func,
links: PropTypes.object,
};

export default withLinks(PendingReviewsList);
2 changes: 2 additions & 0 deletions src/components/Solutions/PendingReviewsList/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import PendingReviewsList from './PendingReviewsList';
export default PendingReviewsList;
34 changes: 34 additions & 0 deletions src/components/helpers/SourceCodeViewer/SourceCodeViewer.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,37 @@
.sourceCodeViewerComments > div > .recodex-markdown-container p:last-child {
margin-bottom: 0.2rem;
}

.sourceCodeViewer .scvAddButton {
width: 0;
height: 0;
line-height: 0;
font-size: 0;
display: block;
margin: 0;
padding: 0;
position:relative;
}

.sourceCodeViewer.addComment *:hover + .scvAddButton::before, .sourceCodeViewer.addComment .scvAddButton:hover::before {
content: '+';
font-size: 21px;
line-height: 18px;
text-align: center;
padding: 0;
width: 21px;
height: 21px;
color: #ddf;
background-color: #77c;
position: absolute;
top: -23px;
left: 3.2em;
border-radius: 5px;
text-shadow: none;
cursor: pointer;
opacity: 0.7;
}

.sourceCodeViewer.addComment .scvAddButton:hover::before {
opacity: 1;
}
14 changes: 8 additions & 6 deletions src/components/helpers/SourceCodeViewer/SourceCodeViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ class SourceCodeViewer extends React.Component {
state = { activeLine: null, newComment: null, editComment: null, editInitialValues: null };

lineClickHandler = ev => {
ev.stopPropagation();
window.getSelection()?.removeAllRanges();

// opens new comment form if no other form is currently open
const lineNumber = parseInt(ev.target.dataset.line);
const target = ev.target.closest('*[data-line]');
const lineNumber = target && parseInt(target.dataset.line);
if (lineNumber && !isNaN(lineNumber) && !this.state.activeLine) {
ev.stopPropagation();
window.getSelection()?.removeAllRanges();
this.setState({ activeLine: lineNumber, newComment: lineNumber });
}
};
Expand Down Expand Up @@ -172,6 +172,8 @@ class SourceCodeViewer extends React.Component {
key: `cseg${lineNumber}`,
})}

<span className="scvAddButton" data-line={lineNumber} onClick={this.lineClickHandler}></span>

{(comments[lineNumber] || (this.state.newComment && this.state.newComment === lineNumber)) && (
<div className="sourceCodeViewerComments">
{(comments[lineNumber] || []).map(comment =>
Expand Down Expand Up @@ -214,12 +216,12 @@ class SourceCodeViewer extends React.Component {
});

render() {
const { name, content = '' } = this.props;
const { name, content = '', addComment } = this.props;
return canUseDOM ? (
<SyntaxHighlighter
language={getPrismModeFromExtension(getFileExtensionLC(name))}
style={vs}
className="sourceCodeViewer"
className={addComment && !this.state.activeLine ? 'sourceCodeViewer addComment' : 'sourceCodeViewer'}
showLineNumbers={true}
showInlineLineNumbers={true}
wrapLines={true}
Expand Down
4 changes: 2 additions & 2 deletions src/containers/UsersNameContainer/UsersNameContainer.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
white-space: nowrap;
}

.simpleName:after {
.simpleName::after {
content: ', ';
}

Expand All @@ -14,6 +14,6 @@
margin-right: 3px;
}

.simpleName:last-of-type:after {
.simpleName:last-of-type::after {
content: '';
}
6 changes: 6 additions & 0 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,11 @@
"app.passwordStrength.somewhatOk": "Šlo by to i lépe.",
"app.passwordStrength.unknown": "...",
"app.passwordStrength.worst": "Nevyhovující",
"app.pendingReviewsList.by": "od",
"app.pendingReviewsList.failed": "Načítání otevřených revizí řešení selhalo. Prosíme, zkuste po chvíli občerstvit tuto komponentu.",
"app.pendingReviewsList.reviewOpenedAt": "revize otevřena",
"app.pendingReviewsList.submitted": "odevzdáno",
"app.pendingReviewsList.title": "Všechny otevřené revize",
"app.pipeline.associatedExercises": "Přidružené úlohy",
"app.pipeline.description": "Popis",
"app.pipeline.failedDetail": "Načítání detailů pipeline selhalo. Ujistěte se prosím, že jste připojeni k Internetu a zkuste to později.",
Expand Down Expand Up @@ -1444,6 +1449,7 @@
"app.reviewCommentForm.suppressNotification": "Neodesílat oznámení",
"app.reviewCommentForm.suppressNotificationExplanation": "Poté, co byla revize uzavřena, se s každou provedenou změnou posílá oznámení autorovi. Můžete potlačit odeslání oznámení, pokud provádíte pouze drobnou úpravu.",
"app.reviewSolutionButtons.close": "Uzavřít revizi",
"app.reviewSolutionButtons.closeAll": "Zavřit všechny otevřené revize",
"app.reviewSolutionButtons.closePendingReviews": "Uzavřít probíhající revize",
"app.reviewSolutionButtons.delete": "Smazat revizi",
"app.reviewSolutionButtons.deleteConfirm": "Všechny komentáře v kódu budou smazány společně s revizí. Opravdu si přejete smazat?",
Expand Down
6 changes: 6 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,11 @@
"app.passwordStrength.somewhatOk": "You can do better.",
"app.passwordStrength.unknown": "...",
"app.passwordStrength.worst": "Unsatisfactory",
"app.pendingReviewsList.by": "by",
"app.pendingReviewsList.failed": "Loading of the open reviews failed. Please try refreshing this component later.",
"app.pendingReviewsList.reviewOpenedAt": "review opened at",
"app.pendingReviewsList.submitted": "submitted",
"app.pendingReviewsList.title": "All open reviews",
"app.pipeline.associatedExercises": "Associated exercises",
"app.pipeline.description": "Description",
"app.pipeline.failedDetail": "Loading the details of the pipeline failed. Please make sure you are connected to the Internet and try again later.",
Expand Down Expand Up @@ -1444,6 +1449,7 @@
"app.reviewCommentForm.suppressNotification": "Suppress e-mail notification",
"app.reviewCommentForm.suppressNotificationExplanation": "When the review is closed, a notification is sent to the author with every change. You may suppress the notification if the change you are performing is not significant.",
"app.reviewSolutionButtons.close": "Close Review",
"app.reviewSolutionButtons.closeAll": "Close All Open Reviews",
"app.reviewSolutionButtons.closePendingReviews": "Close pending reviews",
"app.reviewSolutionButtons.delete": "Erase Review",
"app.reviewSolutionButtons.deleteConfirm": "All review comments will be erased as well. Do you wish to proceed?",
Expand Down
Loading

0 comments on commit bfe7a42

Please sign in to comment.