SubmissionTray.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. /*
  2. * Copyright (C) 2017 - present Instructure, Inc.
  3. *
  4. * This file is part of Canvas.
  5. *
  6. * Canvas is free software: you can redistribute it and/or modify it under
  7. * the terms of the GNU Affero General Public License as published by the Free
  8. * Software Foundation, version 3 of the License.
  9. *
  10. * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
  11. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. * details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License along
  16. * with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. import React from 'react';
  19. import { arrayOf, bool, func, number, shape, string } from 'prop-types';
  20. import I18n from 'i18n!gradebook';
  21. import Avatar from 'instructure-ui/lib/components/Avatar';
  22. import Button from 'instructure-ui/lib/components/Button';
  23. import Container from 'instructure-ui/lib/components/Container';
  24. import Heading from 'instructure-ui/lib/components/Heading';
  25. import Link from 'instructure-ui/lib/components/Link';
  26. import Spinner from 'instructure-ui/lib/components/Spinner';
  27. import Tray from 'instructure-ui/lib/components/Tray';
  28. import Typography from 'instructure-ui/lib/components/Typography';
  29. import IconSpeedGraderLine from 'instructure-icons/lib/Line/IconSpeedGraderLine';
  30. import Carousel from 'jsx/gradezilla/default_gradebook/components/Carousel';
  31. import GradeInput from 'jsx/gradezilla/default_gradebook/components/GradeInput';
  32. import LatePolicyGrade from 'jsx/gradezilla/default_gradebook/components/LatePolicyGrade';
  33. import CommentPropTypes from 'jsx/gradezilla/default_gradebook/propTypes/CommentPropTypes';
  34. import SubmissionCommentListItem from 'jsx/gradezilla/default_gradebook/components/SubmissionCommentListItem';
  35. import SubmissionCommentCreateForm from 'jsx/gradezilla/default_gradebook/components/SubmissionCommentCreateForm';
  36. import SubmissionStatus from 'jsx/gradezilla/default_gradebook/components/SubmissionStatus';
  37. import SubmissionTrayRadioInputGroup from 'jsx/gradezilla/default_gradebook/components/SubmissionTrayRadioInputGroup';
  38. function renderAvatar (name, avatarUrl) {
  39. return (
  40. <div id="SubmissionTray__Avatar">
  41. <Avatar name={name} src={avatarUrl} size="auto" />
  42. </div>
  43. );
  44. }
  45. function renderSpeedGraderLink (speedGraderUrl) {
  46. return (
  47. <Container as="div" textAlign="center">
  48. <Button href={speedGraderUrl} variant="link">
  49. <IconSpeedGraderLine />
  50. {I18n.t('SpeedGrader')}
  51. </Button>
  52. </Container>
  53. );
  54. }
  55. function renderTraySubHeading (headingText) {
  56. return (
  57. <Heading level="h4" as="h2" margin="auto auto small">
  58. <Typography weight="bold">
  59. {headingText}
  60. </Typography>
  61. </Heading>
  62. );
  63. }
  64. export default class SubmissionTray extends React.Component {
  65. static defaultProps = {
  66. contentRef: undefined,
  67. gradingDisabled: false,
  68. latePolicy: { lateSubmissionInterval: 'day' },
  69. submission: { drop: false }
  70. };
  71. static propTypes = {
  72. assignment: shape({
  73. name: string.isRequired,
  74. htmlUrl: string.isRequired,
  75. muted: bool.isRequired,
  76. published: bool.isRequired
  77. }).isRequired,
  78. contentRef: func,
  79. currentUserId: string.isRequired,
  80. editedCommentId: string,
  81. editSubmissionComment: func.isRequired,
  82. gradingDisabled: bool,
  83. isOpen: bool.isRequired,
  84. colors: shape({
  85. late: string.isRequired,
  86. missing: string.isRequired,
  87. excused: string.isRequired
  88. }).isRequired,
  89. onClose: func.isRequired,
  90. onGradeSubmission: func.isRequired,
  91. onRequestClose: func.isRequired,
  92. student: shape({
  93. id: string.isRequired,
  94. name: string.isRequired,
  95. avatarUrl: string,
  96. gradesUrl: string.isRequired
  97. }).isRequired,
  98. submission: shape({
  99. drop: bool,
  100. excused: bool.isRequired,
  101. grade: string,
  102. late: bool.isRequired,
  103. missing: bool.isRequired,
  104. pointsDeducted: number,
  105. secondsLate: number.isRequired,
  106. assignmentId: string.isRequired
  107. }),
  108. isFirstAssignment: bool.isRequired,
  109. isLastAssignment: bool.isRequired,
  110. selectNextAssignment: func.isRequired,
  111. selectPreviousAssignment: func.isRequired,
  112. isFirstStudent: bool.isRequired,
  113. isLastStudent: bool.isRequired,
  114. selectNextStudent: func.isRequired,
  115. selectPreviousStudent: func.isRequired,
  116. courseId: string.isRequired,
  117. speedGraderEnabled: bool.isRequired,
  118. submissionUpdating: bool.isRequired,
  119. updateSubmission: func.isRequired,
  120. updateSubmissionComment: func.isRequired,
  121. locale: string.isRequired,
  122. latePolicy: shape({
  123. lateSubmissionInterval: string
  124. }).isRequired,
  125. submissionComments: arrayOf(shape(CommentPropTypes).isRequired).isRequired,
  126. submissionCommentsLoaded: bool.isRequired,
  127. createSubmissionComment: func.isRequired,
  128. deleteSubmissionComment: func.isRequired,
  129. processing: bool.isRequired,
  130. setProcessing: func.isRequired,
  131. isInOtherGradingPeriod: bool.isRequired,
  132. isInClosedGradingPeriod: bool.isRequired,
  133. isInNoGradingPeriod: bool.isRequired
  134. };
  135. cancelCommenting = () => {
  136. this.props.editSubmissionComment(null);
  137. };
  138. renderSubmissionCommentList () {
  139. return this.props.submissionComments.map(comment => (
  140. <SubmissionCommentListItem
  141. author={comment.author}
  142. cancelCommenting={this.cancelCommenting}
  143. currentUserIsAuthor={this.props.currentUserId === comment.authorId}
  144. authorUrl={comment.authorUrl}
  145. authorAvatarUrl={comment.authorAvatarUrl}
  146. comment={comment.comment}
  147. createdAt={comment.createdAt}
  148. editedAt={comment.editedAt}
  149. editing={!!this.props.editedCommentId && this.props.editedCommentId === comment.id}
  150. id={comment.id}
  151. key={comment.id}
  152. last={this.props.submissionComments[this.props.submissionComments.length - 1].id === comment.id}
  153. deleteSubmissionComment={this.props.deleteSubmissionComment}
  154. editSubmissionComment={this.props.editSubmissionComment}
  155. updateSubmissionComment={this.props.updateSubmissionComment}
  156. processing={this.props.processing}
  157. setProcessing={this.props.setProcessing}
  158. />
  159. ));
  160. }
  161. renderSubmissionComments () {
  162. if (this.props.submissionCommentsLoaded) {
  163. return (
  164. <div>
  165. {renderTraySubHeading('Comments')}
  166. {this.renderSubmissionCommentList()}
  167. {
  168. !this.props.editedCommentId &&
  169. <SubmissionCommentCreateForm
  170. cancelCommenting={this.cancelCommenting}
  171. createSubmissionComment={this.props.createSubmissionComment}
  172. processing={this.props.processing}
  173. setProcessing={this.props.setProcessing}
  174. />
  175. }
  176. </div>
  177. );
  178. }
  179. return (
  180. <div style={{ textAlign: 'center' }}>
  181. <Spinner title={I18n.t('Loading comments')} size="large" />
  182. </div>
  183. );
  184. }
  185. render () {
  186. const { name, avatarUrl } = this.props.student;
  187. const assignmentParam = `assignment_id=${this.props.submission.assignmentId}`;
  188. const studentParam = `#%7B%22student_id%22%3A${this.props.student.id}%7D`;
  189. const speedGraderUrl = `/courses/${this.props.courseId}/gradebook/speed_grader?${assignmentParam}${studentParam}`;
  190. const submissionCommentsProps = {
  191. submissionComments: this.props.submissionComments,
  192. submissionCommentsLoaded: this.props.submissionCommentsLoaded,
  193. deleteSubmissionComment: this.props.deleteSubmissionComment,
  194. createSubmissionComment: this.props.createSubmissionComment,
  195. processing: this.props.processing,
  196. setProcessing: this.props.setProcessing,
  197. };
  198. const trayIsBusy = this.props.processing || this.props.submissionUpdating || !this.props.submissionCommentsLoaded;
  199. let carouselContainerStyleOverride = '0 0 0 0';
  200. if (!avatarUrl) {
  201. // When we don't have an avatar, let's ensure there's enough space between the tray close button and the student
  202. // carousel's previous student arrow
  203. carouselContainerStyleOverride = 'small 0 0 0';
  204. }
  205. return (
  206. <Tray
  207. contentRef={this.props.contentRef}
  208. label={I18n.t('Submission tray')}
  209. closeButtonLabel={I18n.t('Close submission tray')}
  210. applicationElement={() => document.getElementById('application')}
  211. open={this.props.isOpen}
  212. shouldContainFocus
  213. placement="end"
  214. onDismiss={this.props.onRequestClose}
  215. onClose={this.props.onClose}
  216. >
  217. <div className="SubmissionTray__Container">
  218. <div id="SubmissionTray__Content" style={{ display: 'flex', flexDirection: 'column' }}>
  219. <Container as="div" padding={carouselContainerStyleOverride}>
  220. {avatarUrl && renderAvatar(name, avatarUrl)}
  221. <Carousel
  222. id="student-carousel"
  223. disabled={trayIsBusy}
  224. displayLeftArrow={!this.props.isFirstStudent}
  225. displayRightArrow={!this.props.isLastStudent}
  226. leftArrowDescription={I18n.t('Previous student')}
  227. onLeftArrowClick={this.props.selectPreviousStudent}
  228. onRightArrowClick={this.props.selectNextStudent}
  229. rightArrowDescription={I18n.t('Next student')}
  230. >
  231. <Link href={this.props.student.gradesUrl}>
  232. {name}
  233. </Link>
  234. </Carousel>
  235. <Container as="div" margin="small 0" className="hr" />
  236. <Carousel
  237. id="assignment-carousel"
  238. disabled={trayIsBusy}
  239. displayLeftArrow={!this.props.isFirstAssignment}
  240. displayRightArrow={!this.props.isLastAssignment}
  241. leftArrowDescription={I18n.t('Previous assignment')}
  242. onLeftArrowClick={this.props.selectPreviousAssignment}
  243. onRightArrowClick={this.props.selectNextAssignment}
  244. rightArrowDescription={I18n.t('Next assignment')}
  245. >
  246. <Link href={this.props.assignment.htmlUrl}>
  247. {this.props.assignment.name}
  248. </Link>
  249. </Carousel>
  250. { this.props.speedGraderEnabled && renderSpeedGraderLink(speedGraderUrl) }
  251. <Container as="div" margin="small 0" className="hr" />
  252. </Container>
  253. <Container as="div" style={{ overflowY: 'auto', flex: '1 1 auto' }}>
  254. <SubmissionStatus
  255. assignment={this.props.assignment}
  256. submission={this.props.submission}
  257. isInOtherGradingPeriod={this.props.isInOtherGradingPeriod}
  258. isInClosedGradingPeriod={this.props.isInClosedGradingPeriod}
  259. isInNoGradingPeriod={this.props.isInNoGradingPeriod}
  260. />
  261. <GradeInput
  262. assignment={this.props.assignment}
  263. disabled={this.props.gradingDisabled}
  264. onSubmissionUpdate={this.props.onGradeSubmission}
  265. submission={this.props.submission}
  266. submissionUpdating={this.props.submissionUpdating}
  267. />
  268. {!!this.props.submission.pointsDeducted &&
  269. <Container as="div" margin="small 0 0 0">
  270. <LatePolicyGrade submission={this.props.submission} />
  271. </Container>
  272. }
  273. <Container as="div" margin="small 0" className="hr" />
  274. <Container as="div" id="SubmissionTray__RadioInputGroup" margin="0 0 small 0">
  275. <SubmissionTrayRadioInputGroup
  276. colors={this.props.colors}
  277. locale={this.props.locale}
  278. latePolicy={this.props.latePolicy}
  279. submission={this.props.submission}
  280. submissionUpdating={this.props.submissionUpdating}
  281. updateSubmission={this.props.updateSubmission}
  282. />
  283. </Container>
  284. <Container as="div" margin="small 0" className="hr" />
  285. <Container as="div" id="SubmissionTray__Comments" padding="xx-small">
  286. {this.renderSubmissionComments(submissionCommentsProps)}
  287. </Container>
  288. </Container>
  289. </div>
  290. </div>
  291. </Tray>
  292. );
  293. }
  294. }