CoursesPane.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /*
  2. * Copyright (C) 2015 - 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 { debounce } from 'underscore'
  21. import I18n from 'i18n!account_course_user_search'
  22. import CoursesStore from './CoursesStore'
  23. import TermsStore from './TermsStore'
  24. import AccountsTreeStore from './AccountsTreeStore'
  25. import CoursesList from './CoursesList'
  26. import CoursesToolbar from './CoursesToolbar'
  27. import renderSearchMessage from './renderSearchMessage'
  28. const MIN_SEARCH_LENGTH = 3
  29. const stores = [CoursesStore, TermsStore, AccountsTreeStore]
  30. const { shape, arrayOf, string } = PropTypes
  31. class CoursesPane extends React.Component {
  32. static propTypes = {
  33. roles: arrayOf(shape({ id: string.isRequired })).isRequired,
  34. addUserUrls: shape({
  35. USER_LISTS_URL: string.isRequired,
  36. ENROLL_USERS_URL: string.isRequired,
  37. }).isRequired,
  38. accountId: string.isRequired,
  39. }
  40. constructor () {
  41. super()
  42. const filters = {
  43. enrollment_term_id: '',
  44. search_term: '',
  45. with_students: false,
  46. sort: 'sis_course_id',
  47. order: 'asc',
  48. search_by: 'course',
  49. }
  50. this.state = {
  51. filters,
  52. draftFilters: filters,
  53. errors: {},
  54. previousCourses: {data: []},
  55. }
  56. // Doing this here because the class property version didn't work :(
  57. this.debouncedApplyFilters = debounce(this.onApplyFilters, 250)
  58. }
  59. componentWillMount () {
  60. stores.forEach(s => s.addChangeListener(this.refresh))
  61. }
  62. componentDidMount () {
  63. this.fetchCourses()
  64. TermsStore.loadAll()
  65. AccountsTreeStore.loadTree()
  66. }
  67. componentWillUnmount () {
  68. stores.forEach(s => s.removeChangeListener(this.refresh))
  69. }
  70. fetchCourses = () => {
  71. CoursesStore.load(this.state.filters)
  72. }
  73. fetchMoreCourses = () => {
  74. CoursesStore.loadMore(this.state.filters)
  75. }
  76. onUpdateFilters = (newFilters) => {
  77. this.setState({
  78. errors: {},
  79. draftFilters: Object.assign({}, this.state.draftFilters, newFilters)
  80. }, this.debouncedApplyFilters)
  81. }
  82. onApplyFilters = () => {
  83. const filters = this.state.draftFilters
  84. if (filters.search_term && filters.search_term.length < MIN_SEARCH_LENGTH) {
  85. this.setState({errors: {search_term: I18n.t('Search term must be at least %{num} characters', {num: MIN_SEARCH_LENGTH})}})
  86. } else {
  87. this.setState({filters, errors: {}}, this.fetchCourses)
  88. }
  89. }
  90. onChangeSort = (column) => {
  91. const {sort, order} = this.state.filters
  92. let newOrder = 'asc'
  93. if (column === sort && order === 'asc') {
  94. newOrder = 'desc'
  95. }
  96. const newFilters = Object.assign({}, this.state.filters, {
  97. sort: column,
  98. order: newOrder
  99. })
  100. this.setState({filters: newFilters, previousCourses: CoursesStore.get(this.state.filters)}, this.fetchCourses)
  101. }
  102. refresh = () => {
  103. this.forceUpdate()
  104. }
  105. render () {
  106. const { filters, draftFilters, errors } = this.state
  107. let courses = CoursesStore.get(filters)
  108. if (!courses || !courses.data) {
  109. courses = this.state.previousCourses
  110. }
  111. const terms = TermsStore.get()
  112. const accounts = AccountsTreeStore.getTree()
  113. const isLoading = !(courses && !courses.loading && terms && !terms.loading)
  114. return (
  115. <div>
  116. <CoursesToolbar
  117. onUpdateFilters={this.onUpdateFilters}
  118. onApplyFilters={this.onApplyFilters}
  119. terms={terms && terms.data}
  120. accounts={accounts}
  121. isLoading={isLoading}
  122. {...draftFilters}
  123. errors={errors}
  124. />
  125. <CoursesList
  126. onChangeSort={this.onChangeSort}
  127. accountId={this.props.accountId}
  128. courses={courses.data}
  129. roles={this.props.roles}
  130. addUserUrls={this.props.addUserUrls}
  131. sort={filters.sort}
  132. order={filters.order}
  133. />
  134. {renderSearchMessage(courses, this.fetchMoreCourses, I18n.t('No courses found'))}
  135. </div>
  136. )
  137. }
  138. }
  139. export default CoursesPane