GradingPeriodSetCollection.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  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 _ from 'underscore'
  21. import $ from 'jquery'
  22. import Button from 'instructure-ui/lib/components/Button'
  23. import I18n from 'i18n!grading_periods'
  24. import GradingPeriodSet from 'jsx/grading/GradingPeriodSet'
  25. import SearchGradingPeriodsField from 'jsx/grading/SearchGradingPeriodsField'
  26. import SearchHelpers from 'jsx/shared/helpers/searchHelpers'
  27. import DateHelper from 'jsx/shared/helpers/dateHelper'
  28. import EnrollmentTermsDropdown from 'jsx/grading/EnrollmentTermsDropdown'
  29. import NewGradingPeriodSetForm from 'jsx/grading/NewGradingPeriodSetForm'
  30. import EditGradingPeriodSetForm from 'jsx/grading/EditGradingPeriodSetForm'
  31. import SetsApi from 'compiled/api/gradingPeriodSetsApi'
  32. import TermsApi from 'compiled/api/enrollmentTermsApi'
  33. import 'jquery.instructure_misc_plugins'
  34. const presentEnrollmentTerms = function(enrollmentTerms) {
  35. return _.map(enrollmentTerms, term => {
  36. let newTerm = _.extend({}, term);
  37. if (newTerm.name) {
  38. newTerm.displayName = newTerm.name;
  39. } else if (_.isDate(newTerm.startAt)) {
  40. let started = DateHelper.formatDateForDisplay(newTerm.startAt);
  41. newTerm.displayName = I18n.t("Term starting ") + started;
  42. } else {
  43. let created = DateHelper.formatDateForDisplay(newTerm.createdAt);
  44. newTerm.displayName = I18n.t("Term created ") + created;
  45. }
  46. return newTerm;
  47. });
  48. };
  49. const getEditGradingPeriodSetRef = function(set) {
  50. return "edit-grading-period-set-" + set.id;
  51. };
  52. const { bool, string, shape } = PropTypes;
  53. let GradingPeriodSetCollection = React.createClass({
  54. propTypes: {
  55. readOnly: bool.isRequired,
  56. urls: shape({
  57. gradingPeriodSetsURL: string.isRequired,
  58. gradingPeriodsUpdateURL: string.isRequired,
  59. enrollmentTermsURL: string.isRequired,
  60. deleteGradingPeriodURL: string.isRequired
  61. }).isRequired,
  62. },
  63. getInitialState() {
  64. return {
  65. enrollmentTerms: [],
  66. sets: [],
  67. expandedSetIDs: [],
  68. showNewSetForm: false,
  69. searchText: "",
  70. selectedTermID: "0",
  71. editSet: {
  72. id: null,
  73. saving: false
  74. }
  75. };
  76. },
  77. componentDidUpdate(prevProps, prevState) {
  78. if (prevState.editSet.id && (prevState.editSet.id !== this.state.editSet.id)) {
  79. let set = {id: prevState.editSet.id};
  80. this.refs[this.getShowGradingPeriodSetRef(set)].refs.editButton.focus();
  81. }
  82. },
  83. addGradingPeriodSet(set, termIDs) {
  84. this.setState({
  85. sets: [set].concat(this.state.sets),
  86. expandedSetIDs: this.state.expandedSetIDs.concat([set.id]),
  87. enrollmentTerms: this.associateTermsWithSet(set.id, termIDs),
  88. showNewSetForm: false
  89. }, () => {
  90. this.refs.addSetFormButton.focus();
  91. });
  92. },
  93. associateTermsWithSet(setID, termIDs) {
  94. return _.map(this.state.enrollmentTerms, function(term) {
  95. if (_.contains(termIDs, term.id)) {
  96. let newTerm = _.extend({}, term);
  97. newTerm.gradingPeriodGroupId = setID;
  98. return newTerm;
  99. } else {
  100. return term;
  101. }
  102. });
  103. },
  104. componentWillMount() {
  105. this.getSets();
  106. this.getTerms();
  107. },
  108. getSets() {
  109. SetsApi.list()
  110. .then((sets) => { this.onSetsLoaded(sets); })
  111. .catch((_) => {
  112. $.flashError(I18n.t(
  113. "An error occured while fetching grading period sets."
  114. ));
  115. });
  116. },
  117. getTerms() {
  118. TermsApi.list()
  119. .then((terms) => { this.onTermsLoaded(terms); })
  120. .catch((_) => {
  121. $.flashError(I18n.t(
  122. "An error occured while fetching enrollment terms."
  123. ));
  124. });
  125. },
  126. onTermsLoaded(terms) {
  127. this.setState({ enrollmentTerms: presentEnrollmentTerms(terms) });
  128. },
  129. onSetsLoaded(sets) {
  130. const sortedSets = _.sortBy(sets, "createdAt").reverse();
  131. this.setState({ sets: sortedSets });
  132. },
  133. onSetUpdated(updatedSet) {
  134. let sets = _.map(this.state.sets, (set) => {
  135. return (set.id === updatedSet.id) ? _.extend({}, set, updatedSet) : set;
  136. });
  137. let terms = _.map(this.state.enrollmentTerms, function(term) {
  138. if (_.contains(updatedSet.enrollmentTermIDs, term.id)) {
  139. return _.extend({}, term, { gradingPeriodGroupId: updatedSet.id });
  140. } else if (term.gradingPeriodGroupId === updatedSet.id) {
  141. return _.extend({}, term, { gradingPeriodGroupId: null });
  142. } else {
  143. return term;
  144. }
  145. });
  146. this.setState({ sets: sets, enrollmentTerms: terms });
  147. $.flashMessage(I18n.t("The grading period set was updated successfully."));
  148. },
  149. setAndGradingPeriodTitles(set) {
  150. let titles = _.pluck(set.gradingPeriods, 'title');
  151. titles.unshift(set.title);
  152. return _.compact(titles);
  153. },
  154. searchTextMatchesTitles(titles) {
  155. return _.any(titles, (title) => {
  156. return SearchHelpers
  157. .substringMatchRegex(this.state.searchText).test(title);
  158. });
  159. },
  160. filterSetsBySearchText(sets, searchText) {
  161. if (searchText === "") return sets;
  162. return _.filter(sets, (set) => {
  163. let titles = this.setAndGradingPeriodTitles(set);
  164. return this.searchTextMatchesTitles(titles);
  165. });
  166. },
  167. changeSearchText(searchText) {
  168. if (searchText !== this.state.searchText) {
  169. this.setState({ searchText: searchText });
  170. }
  171. },
  172. filterSetsBySelectedTerm(sets, terms, selectedTermID) {
  173. if (selectedTermID === "0") return sets;
  174. const activeTerm = _.findWhere(terms, { id: selectedTermID });
  175. const setID = activeTerm.gradingPeriodGroupId;
  176. return _.where(sets, { id: setID });
  177. },
  178. changeSelectedEnrollmentTerm(event) {
  179. this.setState({ selectedTermID: event.target.value });
  180. },
  181. alertForMatchingSets(numSets) {
  182. let msg;
  183. if (this.state.selectedTermID === "0" && this.state.searchText === "") {
  184. msg = I18n.t("Showing all sets of grading periods.");
  185. } else {
  186. msg = I18n.t({
  187. one: "1 set of grading periods found.",
  188. other: "%{count} sets of grading periods found.",
  189. zero: "No matching sets of grading periods found."
  190. }, {count: numSets}
  191. );
  192. }
  193. const polite = true;
  194. $.screenReaderFlashMessageExclusive(msg, polite);
  195. },
  196. getVisibleSets() {
  197. let setsFilteredBySearchText =
  198. this.filterSetsBySearchText(this.state.sets, this.state.searchText);
  199. let filterByTermArgs = [
  200. setsFilteredBySearchText,
  201. this.state.enrollmentTerms,
  202. this.state.selectedTermID
  203. ];
  204. let visibleSets = this.filterSetsBySelectedTerm(...filterByTermArgs);
  205. this.alertForMatchingSets(visibleSets.length);
  206. return visibleSets;
  207. },
  208. toggleSetBody(setId) {
  209. if (_.contains(this.state.expandedSetIDs, setId)) {
  210. this.setState({ expandedSetIDs: _.without(this.state.expandedSetIDs, setId) });
  211. } else {
  212. this.setState({ expandedSetIDs: this.state.expandedSetIDs.concat([setId]) });
  213. }
  214. },
  215. editGradingPeriodSet(set) {
  216. this.setState({ editSet: {id: set.id, saving: false} });
  217. },
  218. nodeToFocusOnAfterSetDeletion(setID) {
  219. const index = this.state.sets.findIndex(set => set.id === setID);
  220. if (index < 1) {
  221. return this.refs.addSetFormButton;
  222. } else {
  223. const setRef = this.getShowGradingPeriodSetRef(this.state.sets[index - 1]);
  224. const setToFocus = this.refs[setRef];
  225. return setToFocus.refs.editButton;
  226. }
  227. },
  228. removeGradingPeriodSet(setID) {
  229. let newSets = _.reject(this.state.sets, set => set.id === setID);
  230. const nodeToFocus = this.nodeToFocusOnAfterSetDeletion(setID);
  231. this.setState({ sets: newSets }, () => nodeToFocus.focus());
  232. },
  233. updateSetPeriods(setID, gradingPeriods) {
  234. let newSets = _.map(this.state.sets, (set) => {
  235. if (set.id === setID) {
  236. return _.extend({}, set, { gradingPeriods: gradingPeriods });
  237. }
  238. return set;
  239. });
  240. this.setState({ sets: newSets });
  241. },
  242. openNewSetForm() {
  243. this.setState({ showNewSetForm: true });
  244. },
  245. closeNewSetForm() {
  246. this.setState({ showNewSetForm: false }, () => {
  247. this.refs.addSetFormButton.focus();
  248. });
  249. },
  250. termsBelongingToActiveSets() {
  251. const setIDs = _.pluck(this.state.sets, "id");
  252. return _.filter(this.state.enrollmentTerms, function(term) {
  253. const setID = term.gradingPeriodGroupId;
  254. return setID && _.contains(setIDs, setID);
  255. });
  256. },
  257. termsNotBelongingToActiveSets() {
  258. return _.difference(this.state.enrollmentTerms, this.termsBelongingToActiveSets());
  259. },
  260. selectableTermsForEditSetForm(setID) {
  261. const termsBelongingToThisSet = _.where(this.termsBelongingToActiveSets(), { gradingPeriodGroupId: setID });
  262. return _.union(this.termsNotBelongingToActiveSets(), termsBelongingToThisSet);
  263. },
  264. closeEditSetForm(id) {
  265. this.setState({ editSet: {id: null, saving: false} });
  266. },
  267. getShowGradingPeriodSetRef(set) {
  268. return "show-grading-period-set-" + set.id;
  269. },
  270. renderEditGradingPeriodSetForm(set) {
  271. let cancelCallback = () => {
  272. this.closeEditSetForm(set.id);
  273. };
  274. let saveCallback = (set) => {
  275. let editSet = _.extend({}, this.state.editSet, {saving: true});
  276. this.setState({editSet: editSet});
  277. SetsApi.update(set)
  278. .then((updated) => {
  279. this.onSetUpdated(updated);
  280. this.closeEditSetForm(set.id);
  281. })
  282. .catch((_) => {
  283. $.flashError(I18n.t(
  284. "An error occured while updating the grading period set."
  285. ));
  286. });
  287. };
  288. return (
  289. <EditGradingPeriodSetForm
  290. key = {set.id}
  291. ref = {getEditGradingPeriodSetRef(set)}
  292. set = {set}
  293. enrollmentTerms = {this.selectableTermsForEditSetForm(set.id)}
  294. disabled = {this.state.editSet.saving}
  295. onCancel = {cancelCallback}
  296. onSave = {saveCallback} />
  297. );
  298. },
  299. renderSets() {
  300. const urls = {
  301. batchUpdateURL: this.props.urls.gradingPeriodsUpdateURL,
  302. gradingPeriodSetsURL: this.props.urls.gradingPeriodSetsURL,
  303. deleteGradingPeriodURL: this.props.urls.deleteGradingPeriodURL
  304. };
  305. return _.map(this.getVisibleSets(), set => {
  306. if (this.state.editSet.id === set.id) {
  307. return this.renderEditGradingPeriodSetForm(set);
  308. } else {
  309. return (
  310. <GradingPeriodSet
  311. key = {set.id}
  312. ref = {this.getShowGradingPeriodSetRef(set)}
  313. set = {set}
  314. gradingPeriods = {set.gradingPeriods}
  315. urls = {urls}
  316. actionsDisabled = {!!this.state.editSet.id}
  317. readOnly = {this.props.readOnly}
  318. permissions = {set.permissions}
  319. terms = {this.state.enrollmentTerms}
  320. expanded = {_.contains(this.state.expandedSetIDs, set.id)}
  321. onEdit = {this.editGradingPeriodSet}
  322. onDelete = {this.removeGradingPeriodSet}
  323. onPeriodsChange = {this.updateSetPeriods}
  324. onToggleBody = {() => { this.toggleSetBody(set.id) }}
  325. />
  326. );
  327. }
  328. });
  329. },
  330. renderNewGradingPeriodSetForm() {
  331. if (this.state.showNewSetForm) {
  332. return (
  333. <NewGradingPeriodSetForm
  334. ref = "newSetForm"
  335. closeForm = {this.closeNewSetForm}
  336. urls = {this.props.urls}
  337. enrollmentTerms = {this.termsNotBelongingToActiveSets()}
  338. readOnly = {this.props.readOnly}
  339. addGradingPeriodSet = {this.addGradingPeriodSet}
  340. />
  341. );
  342. }
  343. },
  344. renderAddSetFormButton() {
  345. let disable = this.state.showNewSetForm || !!this.state.editSet.id;
  346. if (!this.props.readOnly) {
  347. return (
  348. <Button
  349. ref = 'addSetFormButton'
  350. variant = 'primary'
  351. disabled = {disable}
  352. onClick = {this.openNewSetForm}
  353. aria-label = {I18n.t("Add Set of Grading Periods")}
  354. >
  355. <i className="icon-plus"/>
  356. &nbsp;
  357. <span aria-hidden="true">{I18n.t("Set of Grading Periods")}</span>
  358. </Button>
  359. );
  360. }
  361. },
  362. render() {
  363. return (
  364. <div>
  365. <div className="GradingPeriodSets__toolbar header-bar no-line ic-Form-action-box">
  366. <div className="ic-Form-action-box__Form">
  367. <div className="ic-Form-control">
  368. <EnrollmentTermsDropdown
  369. terms={this.termsBelongingToActiveSets()}
  370. changeSelectedEnrollmentTerm={this.changeSelectedEnrollmentTerm} />
  371. </div>
  372. <SearchGradingPeriodsField changeSearchText={this.changeSearchText} />
  373. </div>
  374. <div className="ic-Form-action-box__Actions">
  375. {this.renderAddSetFormButton()}
  376. </div>
  377. </div>
  378. {this.renderNewGradingPeriodSetForm()}
  379. <div id="grading-period-sets">
  380. {this.renderSets()}
  381. </div>
  382. </div>
  383. );
  384. }
  385. });
  386. export default GradingPeriodSetCollection