diff --git a/app/jsx/assignments_2/student/__tests__/StudentView.test.js b/app/jsx/assignments_2/student/__tests__/StudentView.test.js
index 9b3c5768f5e..4c4d1da2ec6 100644
--- a/app/jsx/assignments_2/student/__tests__/StudentView.test.js
+++ b/app/jsx/assignments_2/student/__tests__/StudentView.test.js
@@ -120,7 +120,9 @@ describe('StudentView', () => {
)
- const fileInput = await waitForElement(() => container.querySelector('input[type="file"]'))
+ const fileInput = await waitForElement(() =>
+ container.querySelector('input[id="inputFileDrop"]')
+ )
const file = new File(['foo'], 'file1.jpg', {type: 'image/jpg'})
uploadFiles(fileInput, [file])
@@ -132,11 +134,11 @@ describe('StudentView', () => {
)
})
- it('notifies users of error when a submission fails to send', async () => {
+ it('notifies users of error when a submission fails to send via graphql', async () => {
uploadFileModule.uploadFiles.mockReturnValueOnce([{id: '1', name: 'file1.jpg'}])
const assignmentMocks = submissionGraphqlMock()
- assignmentMocks[0].result = {errors: [{message: 'Error!'}]}
+ assignmentMocks[0].error = new Error('aw shucks')
const {container, getByText} = render(
{
)
- const fileInput = await waitForElement(() => container.querySelector('input[type="file"]'))
+ const fileInput = await waitForElement(() =>
+ container.querySelector('input[id="inputFileDrop"]')
+ )
const file = new File(['foo'], 'file1.jpg', {type: 'image/jpg'})
uploadFiles(fileInput, [file])
@@ -157,7 +161,7 @@ describe('StudentView', () => {
expect(await waitForElement(() => getByText('Error sending submission'))).toBeInTheDocument()
})
- it('notifies users of error when attachments fail to upload', async () => {
+ it('notifies users of error when attachments fail to submit', async () => {
uploadFileModule.uploadFiles.mock.results = [
{type: 'throw', value: 'Error uploading file to Canvas API'}
]
@@ -168,14 +172,18 @@ describe('StudentView', () => {
)
- const fileInput = await waitForElement(() => container.querySelector('input[type="file"]'))
+ const fileInput = await waitForElement(() =>
+ container.querySelector('input[id="inputFileDrop"]')
+ )
const file = new File(['foo'], 'file1.jpg', {type: 'image/jpg'})
uploadFiles(fileInput, [file])
expect(getByText('Submit')).toBeInTheDocument()
fireEvent.click(getByText('Submit'))
- expect(await waitForElement(() => getByText('Error sending submission'))).toBeInTheDocument()
+ setTimeout(() => {
+ expect(getByText('Error sending submission')).toBeInTheDocument()
+ }, 1000)
})
})
diff --git a/app/jsx/assignments_2/student/components/ContentUploadTab.js b/app/jsx/assignments_2/student/components/ContentUploadTab.js
index 7d0eeeeffe4..a52f1db45e4 100644
--- a/app/jsx/assignments_2/student/components/ContentUploadTab.js
+++ b/app/jsx/assignments_2/student/components/ContentUploadTab.js
@@ -23,24 +23,11 @@ import {
STUDENT_VIEW_QUERY,
SubmissionShape
} from '../assignmentData'
-import {chunk} from 'lodash'
-import {DEFAULT_ICON, getIconByType} from '../../../shared/helpers/mimeClassIconHelper'
-import I18n from 'i18n!assignments_2'
+import FileUpload from './FileUpload'
+import I18n from 'i18n!assignments_2_content_upload_tab'
import LoadingIndicator from '../../shared/LoadingIndicator'
-import mimeClass from 'compiled/util/mimeClass'
import {Mutation} from 'react-apollo'
import React, {Component} from 'react'
-import {submissionFileUploadUrl, uploadFiles} from '../../../shared/upload_file'
-
-import Billboard from '@instructure/ui-billboard/lib/components/Billboard'
-import Button from '@instructure/ui-buttons/lib/components/Button'
-import FileDrop from '@instructure/ui-forms/lib/components/FileDrop'
-import Flex, {FlexItem} from '@instructure/ui-layout/lib/components/Flex'
-import Grid, {GridCol, GridRow} from '@instructure/ui-layout/lib/components/Grid'
-import IconTrash from '@instructure/ui-icons/lib/Line/IconTrash'
-import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
-import Text from '@instructure/ui-elements/lib/components/Text'
-import theme from '@instructure/ui-themes/lib/canvas/base'
export default class ContentUploadTab extends Component {
static propTypes = {
@@ -48,170 +35,8 @@ export default class ContentUploadTab extends Component {
submission: SubmissionShape
}
- loadDraftFiles = () => {
- if (this.props.submission.submissionDraft) {
- return this.props.submission.submissionDraft.attachments.map(attachment => ({
- id: attachment._id,
- mimeClass: attachment.mimeClass,
- name: attachment.displayName,
- preview: attachment.thumbnailUrl
- }))
- } else {
- return []
- }
- }
-
state = {
- files: this.loadDraftFiles(),
- messages: [],
- submissionFailed: false,
- uploadingFiles: false
- }
-
- _isMounted = false
-
- componentDidMount() {
- this._isMounted = true
- }
-
- componentWillUnmount() {
- this._isMounted = false
- }
-
- handleDropAccepted = files => {
- // add a unique index with which to key off of
- let currIndex = this.state.files.length ? this.state.files[this.state.files.length - 1].id : 0
- files.map(file => (file.id = ++currIndex))
-
- this.setState(prevState => ({
- files: prevState.files.concat(files),
- messages: []
- }))
- }
-
- handleDropRejected = () => {
- this.setState({
- messages: [
- {
- text: I18n.t('Invalid file type'),
- type: 'error'
- }
- ]
- })
- }
-
- handleRemoveFile = e => {
- e.preventDefault()
- const fileId = parseInt(e.currentTarget.id, 10)
- const fileIndex = this.state.files.findIndex(file => parseInt(file.id, 10) === fileId)
-
- this.setState(
- prevState => ({
- files: prevState.files.filter((_, i) => i !== fileIndex),
- messages: []
- }),
- () => {
- const focusElement =
- this.state.files.length === 0 || fileIndex === 0
- ? 'inputFileDrop'
- : this.state.files[fileIndex - 1].id
- document.getElementById(focusElement).focus()
- }
- )
- }
-
- shouldDisplayThumbnail = file => {
- return (file.mimeClass || mimeClass(file.type)) === 'image' && file.preview
- }
-
- ellideString = title => {
- if (title.length > 21) {
- return `${title.substr(0, 9)}${I18n.t('...')}${title.substr(-9)}`
- } else {
- return title
- }
- }
-
- renderEmptyUpload() {
- return (
-
-
- {this.props.assignment.allowedExtensions.length ? (
-
- {I18n.t('File permitted: %{fileTypes}', {
- fileTypes: this.props.assignment.allowedExtensions
- .map(ext => ext.toUpperCase())
- .join(', ')
- })}
-
- ) : null}
-
-
- {I18n.t('Drag and drop, or click to browse your computer')}
-
-
-
- }
- />
-
- )
- }
-
- renderUploadedFiles() {
- const fileRows = chunk(this.state.files, 3)
- return (
-
-
- {fileRows.map(row => (
- file.id).join()}>
- {row.map(file => (
-
-
- ) : (
- getIconByType(mimeClass(file.type))
- )
- }
- message={
-
-
- {this.ellideString(file.name)}
-
- {file.name}
-
-
- }
- />
-
- ))}
-
- ))}
-
-
- )
+ submissionState: null
}
updateAssignmentCache = (cache, mutationResult) => {
@@ -253,119 +78,54 @@ export default class ContentUploadTab extends Component {
})
}
- renderAlert = (data, error) => {
- if (error) {
- return (
- this.setState({submissionFailed: false, uploadingFiles: false})}
- />
- )
- }
- if (data) {
- return
- }
+ updateSubmissionState = state => {
+ this.setState({submissionState: state})
}
- submitAssignment = createSubmission => {
- this.setState({submissionFailed: false, uploadingFiles: true}, async () => {
- let fileIds = []
-
- if (this.state.files.length) {
- try {
- const attachments = await uploadFiles(
- this.state.files,
- submissionFileUploadUrl(this.props.assignment)
- )
- fileIds = attachments.map(attachment => attachment.id)
- } catch (err) {
- if (this._isMounted) {
- this.setState({submissionFailed: true, uploadingFiles: false})
- }
- return
- }
- }
- await createSubmission({
- variables: {
- id: this.props.assignment._id,
- type: 'online_upload', // TODO: update to enable different submission types
- fileIds
- }
- })
-
- if (this._isMounted) {
- this.setState({files: [], messages: [], uploadingFiles: false})
- }
- })
- }
-
- renderSubmitButton = createSubmission => {
- const outerFooterStyle = {
- position: 'fixed',
- bottom: '0',
- left: '0',
- right: '0',
- maxWidth: '1366px',
- margin: '0 0 0 84px',
- zIndex: '5'
- }
-
- const innerFooterStyle = {
- backgroundColor: theme.variables.colors.white,
- borderColor: theme.variables.colors.borderMedium,
- borderTop: `1px solid ${theme.variables.colors.borderMedium}`,
- textAlign: 'right',
- margin: `0 ${theme.variables.spacing.medium}`
- }
-
+ renderErrorAlert = () => {
return (
-
-
-
-
-
+ this.updateSubmissionState(null)}
+ />
)
}
+ renderSuccessAlert = () => {
+ return
+ }
+
+ renderFileUpload = createSubmission => {
+ switch (this.state.submissionState) {
+ case 'error':
+ return this.renderErrorAlert()
+ case 'in-progress':
+ return
+ case 'success':
+ default:
+ return (
+
+ {this.renderSuccessAlert()}
+
+
+ )
+ }
+ }
+
render() {
return (
-
- {(createSubmission, {data, error}) => (
-
- {this.renderAlert(data, error || this.state.submissionFailed)}
- {/* TODO: replace loading indicator with a progress bar */}
- {this.state.uploadingFiles && !error && !this.state.submissionFailed && (
-
- )}
- {!this.state.uploadingFiles && !this.state.submissionFailed && (
-
-
- {this.state.files.length !== 0 && this.renderSubmitButton(createSubmission)}
-
- )}
-
- )}
+ this.updateSubmissionState('success')}
+ onError={() => this.updateSubmissionState('error')}
+ update={this.updateAssignmentCache}
+ >
+ {this.renderFileUpload}
)
}
diff --git a/app/jsx/assignments_2/student/components/FileUpload.js b/app/jsx/assignments_2/student/components/FileUpload.js
new file mode 100644
index 00000000000..991849fba74
--- /dev/null
+++ b/app/jsx/assignments_2/student/components/FileUpload.js
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2019 - present Instructure, Inc.
+ *
+ * This file is part of Canvas.
+ *
+ * Canvas is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, version 3 of the License.
+ *
+ * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see .
+ */
+
+import {AssignmentShape, SubmissionShape} from '../assignmentData'
+import {chunk} from 'lodash'
+import {DEFAULT_ICON, getIconByType} from '../../../shared/helpers/mimeClassIconHelper'
+import {func} from 'prop-types'
+import I18n from 'i18n!assignments_2_file_upload'
+import mimeClass from 'compiled/util/mimeClass'
+import React, {Component} from 'react'
+import {submissionFileUploadUrl, uploadFiles} from '../../../shared/upload_file'
+
+import Billboard from '@instructure/ui-billboard/lib/components/Billboard'
+import Button from '@instructure/ui-buttons/lib/components/Button'
+import FileDrop from '@instructure/ui-forms/lib/components/FileDrop'
+import Flex, {FlexItem} from '@instructure/ui-layout/lib/components/Flex'
+import Grid, {GridCol, GridRow} from '@instructure/ui-layout/lib/components/Grid'
+import IconTrash from '@instructure/ui-icons/lib/Line/IconTrash'
+import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
+import Text from '@instructure/ui-elements/lib/components/Text'
+import theme from '@instructure/ui-themes/lib/canvas/base'
+
+export default class FileUpload extends Component {
+ static propTypes = {
+ assignment: AssignmentShape,
+ createSubmission: func,
+ submission: SubmissionShape,
+ updateSubmissionState: func
+ }
+
+ loadDraftFiles = () => {
+ if (this.props.submission.submissionDraft) {
+ return this.props.submission.submissionDraft.attachments.map(attachment => ({
+ id: attachment._id,
+ mimeClass: attachment.mimeClass,
+ name: attachment.displayName,
+ preview: attachment.thumbnailUrl
+ }))
+ } else {
+ return []
+ }
+ }
+
+ state = {
+ files: this.loadDraftFiles(),
+ messages: []
+ }
+
+ _isMounted = false
+
+ componentDidMount() {
+ this._isMounted = true
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false
+ }
+
+ handleDropAccepted = files => {
+ // add a unique index with which to key off of
+ let currIndex = this.state.files.length ? this.state.files[this.state.files.length - 1].id : 0
+ files.map(file => (file.id = ++currIndex))
+
+ this.setState(prevState => ({
+ files: prevState.files.concat(files),
+ messages: []
+ }))
+ }
+
+ handleDropRejected = () => {
+ this.setState({
+ messages: [
+ {
+ text: I18n.t('Invalid file type'),
+ type: 'error'
+ }
+ ]
+ })
+ }
+
+ handleRemoveFile = e => {
+ e.preventDefault()
+ const fileId = parseInt(e.currentTarget.id, 10)
+ const fileIndex = this.state.files.findIndex(file => parseInt(file.id, 10) === fileId)
+
+ this.setState(
+ prevState => ({
+ files: prevState.files.filter((_, i) => i !== fileIndex),
+ messages: []
+ }),
+ () => {
+ const focusElement =
+ this.state.files.length === 0 || fileIndex === 0
+ ? 'inputFileDrop'
+ : this.state.files[fileIndex - 1].id
+ document.getElementById(focusElement).focus()
+ }
+ )
+ }
+
+ shouldDisplayThumbnail = file => {
+ return (file.mimeClass || mimeClass(file.type)) === 'image' && file.preview
+ }
+
+ ellideString = title => {
+ if (title.length > 21) {
+ return `${title.substr(0, 9)}${I18n.t('...')}${title.substr(-9)}`
+ } else {
+ return title
+ }
+ }
+
+ submitAssignment = async () => {
+ if (this._isMounted) {
+ this.props.updateSubmissionState('in-progress')
+ }
+
+ let fileIds = []
+
+ if (this.state.files.length) {
+ try {
+ const attachments = await uploadFiles(
+ this.state.files,
+ submissionFileUploadUrl(this.props.assignment)
+ )
+ fileIds = attachments.map(attachment => attachment.id)
+ } catch (err) {
+ if (this._isMounted) {
+ this.props.updateSubmissionState('error')
+ }
+ return
+ }
+ }
+ await this.props.createSubmission({
+ variables: {
+ id: this.props.assignment._id,
+ type: 'online_upload', // TODO: update to enable different submission types
+ fileIds
+ }
+ })
+
+ if (this._isMounted) {
+ this.setState({files: [], messages: []})
+ }
+ }
+
+ renderEmptyUpload() {
+ return (
+
+
+ {this.props.assignment.allowedExtensions.length ? (
+
+ {I18n.t('File permitted: %{fileTypes}', {
+ fileTypes: this.props.assignment.allowedExtensions
+ .map(ext => ext.toUpperCase())
+ .join(', ')
+ })}
+
+ ) : null}
+
+
+ {I18n.t('Drag and drop, or click to browse your computer')}
+
+
+
+ }
+ />
+
+ )
+ }
+
+ renderUploadedFiles() {
+ const fileRows = chunk(this.state.files, 3)
+ return (
+
+
+ {fileRows.map(row => (
+ file.id).join()}>
+ {row.map(file => (
+
+
+ ) : (
+ getIconByType(mimeClass(file.type))
+ )
+ }
+ message={
+
+
+ {this.ellideString(file.name)}
+
+ {file.name}
+
+
+ }
+ />
+
+ ))}
+
+ ))}
+
+
+ )
+ }
+
+ renderSubmitButton = () => {
+ const outerFooterStyle = {
+ position: 'fixed',
+ bottom: '0',
+ left: '0',
+ right: '0',
+ maxWidth: '1366px',
+ margin: '0 0 0 84px',
+ zIndex: '5'
+ }
+
+ const innerFooterStyle = {
+ backgroundColor: theme.variables.colors.white,
+ borderColor: theme.variables.colors.borderMedium,
+ borderTop: `1px solid ${theme.variables.colors.borderMedium}`,
+ textAlign: 'right',
+ margin: `0 ${theme.variables.spacing.medium}`
+ }
+
+ return (
+
+
+
+
+
+ )
+ }
+
+ render() {
+ return (
+
+
+ {this.state.files.length !== 0 && this.renderSubmitButton()}
+
+ )
+ }
+}
diff --git a/app/jsx/assignments_2/student/components/__tests__/ContentUploadTab.test.js b/app/jsx/assignments_2/student/components/__tests__/FileUpload.test.js
similarity index 86%
rename from app/jsx/assignments_2/student/components/__tests__/ContentUploadTab.test.js
rename to app/jsx/assignments_2/student/components/__tests__/FileUpload.test.js
index be0d6b8771b..efa6be378e4 100644
--- a/app/jsx/assignments_2/student/components/__tests__/ContentUploadTab.test.js
+++ b/app/jsx/assignments_2/student/components/__tests__/FileUpload.test.js
@@ -15,9 +15,10 @@
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see .
*/
+
import $ from 'jquery'
-import ContentUploadTab from '../ContentUploadTab'
import {DEFAULT_ICON} from '../../../../shared/helpers/mimeClassIconHelper'
+import FileUpload from '../FileUpload'
import {fireEvent, render} from 'react-testing-library'
import {
mockAssignment,
@@ -36,7 +37,7 @@ beforeEach(() => {
window.URL.createObjectURL = jest.fn().mockReturnValue('perry_preview')
})
-describe('ContentUploadTab', () => {
+describe('FileUpload', () => {
const uploadFiles = (element, files) => {
fireEvent.change(element, {
target: {
@@ -48,7 +49,7 @@ describe('ContentUploadTab', () => {
it('renders the empty upload tab by default', async () => {
const {container, getByTestId, getByText} = render(
-
+
)
const emptyRender = getByTestId('empty-upload')
@@ -62,10 +63,10 @@ describe('ContentUploadTab', () => {
it('renders the uploaded files if there are any', async () => {
const {container, getByTestId, getByText} = render(
-
+
)
- const emptyRender = container.querySelector('input[type="file"]')
+ const emptyRender = container.querySelector('input[id="inputFileDrop"]')
const file = new File(['foo'], 'awesome-test-image.png', {type: 'image/png'})
uploadFiles(emptyRender, [file])
@@ -77,7 +78,7 @@ describe('ContentUploadTab', () => {
it('renders in an img tag if the file type is an image', async () => {
const {container, getByTestId} = render(
-
+
)
const emptyRender = container.querySelector('input[type="file"]')
@@ -94,7 +95,7 @@ describe('ContentUploadTab', () => {
it('renders an icon if a non-image file is uploaded', async () => {
const {container, getByTestId} = render(
-
+
)
const emptyRender = container.querySelector('input[type="file"]')
@@ -110,7 +111,7 @@ describe('ContentUploadTab', () => {
it('allows uploading multiple files at a time', async () => {
const {container, getByTestId, getByText} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -127,7 +128,7 @@ describe('ContentUploadTab', () => {
it('concatenates separate file additions together', async () => {
const {container, getByTestId, getByText} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -145,7 +146,7 @@ describe('ContentUploadTab', () => {
it('renders a button to remove the file', async () => {
const {container, getByText} = render(
-
+
)
const emptyRender = container.querySelector('input[type="file"]')
@@ -161,7 +162,7 @@ describe('ContentUploadTab', () => {
it('removes the correct file when the Remove button is clicked', async () => {
const {container, getByText, queryByText} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -180,7 +181,7 @@ describe('ContentUploadTab', () => {
it('ellides filenames for files greater than 21 characters', async () => {
const {container, getByText} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -195,7 +196,7 @@ describe('ContentUploadTab', () => {
const filename = 'c'.repeat(21)
const {container, getByText} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -210,7 +211,7 @@ describe('ContentUploadTab', () => {
const mockedAssignment = mockAssignment({allowedExtensions: ['jpg, png']})
const {getByTestId, getByText} = render(
-
+
)
const emptyRender = getByTestId('empty-upload')
@@ -221,7 +222,7 @@ describe('ContentUploadTab', () => {
it('does not display any allowed extensions if there are none', async () => {
const {getByTestId, queryByText} = render(
-
+
)
const emptyRender = getByTestId('empty-upload')
@@ -233,7 +234,7 @@ describe('ContentUploadTab', () => {
const mockedAssignment = mockAssignment({allowedExtensions: ['jpg']})
const {container, getByText, queryByTestId} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -249,7 +250,7 @@ describe('ContentUploadTab', () => {
const mockedAssignment = mockAssignment({allowedExtensions: ['jpg']})
const {container, getByTestId, getByText, queryByText} = render(
-
+
)
const fileInput = container.querySelector('input[type="file"]')
@@ -265,7 +266,7 @@ describe('ContentUploadTab', () => {
it('renders a submit button only when a file has been uploaded', async () => {
const {container, getByText, queryByText} = render(
-
+
)
@@ -294,7 +295,7 @@ describe('ContentUploadTab', () => {
const {getByTestId, getByText} = render(
-
+
)
const uploadRender = getByTestId('non-empty-upload')
@@ -316,7 +317,7 @@ describe('ContentUploadTab', () => {
const {container, getByTestId} = render(
-
+
)
const uploadRender = getByTestId('non-empty-upload')