ActionMenu.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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 $ from 'jquery'
  19. import React from 'react'
  20. import PropTypes from 'prop-types'
  21. import IconMiniArrowDownSolid from 'instructure-icons/lib/Solid/IconMiniArrowDownSolid'
  22. import Button from 'instructure-ui/lib/components/Button'
  23. import { MenuItem, MenuItemSeparator } from 'instructure-ui/lib/components/Menu'
  24. import PopoverMenu from 'instructure-ui/lib/components/PopoverMenu'
  25. import Typography from 'instructure-ui/lib/components/Typography'
  26. import GradebookExportManager from 'jsx/gradezilla/shared/GradebookExportManager'
  27. import { AppLaunch } from 'jsx/gradezilla/SISGradePassback/PostGradesApp'
  28. import tz from 'timezone'
  29. import DateHelper from 'jsx/shared/helpers/dateHelper'
  30. import I18n from 'i18n!gradebook'
  31. import 'compiled/jquery.rails_flash_notifications'
  32. const { arrayOf, bool, func, object, shape, string } = PropTypes;
  33. class ActionMenu extends React.Component {
  34. static defaultProps = {
  35. lastExport: undefined,
  36. attachment: undefined,
  37. postGradesLtis: [],
  38. publishGradesToSis: {
  39. publishToSisUrl: undefined
  40. }
  41. };
  42. static propTypes = {
  43. gradebookIsEditable: bool.isRequired,
  44. contextAllowsGradebookUploads: bool.isRequired,
  45. gradebookImportUrl: string.isRequired,
  46. currentUserId: string.isRequired,
  47. gradebookExportUrl: string.isRequired,
  48. lastExport: shape({
  49. progressId: string.isRequired,
  50. workflowState: string.isRequired,
  51. }),
  52. attachment: shape({
  53. id: string.isRequired,
  54. downloadUrl: string.isRequired,
  55. updatedAt: string.isRequired
  56. }),
  57. postGradesLtis: arrayOf(shape({
  58. id: string.isRequired,
  59. name: string.isRequired,
  60. onSelect: func.isRequired
  61. })),
  62. postGradesFeature: shape({
  63. enabled: bool.isRequired,
  64. store: object.isRequired,
  65. returnFocusTo: object
  66. }).isRequired,
  67. publishGradesToSis: shape({
  68. isEnabled: bool.isRequired,
  69. publishToSisUrl: string
  70. })
  71. };
  72. static downloadableLink (url) {
  73. return `${url}&download_frd=1`;
  74. }
  75. static gotoUrl (url) {
  76. window.location.href = url;
  77. }
  78. static initialState = {
  79. exportInProgress: false
  80. };
  81. constructor (props) {
  82. super(props);
  83. this.state = ActionMenu.initialState;
  84. this.launchPostGrades = this.launchPostGrades.bind(this);
  85. }
  86. componentWillMount () {
  87. const existingExport = this.getExistingExport();
  88. this.exportManager = new GradebookExportManager(this.props.gradebookExportUrl, this.props.currentUserId, existingExport);
  89. }
  90. componentWillUnmount () {
  91. if (this.exportManager) this.exportManager.clearMonitor();
  92. }
  93. getExistingExport () {
  94. if (!(this.props.lastExport && this.props.attachment)) return undefined;
  95. if (!(this.props.lastExport.progressId && this.props.attachment.id)) return undefined;
  96. return {
  97. progressId: this.props.lastExport.progressId,
  98. attachmentId: this.props.attachment.id,
  99. workflowState: this.props.lastExport.workflowState
  100. };
  101. }
  102. setExportInProgress (status) {
  103. this.setState({ exportInProgress: !!status });
  104. }
  105. handleExport () {
  106. this.setExportInProgress(true);
  107. $.flashMessage(I18n.t('Gradebook export started'));
  108. return this.exportManager.startExport().then((resolution) => {
  109. this.setExportInProgress(false);
  110. const attachmentUrl = resolution.attachmentUrl;
  111. const updatedAt = new Date(resolution.updatedAt);
  112. const previousExport = {
  113. label: `${I18n.t('New Export')} (${DateHelper.formatDatetimeForDisplay(updatedAt)})`,
  114. attachmentUrl: ActionMenu.downloadableLink(attachmentUrl)
  115. };
  116. this.setState({ previousExport });
  117. // Since we're still on the page, let's automatically download the CSV for them as well
  118. ActionMenu.gotoUrl(attachmentUrl);
  119. }).catch((reason) => {
  120. this.setExportInProgress(false);
  121. $.flashError(I18n.t('Gradebook Export Failed: %{reason}', { reason }));
  122. });
  123. }
  124. handleImport () {
  125. ActionMenu.gotoUrl(this.props.gradebookImportUrl);
  126. }
  127. handlePublishGradesToSis () {
  128. ActionMenu.gotoUrl(this.props.publishGradesToSis.publishToSisUrl);
  129. }
  130. disableImports () {
  131. return !(this.props.gradebookIsEditable && this.props.contextAllowsGradebookUploads);
  132. }
  133. lastExportFromProps () {
  134. if (!(this.props.lastExport && this.props.lastExport.workflowState === 'completed')) return undefined;
  135. return this.props.lastExport;
  136. }
  137. lastExportFromState () {
  138. if (this.state.exportInProgress || !this.state.previousExport) return undefined;
  139. return this.state.previousExport;
  140. }
  141. previousExport () {
  142. const completedExportFromState = this.lastExportFromState();
  143. if (completedExportFromState) return completedExportFromState;
  144. const completedLastExport = this.lastExportFromProps();
  145. const attachment = completedLastExport && this.props.attachment;
  146. if (!completedLastExport || !attachment) return undefined;
  147. const updatedAt = tz.parse(attachment.updatedAt);
  148. return {
  149. label: `${I18n.t('Previous Export')} (${DateHelper.formatDatetimeForDisplay(updatedAt)})`,
  150. attachmentUrl: ActionMenu.downloadableLink(attachment.downloadUrl)
  151. };
  152. }
  153. exportInProgress () {
  154. return this.state.exportInProgress;
  155. }
  156. launchPostGrades () {
  157. const { store, returnFocusTo } = this.props.postGradesFeature;
  158. setTimeout(() => AppLaunch(store, returnFocusTo), 10);
  159. }
  160. renderPostGradesTools () {
  161. const tools = this.renderPostGradesLtis();
  162. if (this.props.postGradesFeature.enabled) {
  163. tools.push(this.renderPostGradesFeature());
  164. }
  165. if (tools.length) {
  166. tools.push(<MenuItemSeparator key="postGradesSeparator" />);
  167. }
  168. return tools;
  169. }
  170. renderPostGradesLtis () {
  171. return this.props.postGradesLtis.map((tool) => {
  172. const key = `post_grades_lti_${tool.id}`;
  173. return (
  174. <MenuItem onSelect={tool.onSelect} key={key}>
  175. <span data-menu-id={key}>
  176. {I18n.t('Sync to %{name}', {name: tool.name})}
  177. </span>
  178. </MenuItem>
  179. );
  180. });
  181. }
  182. renderPostGradesFeature () {
  183. const sisName = this.props.postGradesFeature.label || I18n.t('SIS');
  184. return (
  185. <MenuItem onSelect={this.launchPostGrades} key="post_grades_feature_tool">
  186. <span data-menu-id="post_grades_feature_tool">
  187. {I18n.t('Sync to %{sisName}', {sisName})}
  188. </span>
  189. </MenuItem>
  190. );
  191. }
  192. renderPreviousExports () {
  193. const previousExport = this.previousExport();
  194. if (!previousExport) return '';
  195. const lastExportDescription = previousExport.label;
  196. const downloadFrdUrl = previousExport.attachmentUrl;
  197. const previousMenu = (
  198. <MenuItem key="previousExport" onSelect={() => { ActionMenu.gotoUrl(downloadFrdUrl) }}>
  199. <span data-menu-id="previous-export">{lastExportDescription}</span>
  200. </MenuItem>
  201. );
  202. return [
  203. (<MenuItemSeparator key="previousExportSeparator" />),
  204. previousMenu
  205. ];
  206. }
  207. renderPublishGradesToSis () {
  208. const { isEnabled, publishToSisUrl } = this.props.publishGradesToSis;
  209. if (!isEnabled || !publishToSisUrl) {
  210. return null;
  211. }
  212. return (
  213. <MenuItem onSelect={() => { this.handlePublishGradesToSis() }}>
  214. <span data-menu-id="publish-grades-to-sis">
  215. {I18n.t('Sync grades to SIS')}
  216. </span>
  217. </MenuItem>
  218. );
  219. }
  220. render () {
  221. const buttonTypographyProps = {
  222. weight: 'normal',
  223. style: 'normal',
  224. size: 'medium',
  225. color: 'primary'
  226. };
  227. const publishGradesToSis = this.renderPublishGradesToSis();
  228. return (
  229. <PopoverMenu
  230. trigger={
  231. <Button variant="link">
  232. <Typography {...buttonTypographyProps}>
  233. { I18n.t('Actions') }<IconMiniArrowDownSolid />
  234. </Typography>
  235. </Button>
  236. }
  237. >
  238. { this.renderPostGradesTools() }
  239. {publishGradesToSis}
  240. <MenuItem disabled={this.disableImports()} onSelect={() => { this.handleImport() }}>
  241. <span data-menu-id="import">{ I18n.t('Import') }</span>
  242. </MenuItem>
  243. <MenuItem disabled={this.exportInProgress()} onSelect={() => { this.handleExport() }}>
  244. <span data-menu-id="export">
  245. { this.exportInProgress() ? I18n.t('Export in progress') : I18n.t('Export') }
  246. </span>
  247. </MenuItem>
  248. { [...this.renderPreviousExports()] }
  249. </PopoverMenu>
  250. );
  251. }
  252. }
  253. export default ActionMenu