Skip to content

Commit

Permalink
Review summary (a global comment that is not attached to any file:lin…
Browse files Browse the repository at this point in the history
…e) added.
  • Loading branch information
krulis-martin committed Jul 25, 2023
1 parent 666c4b9 commit e5ccc23
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 96 deletions.
125 changes: 125 additions & 0 deletions src/components/Solutions/ReviewSummary/ReviewSummary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';

import ReviewCommentForm, { newCommentFormInitialValues } from '../../forms/ReviewCommentForm';
import SourceCodeComment from '../../helpers/SourceCodeViewer/SourceCodeComment';
import Button from '../../widgets/TheButton';
import Box from '../../widgets/Box';
import { AddIcon } from '../../icons';

class ReviewSummary extends Component {
state = { add: false, edit: null, editInitialValues: null };

startCreation = () => this.setState({ add: true });
close = () => this.setState({ add: false, edit: null });

createNewComment = ({ text, issue, suppressNotification = false }) => {
const { addComment } = this.props;
text = text.trim();
return addComment({
text,
issue,
suppressNotification,
file: '',
line: 0,
}).then(this.close);
};

startEditting = ({ id, text, issue }) => {
this.setState({ edit: id, editInitialValues: { text, issue, suppressNotification: false } });
};

editComment = ({ text, issue, suppressNotification = false }) => {
const { updateComment } = this.props;
text = text.trim();
if (!text || !this.state.edit) {
this.close();
return Promise.resove();
}

return updateComment({
id: this.state.edit,
text,
issue,
suppressNotification,
}).then(this.close);
};

render() {
const {
reviewComments = [],
reviewClosed = false,
authorView = false,
restrictCommentAuthor = null,
addComment = null,
updateComment = null,
removeComment = null,
} = this.props;

return (
<Box
title={<FormattedMessage id="app.solutionSourceCodes.reviewSummary.title" defaultMessage="Review summary" />}
noPadding
unlimitedHeight
isOpen>
<div className="sourceCodeViewerComments reviewSummary">
{reviewComments.map(comment =>
this.state.edit === comment.id && updateComment ? (
<ReviewCommentForm
key={comment.id}
form={`review-summary-${comment.id}`}
initialValues={this.state.editInitialValues}
createdAt={comment.createdAt}
authorId={comment.author}
onCancel={this.close}
onSubmit={this.editComment}
showSuppressor={this.props.reviewClosed}
/>
) : (
<SourceCodeComment
key={comment.id}
comment={comment}
authorView={authorView}
restrictCommentAuthor={restrictCommentAuthor}
startEditting={updateComment ? this.startEditting : null}
removeComment={removeComment}
/>
)
)}

{reviewComments.length === 0 && this.state.add && addComment && (
<ReviewCommentForm
form="review-summary-new"
initialValues={newCommentFormInitialValues}
onCancel={this.close}
onSubmit={this.createNewComment}
showSuppressor={reviewClosed}
/>
)}

{reviewComments.length === 0 && addComment && !this.state.add && (
<div className="text-center p-3">
<Button onClick={this.startCreation} size="sm" variant="success">
<AddIcon gapRight />
<FormattedMessage id="generic.create" defaultMessage="Create" />
</Button>
</div>
)}
</div>
</Box>
);
}
}

ReviewSummary.propTypes = {
reviewComments: PropTypes.array,
reviewClosed: PropTypes.bool,
authorView: PropTypes.bool,
restrictCommentAuthor: PropTypes.string,
addComment: PropTypes.func,
updateComment: PropTypes.func,
removeComment: PropTypes.func,
};

export default ReviewSummary;
1 change: 1 addition & 0 deletions src/components/Solutions/ReviewSummary/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ReviewSummary';
2 changes: 1 addition & 1 deletion src/components/Solutions/SolutionActions/ActionButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ ActionButton.propTypes = {
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
shortLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired,
confirm: PropTypes.string,
confirm: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
pending: PropTypes.bool,
captionAsTooltip: PropTypes.bool,
size: PropTypes.string,
Expand Down
17 changes: 10 additions & 7 deletions src/components/Solutions/SolutionActions/SolutionActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const SolutionActions = ({
const evalPoints = safeGet(solution, ['lastSubmission', 'evaluation', 'points']);
const maxPoints = assignment.maxPointsBeforeFirstDeadline;

const openReview = setReviewState && (() => setReviewState(false));
const actionHandlers = {
accept: !accepted && setAccepted && (() => setAccepted(true)),
unaccept: accepted && setAccepted && (() => setAccepted(false)),
Expand All @@ -150,18 +151,20 @@ const SolutionActions = ({
(() => setPoints({ bonusPoints, overriddenPoints: maxPoints })),
clearPoints:
setPoints && solution.overriddenPoints !== null && (() => setPoints({ bonusPoints, overriddenPoints: null })),
open: setReviewState && (!review || !review.startedAt) && (() => setReviewState(false)),
reopen: setReviewState && review && review.closedAt && (() => setReviewState(false)),
open:
openReview &&
(!review || !review.startedAt) &&
(isOnReviewPage ? openReview : () => openReview().then(() => push(reviewPageUri))),
reopen:
openReview &&
review &&
review.closedAt &&
(isOnReviewPage ? openReview : () => openReview().then(() => push(reviewPageUri))),
openClose: setReviewState && (!review || !review.startedAt) && showAllButtons && (() => setReviewState(true)),
close: setReviewState && review && review.startedAt && !review.closedAt && (() => setReviewState(true)),
delete: showAllButtons && review && review.startedAt && deleteReview,
};

if (!isOnReviewPage) {
actionHandlers.open = actionHandlers.open && (() => actionHandlers.open().then(() => push(reviewPageUri)));
actionHandlers.reopen = actionHandlers.reopen && (() => actionHandlers.reopen().then(() => push(reviewPageUri)));
}

const pendingIndicators = { acceptPending, updatePending, pointsPending };
const actions = knownActions
.filter(a => actionHandlers[a])
Expand Down
6 changes: 6 additions & 0 deletions src/components/forms/ReviewCommentForm/ReviewCommentForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import { CheckboxField, MarkdownTextAreaField } from '../Fields';

const textValidator = text => !text || text.trim() === '';

export const newCommentFormInitialValues = {
text: '',
issue: false,
suppressNotification: false,
};

const ReviewCommentForm = ({
authorId = null,
createdAt = null,
Expand Down
3 changes: 1 addition & 2 deletions src/components/forms/ReviewCommentForm/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
import ReviewCommentForm from './ReviewCommentForm';
export default ReviewCommentForm;
export { default, newCommentFormInitialValues } from './ReviewCommentForm';
92 changes: 92 additions & 0 deletions src/components/helpers/SourceCodeViewer/SourceCodeComment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { FormattedMessage } from 'react-intl';
import classnames from 'classnames';

import UsersNameContainer from '../../../containers/UsersNameContainer';
import Confirm from '../../forms/Confirm';
import DateTime from '../../widgets/DateTime';
import Markdown from '../../widgets/Markdown';
import Icon, { DeleteIcon, EditIcon, LoadingIcon, WarningIcon } from '../../icons';

import './SourceCodeViewer.css';

const SourceCodeComment = ({
comment,
authorView = false,
startEditting = null,
removeComment = null,
restrictCommentAuthor = null,
}) => {
return (
<div className={classnames({ issue: comment.issue, 'half-opaque': comment.removing })}>
<span className="icon">
{comment.issue ? (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id={`issue-${comment.id}`}>
{authorView ? (
<FormattedMessage
id="app.sourceCodeViewer.issueTooltipForAuthor"
defaultMessage="This comment is marked as an issue, which means you are expected to fix it in your next submission."
/>
) : (
<FormattedMessage
id="app.sourceCodeViewer.issueTooltip"
defaultMessage="This comment is marked as an issue, which means the author is expected to fix it in the next submission."
/>
)}
</Tooltip>
}>
<WarningIcon className="text-danger" gapRight />
</OverlayTrigger>
) : (
<Icon icon={['far', 'comment']} className="almost-transparent" gapRight />
)}
</span>

<small>
<UsersNameContainer userId={comment.author} showEmail="icon" />
<span className="actions">
{comment.removing && <LoadingIcon />}

{startEditting &&
!comment.removing &&
(!restrictCommentAuthor || restrictCommentAuthor === comment.author) && (
<EditIcon className="text-warning" gapRight onClick={() => startEditting(comment)} />
)}
{removeComment && !comment.removing && (!restrictCommentAuthor || restrictCommentAuthor === comment.author) && (
<Confirm
id={`delcfrm-${comment.id}`}
onConfirmed={() => removeComment(comment.id)}
question={
<FormattedMessage
id="app.sourceCodeViewer.deleteCommentConfirm"
defaultMessage="Do you really wish to remove this comment? This operation cannot be undone."
/>
}>
<DeleteIcon className="text-danger" gapRight />
</Confirm>
)}
</span>
</small>

<small className="float-right mr-2">
<DateTime unixts={comment.createdAt} showRelative />
</small>
<Markdown source={comment.text} />
</div>
);
};

SourceCodeComment.propTypes = {
comment: PropTypes.object.isRequired,
startEditting: PropTypes.func,
removeComment: PropTypes.func,
authorView: PropTypes.bool,
restrictCommentAuthor: PropTypes.string,
};

export default SourceCodeComment;
17 changes: 16 additions & 1 deletion src/components/helpers/SourceCodeViewer/SourceCodeViewer.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@
.sourceCodeViewerComments {
background-color: #f7f7f7;
font-family: var(--font-family-sans-serif);
font-size: 127%;
padding: 0.2em;
white-space: initial;
}

pre .sourceCodeViewerComments, code .sourceCodeViewerComments {
font-size: 127%;
}

.sourceCodeViewerComments > div {
border: 1px solid #e0e0e0;
background-color: white;
Expand Down Expand Up @@ -103,6 +106,18 @@
margin-bottom: 0.2rem;
}

/* Overrides for review summary which is slightly different... */
.reviewSummary > div > .recodex-markdown-container {
padding-left: 0.5rem;
}

.reviewSummary > div > .icon {
position: static;
margin: 0 0.5em;
}


/* Special add button */
.sourceCodeViewer .scvAddButton {
width: 0;
height: 0;
Expand Down

0 comments on commit e5ccc23

Please sign in to comment.