AssignmentColumnHeader.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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, instanceOf, number, shape, string } from 'prop-types';
  20. import IconMoreSolid from 'instructure-icons/lib/Solid/IconMoreSolid';
  21. import IconMutedSolid from 'instructure-icons/lib/Solid/IconMutedSolid';
  22. import IconWarningSolid from 'instructure-icons/lib/Solid/IconWarningSolid';
  23. import Link from 'instructure-ui/lib/components/Link';
  24. import {
  25. MenuItem,
  26. MenuItemFlyout,
  27. MenuItemGroup,
  28. MenuItemSeparator
  29. } from 'instructure-ui/lib/components/Menu';
  30. import PopoverMenu from 'instructure-ui/lib/components/PopoverMenu';
  31. import Typography from 'instructure-ui/lib/components/Typography';
  32. import 'message_students';
  33. import MessageStudentsWhoHelper from 'jsx/gradezilla/shared/helpers/messageStudentsWhoHelper';
  34. import I18n from 'i18n!gradebook';
  35. import ScreenReaderContent from 'instructure-ui/lib/components/ScreenReaderContent';
  36. import ColumnHeader from 'jsx/gradezilla/default_gradebook/components/ColumnHeader';
  37. class AssignmentColumnHeader extends ColumnHeader {
  38. static propTypes = {
  39. ...ColumnHeader.propTypes,
  40. assignment: shape({
  41. courseId: string.isRequired,
  42. htmlUrl: string.isRequired,
  43. id: string.isRequired,
  44. muted: bool.isRequired,
  45. name: string.isRequired,
  46. omitFromFinalGrade: bool.isRequired,
  47. pointsPossible: number,
  48. published: bool.isRequired,
  49. submissionTypes: arrayOf(string).isRequired
  50. }).isRequired,
  51. curveGradesAction: shape({
  52. isDisabled: bool.isRequired,
  53. onSelect: func.isRequired
  54. }).isRequired,
  55. sortBySetting: shape({
  56. direction: string.isRequired,
  57. disabled: bool.isRequired,
  58. isSortColumn: bool.isRequired,
  59. onSortByGradeAscending: func.isRequired,
  60. onSortByGradeDescending: func.isRequired,
  61. onSortByLate: func.isRequired,
  62. onSortByMissing: func.isRequired,
  63. onSortByUnposted: func.isRequired,
  64. settingKey: string.isRequired
  65. }).isRequired,
  66. students: arrayOf(shape({
  67. isInactive: bool.isRequired,
  68. id: string.isRequired,
  69. name: string.isRequired,
  70. submission: shape({
  71. score: number,
  72. submittedAt: instanceOf(Date)
  73. }).isRequired,
  74. })).isRequired,
  75. submissionsLoaded: bool.isRequired,
  76. setDefaultGradeAction: shape({
  77. disabled: bool.isRequired,
  78. onSelect: func.isRequired
  79. }).isRequired,
  80. downloadSubmissionsAction: shape({
  81. hidden: bool.isRequired,
  82. onSelect: func.isRequired
  83. }).isRequired,
  84. reuploadSubmissionsAction: shape({
  85. hidden: bool.isRequired,
  86. onSelect: func.isRequired
  87. }).isRequired,
  88. muteAssignmentAction: shape({
  89. disabled: bool.isRequired,
  90. onSelect: func.isRequired
  91. }).isRequired,
  92. onMenuClose: func.isRequired,
  93. showUnpostedMenuItem: bool.isRequired
  94. };
  95. static defaultProps = {
  96. ...ColumnHeader.defaultProps
  97. };
  98. static renderMutedIcon (screenreaderText) {
  99. return (
  100. <Typography weight="bold" fontStyle="normal" size="small" color="error">
  101. <IconMutedSolid title={screenreaderText} />
  102. </Typography>
  103. );
  104. }
  105. static renderWarningIcon (screenreaderText) {
  106. return (
  107. <Typography weight="bold" fontStyle="normal" size="small" color="brand">
  108. <IconWarningSolid title={screenreaderText} />
  109. </Typography>
  110. );
  111. }
  112. bindAssignmentLink = (ref) => { this.assignmentLink = ref };
  113. bindEnterGradesAsMenuContent = (ref) => { this.enterGradesAsMenuContent = ref };
  114. curveGrades = () => { this.invokeAndSkipFocus(this.props.curveGradesAction) };
  115. setDefaultGrades = () => { this.invokeAndSkipFocus(this.props.setDefaultGradeAction) };
  116. muteAssignment = () => { this.invokeAndSkipFocus(this.props.muteAssignmentAction) };
  117. downloadSubmissions = () => { this.invokeAndSkipFocus(this.props.downloadSubmissionsAction) };
  118. reuploadSubmissions = () => { this.invokeAndSkipFocus(this.props.reuploadSubmissionsAction) };
  119. invokeAndSkipFocus (action) {
  120. this.setState({ skipFocusOnClose: true });
  121. action.onSelect(this.focusAtEnd);
  122. }
  123. focusAtStart = () => { this.assignmentLink.focus() };
  124. handleKeyDown = (event) => {
  125. if (event.which === 9) {
  126. if (this.assignmentLink.focused && !event.shiftKey) {
  127. event.preventDefault();
  128. this.optionsMenuTrigger.focus();
  129. return false; // prevent Grid behavior
  130. }
  131. if (document.activeElement === this.optionsMenuTrigger && event.shiftKey) {
  132. event.preventDefault();
  133. this.assignmentLink.focus();
  134. return false; // prevent Grid behavior
  135. }
  136. }
  137. return ColumnHeader.prototype.handleKeyDown.call(this, event);
  138. };
  139. onEnterGradesAsSettingSelect = (_event, values) => {
  140. this.props.enterGradesAsSetting.onSelect(values[0]);
  141. }
  142. showMessageStudentsWhoDialog = () => {
  143. this.setState({ skipFocusOnClose: true });
  144. const settings = MessageStudentsWhoHelper.settings(this.props.assignment, this.activeStudentDetails());
  145. settings.onClose = this.focusAtEnd;
  146. window.messageStudents(settings);
  147. }
  148. activeStudentDetails () {
  149. const activeStudents = this.props.students.filter(student => !student.isInactive);
  150. return activeStudents.map((student) => {
  151. const { score, submittedAt } = student.submission;
  152. return {
  153. id: student.id,
  154. name: student.name,
  155. score,
  156. submittedAt
  157. };
  158. });
  159. }
  160. renderAssignmentLink () {
  161. const assignment = this.props.assignment;
  162. let assignmentTitle;
  163. let assignmentIcon;
  164. if (assignment.muted) {
  165. assignmentTitle = I18n.t('This assignment is muted');
  166. assignmentIcon = AssignmentColumnHeader.renderMutedIcon(assignmentTitle);
  167. } else if (assignment.omitFromFinalGrade) {
  168. assignmentTitle = I18n.t('This assignment does not count toward the final grade');
  169. assignmentIcon = AssignmentColumnHeader.renderWarningIcon(assignmentTitle);
  170. } else if (assignment.pointsPossible == null || assignment.pointsPossible === 0) {
  171. assignmentTitle = I18n.t('This assignment has no points possible and cannot be included in grade calculation');
  172. assignmentIcon = AssignmentColumnHeader.renderWarningIcon(assignmentTitle);
  173. }
  174. return (
  175. <span className="assignment-name">
  176. <Link ref={this.bindAssignmentLink} title={assignmentTitle} href={assignment.htmlUrl}>
  177. {assignmentIcon}
  178. {assignment.name}
  179. </Link>
  180. </span>
  181. );
  182. }
  183. renderPointsPossible () {
  184. const pointsPossible = I18n.n(this.props.assignment.pointsPossible || 0);
  185. return (
  186. <div className="assignment-points-possible">
  187. { I18n.t('Out of %{pointsPossible}', { pointsPossible }) }
  188. </div>
  189. );
  190. }
  191. renderTrigger () {
  192. const optionsTitle = I18n.t('%{name} Options', { name: this.props.assignment.name });
  193. const menuShown = this.state.menuShown;
  194. const classes = `Gradebook__ColumnHeaderAction ${menuShown ? 'menuShown' : ''}`;
  195. return (
  196. <span ref={this.bindOptionsMenuTrigger} className={classes}>
  197. <Typography weight="bold" fontStyle="normal" size="large" color="brand">
  198. <IconMoreSolid className="rotated" title={optionsTitle} />
  199. </Typography>
  200. </span>
  201. );
  202. }
  203. renderMenu () {
  204. if (!this.props.assignment.published) { return null; }
  205. const { sortBySetting } = this.props;
  206. const selectedSortSetting = sortBySetting.isSortColumn && sortBySetting.settingKey;
  207. return (
  208. <PopoverMenu
  209. contentRef={this.bindOptionsMenuContent}
  210. shouldFocusTriggerOnClose={false}
  211. trigger={this.renderTrigger()}
  212. onToggle={this.onToggle}
  213. onClose={this.props.onMenuClose}
  214. >
  215. <MenuItemFlyout contentRef={this.bindSortByMenuContent} label={I18n.t('Sort by')}>
  216. <MenuItemGroup label={<ScreenReaderContent>{I18n.t('Sort by')}</ScreenReaderContent>}>
  217. <MenuItem
  218. selected={selectedSortSetting === 'grade' && sortBySetting.direction === 'ascending'}
  219. disabled={sortBySetting.disabled}
  220. onSelect={sortBySetting.onSortByGradeAscending}
  221. >
  222. {I18n.t('Grade - Low to High')}
  223. </MenuItem>
  224. <MenuItem
  225. selected={selectedSortSetting === 'grade' && sortBySetting.direction === 'descending'}
  226. disabled={sortBySetting.disabled}
  227. onSelect={sortBySetting.onSortByGradeDescending}
  228. >
  229. {I18n.t('Grade - High to Low')}
  230. </MenuItem>
  231. <MenuItem
  232. selected={selectedSortSetting === 'missing'}
  233. disabled={sortBySetting.disabled}
  234. onSelect={sortBySetting.onSortByMissing}
  235. >
  236. {I18n.t('Missing')}
  237. </MenuItem>
  238. <MenuItem
  239. selected={selectedSortSetting === 'late'}
  240. disabled={sortBySetting.disabled}
  241. onSelect={sortBySetting.onSortByLate}
  242. >
  243. {I18n.t('Late')}
  244. </MenuItem>
  245. {
  246. this.props.showUnpostedMenuItem &&
  247. <MenuItem
  248. selected={selectedSortSetting === 'unposted'}
  249. disabled={sortBySetting.disabled}
  250. onSelect={sortBySetting.onSortByUnposted}
  251. >
  252. {I18n.t('Unposted')}
  253. </MenuItem>
  254. }
  255. </MenuItemGroup>
  256. </MenuItemFlyout>
  257. <MenuItem
  258. disabled={!this.props.submissionsLoaded}
  259. onSelect={this.showMessageStudentsWhoDialog}
  260. >
  261. <span data-menu-item-id="message-students-who">{I18n.t('Message Students Who')}</span>
  262. </MenuItem>
  263. <MenuItem
  264. disabled={this.props.curveGradesAction.isDisabled}
  265. onSelect={this.curveGrades}
  266. >
  267. <span data-menu-item-id="curve-grades">{I18n.t('Curve Grades')}</span>
  268. </MenuItem>
  269. <MenuItem
  270. disabled={this.props.setDefaultGradeAction.disabled}
  271. onSelect={this.setDefaultGrades}
  272. >
  273. <span data-menu-item-id="set-default-grade">{I18n.t('Set Default Grade')}</span>
  274. </MenuItem>
  275. <MenuItem
  276. disabled={this.props.muteAssignmentAction.disabled}
  277. onSelect={this.muteAssignment}
  278. >
  279. <span data-menu-item-id="assignment-muter">
  280. {this.props.assignment.muted ? I18n.t('Unmute Assignment') : I18n.t('Mute Assignment')}
  281. </span>
  282. </MenuItem>
  283. { !this.props.enterGradesAsSetting.hidden && <MenuItemSeparator /> }
  284. {
  285. !this.props.enterGradesAsSetting.hidden && (
  286. <MenuItemFlyout contentRef={this.bindEnterGradesAsMenuContent} label={I18n.t('Enter Grades as')}>
  287. <MenuItemGroup
  288. label={<ScreenReaderContent>{I18n.t('Enter Grades as')}</ScreenReaderContent>}
  289. onSelect={this.onEnterGradesAsSettingSelect}
  290. selected={[this.props.enterGradesAsSetting.selected]}
  291. >
  292. <MenuItem value="points">
  293. { I18n.t('Points') }
  294. </MenuItem>
  295. <MenuItem value="percent">
  296. { I18n.t('Percentage') }
  297. </MenuItem>
  298. {
  299. this.props.enterGradesAsSetting.showGradingSchemeOption && (
  300. <MenuItem value="gradingScheme">
  301. { I18n.t('Grading Scheme') }
  302. </MenuItem>
  303. )
  304. }
  305. </MenuItemGroup>
  306. </MenuItemFlyout>
  307. )
  308. }
  309. {
  310. !(
  311. this.props.downloadSubmissionsAction.hidden &&
  312. this.props.reuploadSubmissionsAction.hidden
  313. ) && <MenuItemSeparator />
  314. }
  315. {
  316. !this.props.downloadSubmissionsAction.hidden &&
  317. <MenuItem onSelect={this.downloadSubmissions}>
  318. <span data-menu-item-id="download-submissions">{I18n.t('Download Submissions')}</span>
  319. </MenuItem>
  320. }
  321. {
  322. !this.props.reuploadSubmissionsAction.hidden &&
  323. <MenuItem onSelect={this.reuploadSubmissions}>
  324. <span data-menu-item-id="reupload-submissions">{I18n.t('Re-Upload Submissions')}</span>
  325. </MenuItem>
  326. }
  327. </PopoverMenu>
  328. );
  329. }
  330. render () {
  331. return (
  332. <div className="Gradebook__ColumnHeaderContent">
  333. <span className="Gradebook__ColumnHeaderDetail">
  334. {this.renderAssignmentLink()}
  335. <Typography weight="normal" fontStyle="normal" size="x-small">
  336. {this.renderPointsPossible()}
  337. </Typography>
  338. </span>
  339. {this.renderMenu()}
  340. </div>
  341. );
  342. }
  343. }
  344. export default AssignmentColumnHeader;