StudentContextTray.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /*
  2. * Copyright (C) 2016 - 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 PropTypes from 'prop-types'
  20. import I18n from 'i18n!student_context_tray'
  21. import FriendlyDatetime from 'jsx/shared/FriendlyDatetime'
  22. import Avatar from './Avatar'
  23. import LastActivity from './LastActivity'
  24. import MetricsList from './MetricsList'
  25. import Rating from './Rating'
  26. import SectionInfo from './SectionInfo'
  27. import SubmissionProgressBars from './SubmissionProgressBars'
  28. import MessageStudents from 'jsx/shared/MessageStudents'
  29. import Heading from 'instructure-ui/lib/components/Heading'
  30. import Button from 'instructure-ui/lib/components/Button'
  31. import Link from 'instructure-ui/lib/components/Link'
  32. import Typography from 'instructure-ui/lib/components/Typography'
  33. import ScreenReaderContent from 'instructure-ui/lib/components/ScreenReaderContent'
  34. import Spinner from 'instructure-ui/lib/components/Spinner'
  35. import Tray from 'instructure-ui/lib/components/Tray'
  36. const courseShape = PropTypes.shape({
  37. permissions: PropTypes.shape({}).isRequired,
  38. submissionsConnection: PropTypes.shape({
  39. edges: PropTypes.arrayOf(PropTypes.shape({}))
  40. }).isRequired
  41. });
  42. const userShape = PropTypes.shape({
  43. enrollments: PropTypes.arrayOf(PropTypes.object).isRequired
  44. });
  45. const dataShape = PropTypes.shape({
  46. loading: PropTypes.bool.isRequired,
  47. course: courseShape,
  48. user: userShape
  49. });
  50. export default class StudentContextTray extends React.Component {
  51. static propTypes = {
  52. courseId: PropTypes.string.isRequired,
  53. studentId: PropTypes.string.isRequired,
  54. returnFocusTo: PropTypes.func.isRequired,
  55. data: dataShape.isRequired,
  56. }
  57. static renderQuickLink (label, srLabel, url, showIf) {
  58. return showIf() ? (
  59. <div className="StudentContextTray-QuickLinks__Link">
  60. <Button
  61. href={url}
  62. variant="ghost"
  63. size="small"
  64. fluidWidth
  65. aria-label={srLabel}
  66. >
  67. <span className="StudentContextTray-QuickLinks__Link-text">{label}</span>
  68. </Button>
  69. </div>
  70. ) : null
  71. }
  72. constructor (props) {
  73. super(props)
  74. this.state = {
  75. isOpen: true,
  76. messageFormOpen: false,
  77. }
  78. }
  79. /**
  80. * Lifecycle
  81. */
  82. componentWillReceiveProps (nextProps) {
  83. if (!this.state.isOpen) {
  84. this.setState({isOpen: true})
  85. }
  86. }
  87. /**
  88. * Handlers
  89. */
  90. getCloseButtonRef = (ref) => {
  91. this.closeButtonRef = ref
  92. }
  93. handleRequestClose = (e) => {
  94. e.preventDefault()
  95. this.setState({
  96. isOpen: false
  97. })
  98. if (this.props.returnFocusTo) {
  99. const focusableItems = this.props.returnFocusTo();
  100. // Because of the way native focus calls return undefined, all focus
  101. // objects should be wrapped in something that will return truthy like
  102. // jQuery wrappers do... and it should be able to check visibility like a
  103. // jQuery wrapper... so just use jQuery.
  104. focusableItems.some($itemToFocus => $itemToFocus.is(':visible') && $itemToFocus.focus())
  105. }
  106. }
  107. handleMessageButtonClick = (e) => {
  108. e.preventDefault()
  109. this.setState({
  110. messageFormOpen: true
  111. })
  112. }
  113. handleMessageFormClose = (e) => {
  114. e.preventDefault()
  115. this.setState({
  116. messageFormOpen: false
  117. }, () => {
  118. this.messageStudentsButton.focus()
  119. })
  120. }
  121. /**
  122. * Renderers
  123. */
  124. renderQuickLinks (user, course) {
  125. return (user.short_name && (
  126. course.permissions.manage_grades ||
  127. course.permissions.view_all_grades ||
  128. course.permissions.view_analytics
  129. )) ? (
  130. <section
  131. className="StudentContextTray__Section StudentContextTray-QuickLinks"
  132. >
  133. {StudentContextTray.renderQuickLink(
  134. I18n.t('Grades'),
  135. I18n.t('View grades for %{name}', { name: user.short_name }),
  136. `/courses/${this.props.courseId}/grades/${this.props.studentId}`,
  137. () =>
  138. course.permissions.manage_grades ||
  139. course.permissions.view_all_grades
  140. )}
  141. {StudentContextTray.renderQuickLink(
  142. I18n.t('Analytics'),
  143. I18n.t('View analytics for %{name}', { name: user.short_name }),
  144. `/courses/${this.props.courseId}/analytics/users/${this.props.studentId}`,
  145. () => course.permissions.view_analytics && user.analytics
  146. )}
  147. </section>
  148. ) : null
  149. }
  150. render () {
  151. const { data: { loading, course, user } } = this.props
  152. return (
  153. <div>
  154. {this.state.messageFormOpen ? (
  155. <MessageStudents
  156. contextCode={`course_${course._id}`}
  157. onRequestClose={this.handleMessageFormClose}
  158. open={this.state.messageFormOpen}
  159. recipients={[{
  160. id: user._id,
  161. displayName: user.short_name
  162. }]}
  163. title='Send a message'
  164. />
  165. ) : null}
  166. <Tray
  167. label={I18n.t('Student Details')}
  168. closeButtonLabel={I18n.t('Close')}
  169. closeButtonRef={this.getCloseButtonRef}
  170. applicationElement={() => document.getElementById('application')}
  171. open={this.state.isOpen}
  172. onDismiss={this.handleRequestClose}
  173. placement='end'
  174. zIndex='1000'
  175. >
  176. <aside
  177. className={user && user.avatar_url
  178. ? 'StudentContextTray StudentContextTray--withAvatar'
  179. : 'StudentContextTray'
  180. }
  181. >
  182. {loading ? (
  183. <div className='StudentContextTray__Spinner'>
  184. <Spinner title={I18n.t('Loading')}
  185. size='large'
  186. />
  187. </div>
  188. ) : (
  189. <div>
  190. <header className="StudentContextTray-Header">
  191. <Avatar user={user}
  192. canMasquerade={course.permissions.become_user}
  193. courseId={this.props.courseId}
  194. />
  195. <div className="StudentContextTray-Header__Layout">
  196. <div className="StudentContextTray-Header__Content">
  197. {user.short_name ? (
  198. <div className="StudentContextTray-Header__Name">
  199. <Heading level="h3" as="h2">
  200. <span className="StudentContextTray-Header__NameLink">
  201. <Link
  202. href={`/courses/${this.props.courseId}/users/${this.props.studentId}`}
  203. aria-label={I18n.t('Go to %{name}\'s profile', {name: user.short_name})}
  204. >
  205. {user.short_name}
  206. </Link>
  207. </span>
  208. </Heading>
  209. </div>
  210. ) : null}
  211. <div className="StudentContextTray-Header__CourseName">
  212. <Typography size="medium" as="div" lineHeight="condensed">
  213. {course.name}
  214. </Typography>
  215. </div>
  216. <Typography size="x-small" color="secondary" as="div">
  217. <SectionInfo user={user} />
  218. </Typography>
  219. <Typography size="x-small" color="secondary" as="div">
  220. <LastActivity user={user} />
  221. </Typography>
  222. </div>
  223. {course.permissions.send_messages ? (
  224. <div className="StudentContextTray-Header__Actions">
  225. <Button
  226. ref={ (b) => this.messageStudentsButton = b }
  227. variant="icon" size="small"
  228. onClick={this.handleMessageButtonClick}
  229. >
  230. <ScreenReaderContent>
  231. {I18n.t('Send a message to %{student}', {student: user.short_name})}
  232. </ScreenReaderContent>
  233. {/* Note: replace with instructure-icon */}
  234. <i className="icon-email" aria-hidden="true" />
  235. </Button>
  236. </div>
  237. ) : null }
  238. </div>
  239. </header>
  240. {this.renderQuickLinks(user, course)}
  241. <MetricsList user={user} analytics={user.analytics} />
  242. <SubmissionProgressBars submissions={course.submissionsConnection.edges.map(n => n.submission)} />
  243. {user.analytics ? (
  244. <section
  245. className="StudentContextTray__Section StudentContextTray-Ratings">
  246. <Heading level="h4" as="h3" border="bottom">
  247. {I18n.t("Activity Compared to Class")}
  248. </Heading>
  249. <div className="StudentContextTray-Ratings__Layout">
  250. <Rating metric={user.analytics.participations}
  251. label={I18n.t('Participation')} />
  252. <Rating metric={user.analytics.page_views}
  253. label={I18n.t('Page Views')} />
  254. </div>
  255. </section>
  256. ) : null}
  257. </div>
  258. )}
  259. </aside>
  260. </Tray>
  261. </div>
  262. );
  263. }
  264. }