SearchResults.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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, { Component } from 'react';
  19. import { connect } from 'react-redux';
  20. import { arrayOf, bool, func, node, shape, string } from 'prop-types';
  21. import $ from 'jquery';
  22. import 'jquery.instructure_date_and_time'
  23. import I18n from 'i18n!gradebook_history';
  24. import Container from 'instructure-ui/lib/components/Container';
  25. import ScreenReaderContent from 'instructure-ui/lib/components/ScreenReaderContent';
  26. import Spinner from 'instructure-ui/lib/components/Spinner';
  27. import Table from 'instructure-ui/lib/components/Table';
  28. import Typography from 'instructure-ui/lib/components/Typography';
  29. import { getHistoryNextPage } from 'jsx/gradebook-history/actions/SearchResultsActions';
  30. import SearchResultsRow from 'jsx/gradebook-history/SearchResultsRow';
  31. const colHeaders = [
  32. I18n.t('Date'),
  33. <ScreenReaderContent>{I18n.t('Anonymous Grading')}</ScreenReaderContent>,
  34. I18n.t('Student'),
  35. I18n.t('Grader'),
  36. I18n.t('Assignment'),
  37. I18n.t('Before'),
  38. I18n.t('After'),
  39. I18n.t('Current')
  40. ];
  41. const nearPageBottom = () => (
  42. document.body.clientHeight - (window.innerHeight + window.scrollY) < 100
  43. );
  44. class SearchResultsComponent extends Component {
  45. static propTypes = {
  46. getNextPage: func.isRequired,
  47. fetchHistoryStatus: string.isRequired,
  48. caption: node.isRequired,
  49. historyItems: arrayOf(shape({
  50. anonymous: bool.isRequired,
  51. assignment: string.isRequired,
  52. date: string.isRequired,
  53. displayAsPoints: bool.isRequired,
  54. grader: string.isRequired,
  55. gradeAfter: string.isRequired,
  56. gradeBefore: string.isRequired,
  57. gradeCurrent: string.isRequired,
  58. id: string.isRequired,
  59. pointsPossibleAfter: string.isRequired,
  60. pointsPossibleBefore: string.isRequired,
  61. pointsPossibleCurrent: string.isRequired,
  62. student: string.isRequired
  63. })).isRequired,
  64. nextPage: string.isRequired,
  65. requestingResults: bool.isRequired
  66. };
  67. componentDidMount () {
  68. this.attachListeners();
  69. }
  70. componentDidUpdate (prevProps) {
  71. // if the page doesn't have a scrollbar, scroll event listener can't be triggered
  72. if (document.body.clientHeight <= window.innerHeight) {
  73. this.getNextPage();
  74. }
  75. if (prevProps.historyItems.length < this.props.historyItems.length) {
  76. $.screenReaderFlashMessage(I18n.t('More results were added at the bottom of the page.'));
  77. }
  78. this.attachListeners();
  79. }
  80. componentWillUnmount () {
  81. this.detachListeners();
  82. }
  83. getNextPage = () => {
  84. if (!this.props.requestingResults && this.props.nextPage && nearPageBottom()) {
  85. this.props.getNextPage(this.props.nextPage);
  86. this.detachListeners();
  87. }
  88. }
  89. attachListeners = () => {
  90. if (this.props.requestingResults || !this.props.nextPage) {
  91. return;
  92. }
  93. document.addEventListener('scroll', this.getNextPage);
  94. window.addEventListener('resize', this.getNextPage);
  95. }
  96. detachListeners = () => {
  97. document.removeEventListener('scroll', this.getNextPage);
  98. window.removeEventListener('resize', this.getNextPage);
  99. }
  100. hasHistory () {
  101. return this.props.historyItems.length > 0;
  102. }
  103. noResultsFound () {
  104. return this.props.fetchHistoryStatus === 'success' && !this.hasHistory();
  105. }
  106. showResults = () => (
  107. <div>
  108. <Table
  109. caption={this.props.caption}
  110. >
  111. <thead>
  112. <tr>
  113. {colHeaders.map(header => (
  114. <th scope="col" key={`${header}-column`}>{ header }</th>
  115. ))}
  116. </tr>
  117. </thead>
  118. <tbody>
  119. {this.props.historyItems.map(item => (
  120. <SearchResultsRow
  121. key={`history-items-${item.id}`}
  122. item={item}
  123. />
  124. ))}
  125. </tbody>
  126. </Table>
  127. </div>
  128. )
  129. showStatus = () => {
  130. if (this.props.requestingResults) {
  131. $.screenReaderFlashMessage(I18n.t('Loading more gradebook history results.'));
  132. return (
  133. <Spinner size="small" title={I18n.t('Loading Results')} />
  134. );
  135. }
  136. if (this.noResultsFound()) {
  137. return (<Typography fontStyle="italic">{I18n.t('No results found.')}</Typography>);
  138. }
  139. if (!this.props.requestingResults && !this.props.nextPage && this.hasHistory()) {
  140. return (<Typography fontStyle="italic">{I18n.t('No more results to load.')}</Typography>);
  141. }
  142. return null;
  143. }
  144. render () {
  145. return (
  146. <div>
  147. {this.hasHistory() && this.showResults()}
  148. <Container as="div" textAlign="center" margin="medium 0 0 0">
  149. {this.showStatus()}
  150. </Container>
  151. </div>
  152. );
  153. }
  154. }
  155. const mapStateToProps = state => (
  156. {
  157. fetchHistoryStatus: state.history.fetchHistoryStatus || '',
  158. historyItems: state.history.items || [],
  159. nextPage: state.history.nextPage || '',
  160. requestingResults: state.history.loading || false,
  161. }
  162. );
  163. const mapDispatchToProps = dispatch => (
  164. {
  165. getNextPage: (url) => {
  166. dispatch(getHistoryNextPage(url));
  167. }
  168. }
  169. );
  170. export default connect(mapStateToProps, mapDispatchToProps)(SearchResultsComponent);
  171. export { SearchResultsComponent };