+
+
+ )
+ }
+ style={styles.cardHeader}
+ subheader={`${formatLongDateTime(createdAt)}${
+ isDelayed ? ' (delayed comment)' : ''
+ }`}
+ subheaderTypographyProps={{ display: 'block' }}
+ title={
+ isAiGenerated && isDraft ? (
+
+ ) : (
+ creator.name
+ )
+ }
+ titleTypographyProps={{
+ display: 'block',
+ fontSize: '1.5rem',
+ }}
+ />
+
+ {isDraft && (
+ }>
+ publishComment?.(editValue)}
+ >
+
+
+
+ )}
+ {canUpdate && isUpdatingAnnotationAllowed ? (
+
+ ) : null}
+ {canDestroy && isUpdatingAnnotationAllowed ? (
+
+ }
+ disabled={false}
+ // Not awaited, so the prompt closes on confirmation as it always has, and cannot be confirmed twice
+ // while the request is in flight.
+ onClick={async (): Promise => deleteComment()}
+ />
+ ) : null}
+
+
+
{renderCommentContent()}
+
+ );
+};
+
+export default CommentCard;
diff --git a/client/app/bundles/course/assessment/submission/containers/Comments.jsx b/client/app/bundles/course/assessment/submission/containers/Comments.jsx
deleted file mode 100644
index 413ba2e9f5d..00000000000
--- a/client/app/bundles/course/assessment/submission/containers/Comments.jsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import { Component } from 'react';
-import { FormattedMessage } from 'react-intl';
-import { connect } from 'react-redux';
-import { Typography } from '@mui/material';
-import PropTypes from 'prop-types';
-
-import { POST_WORKFLOW_STATE } from 'lib/constants/sharedConstants';
-import toast from 'lib/hooks/toast';
-
-import * as commentActions from '../actions/comments';
-import AiFeedbackCommentCard from '../components/comment/AiFeedbackCommentCard';
-import CommentCard from '../components/comment/CommentCard';
-import CommentField from '../components/comment/CommentField';
-import { workflowStates } from '../constants';
-import { postShape, topicShape } from '../propTypes';
-import translations from '../translations';
-
-class VisibleComments extends Component {
- static newCommentIdentifier(field) {
- return `topic_${field}`;
- }
-
- render() {
- const {
- commentForms,
- posts,
- topic,
- handleCreateChange,
- handleUpdateChange,
- createComment,
- updateComment,
- deleteComment,
- publishComment,
- rateComment,
- acceptComment,
- rejectComment,
- graderView,
- renderDelayedCommentButton,
- } = this.props;
-
- return (
-
-
-
-
-
- {posts.map((post) => {
- const isVisible =
- graderView ||
- (!post.isDelayed &&
- post.workflowState !== POST_WORKFLOW_STATE.draft);
- if (!isVisible) {
- return null;
- }
-
- // An AI-generated draft still awaiting a staff decision uses the rateable card (rate -> edit ->
- // accept/reject) in place of the plain comment card + Publish button.
- if (
- post.isAiGenerated &&
- post.workflowState === POST_WORKFLOW_STATE.draft &&
- post.generatedRating
- ) {
- return (
- // Key on createdAt so a re-generated draft (fresh timestamp) remounts with the new content,
- // resetting the card's local edit state.
-
acceptComment(post.id, value)}
- deleteComment={() => deleteComment(post.id)}
- post={post}
- rateComment={(rating) => rateComment(post.id, rating)}
- rejectComment={(value) => rejectComment(post.id, value)}
- />
- );
- }
-
- return (
- deleteComment(post.id)}
- editValue={commentForms.posts[post.id]}
- handleChange={(value) => handleUpdateChange(post.id, value)}
- isUpdatingAnnotationAllowed
- post={post}
- publishComment={(value) => publishComment(post.id, value)}
- updateComment={(value) => updateComment(post.id, value)}
- />
- );
- })}
-
-
-
- );
- }
-}
-
-VisibleComments.propTypes = {
- commentForms: PropTypes.shape({
- topics: PropTypes.objectOf(PropTypes.string),
- posts: PropTypes.objectOf(PropTypes.string),
- isSubmittingNormalComment: PropTypes.bool,
- isSubmittingDelayedComment: PropTypes.bool,
- isUpdatingComment: PropTypes.bool,
- }),
- posts: PropTypes.arrayOf(postShape),
- topic: topicShape,
- graderView: PropTypes.bool.isRequired,
- renderDelayedCommentButton: PropTypes.bool,
-
- handleCreateChange: PropTypes.func.isRequired,
- handleUpdateChange: PropTypes.func.isRequired,
- createComment: PropTypes.func.isRequired,
- updateComment: PropTypes.func.isRequired,
- deleteComment: PropTypes.func.isRequired,
- publishComment: PropTypes.func.isRequired,
- rateComment: PropTypes.func.isRequired,
- acceptComment: PropTypes.func.isRequired,
- rejectComment: PropTypes.func.isRequired,
-};
-
-function mapStateToProps({ assessments: { submission } }, ownProps) {
- const { topic } = ownProps;
- const renderDelayedCommentButton =
- submission.submission.graderView &&
- !submission.assessment.autograded &&
- (submission.submission.workflowState === workflowStates.Submitted ||
- submission.submission.workflowState === workflowStates.Graded);
- return {
- commentForms: submission.commentForms,
- posts: submission.topics[topic.id].postIds.map(
- (postId) => submission.posts[postId],
- ),
- graderView: submission.submission.graderView,
- renderDelayedCommentButton,
- };
-}
-
-function mapDispatchToProps(dispatch, ownProps) {
- const { topic } = ownProps;
-
- return {
- handleCreateChange: (comment) =>
- dispatch(commentActions.onCreateChange(topic.id, comment)),
- handleUpdateChange: (postId, comment) =>
- dispatch(commentActions.onUpdateChange(postId, comment)),
- createComment: (comment, isDelayedComment = false) =>
- dispatch(
- commentActions.create(
- topic.submissionQuestionId,
- comment,
- isDelayedComment,
- ),
- )
- .then(() => toast.success('Successfully created comment.'))
- .catch(() => toast.error('Failed to create comment.')),
- updateComment: (postId, comment) =>
- dispatch(commentActions.update(topic.id, postId, comment)),
- deleteComment: (postId) =>
- dispatch(commentActions.destroy(topic.id, postId)),
- publishComment: (postId, comment) =>
- dispatch(commentActions.publish(topic.id, postId, comment)),
- rateComment: (postId, rating) =>
- dispatch(commentActions.rateAiFeedback(topic.id, postId, rating)),
- acceptComment: (postId, comment) =>
- dispatch(commentActions.acceptAiFeedback(topic.id, postId, comment)),
- rejectComment: (postId, comment) =>
- dispatch(commentActions.rejectAiFeedback(topic.id, postId, comment)),
- };
-}
-
-const Comments = connect(mapStateToProps, mapDispatchToProps)(VisibleComments);
-export default Comments;
diff --git a/client/app/bundles/course/assessment/submission/containers/Comments.tsx b/client/app/bundles/course/assessment/submission/containers/Comments.tsx
new file mode 100644
index 00000000000..830b37ef6e1
--- /dev/null
+++ b/client/app/bundles/course/assessment/submission/containers/Comments.tsx
@@ -0,0 +1,155 @@
+import { FC } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { Typography } from '@mui/material';
+
+import { POST_WORKFLOW_STATE } from 'lib/constants/sharedConstants';
+import { useAppDispatch, useAppSelector } from 'lib/hooks/store';
+import toast from 'lib/hooks/toast';
+
+import * as commentActions from '../actions/comments';
+import AiFeedbackCommentCard from '../components/comment/AiFeedbackCommentCard';
+import CommentCard from '../components/comment/CommentCard';
+import CommentField from '../components/comment/CommentField';
+import { workflowStates } from '../constants';
+import { getAssessment } from '../selectors/assessments';
+import { getCommentForms } from '../selectors/commentForms';
+import { getCommentPosts } from '../selectors/comments';
+import { getSubmission } from '../selectors/submissions';
+import { getTopics } from '../selectors/topics';
+import translations from '../translations';
+import { Topic } from '../types';
+
+// DOM id of the new-comment box, which the submission feature specs locate it by.
+const newCommentIdentifier = (topicId: number): string => `topic_${topicId}`;
+
+interface Props {
+ topic: Topic;
+}
+
+// The comment thread under one submission question.
+const Comments: FC