refactor a2 content upload tab
we need to refactor the ContentUploadTab in a2 to allow a better separation between the graphql mutations and the component rendering. this will also set us up with a better foundation to add more submission types down the line and provides a better separation for testing purposes. Test Plan: * When you do things right, people won't be sure you've done anything at all. * This is just a refactor, all functionality should remain unchanged in the a2 content upload tab and all behavior around uploading and submitting a file. fixes COMMS-2110 Change-Id: Id4651bb8fbeb7d1f0e6fbb9903c1621133dde6a8 Reviewed-on: https://gerrit.instructure.com/196455 Tested-by: Jenkins Reviewed-by: Steven Burnett <sburnett@instructure.com> QA-Review: Matthew Lemon <mlemon@instructure.com> Product-Review: Ryan Norton <rnorton@instructure.com>
This commit is contained in:
parent
617266a92b
commit
c331a1a5dd
|
@ -120,7 +120,9 @@ describe('StudentView', () => {
|
|||
</MockedProvider>
|
||||
)
|
||||
|
||||
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(
|
||||
<MockedProvider
|
||||
defaultOptions={{mutate: {errorPolicy: 'all'}}}
|
||||
|
@ -147,7 +149,9 @@ describe('StudentView', () => {
|
|||
</MockedProvider>
|
||||
)
|
||||
|
||||
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', () => {
|
|||
</MockedProvider>
|
||||
)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<div data-testid="empty-upload">
|
||||
<Billboard
|
||||
heading={I18n.t('Upload File')}
|
||||
hero={DEFAULT_ICON}
|
||||
message={
|
||||
<Flex direction="column">
|
||||
{this.props.assignment.allowedExtensions.length ? (
|
||||
<FlexItem>
|
||||
{I18n.t('File permitted: %{fileTypes}', {
|
||||
fileTypes: this.props.assignment.allowedExtensions
|
||||
.map(ext => ext.toUpperCase())
|
||||
.join(', ')
|
||||
})}
|
||||
</FlexItem>
|
||||
) : null}
|
||||
<FlexItem padding="small 0 0">
|
||||
<Text size="small">
|
||||
{I18n.t('Drag and drop, or click to browse your computer')}
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderUploadedFiles() {
|
||||
const fileRows = chunk(this.state.files, 3)
|
||||
return (
|
||||
<div data-testid="non-empty-upload">
|
||||
<Grid>
|
||||
{fileRows.map(row => (
|
||||
<GridRow key={row.map(file => file.id).join()}>
|
||||
{row.map(file => (
|
||||
<GridCol key={file.id} vAlign="bottom">
|
||||
<Billboard
|
||||
heading={I18n.t('Uploaded')}
|
||||
headingLevel="h3"
|
||||
hero={
|
||||
this.shouldDisplayThumbnail(file) ? (
|
||||
<img
|
||||
alt={I18n.t('%{filename} preview', {filename: file.name})}
|
||||
height="75"
|
||||
src={file.preview}
|
||||
width="75"
|
||||
/>
|
||||
) : (
|
||||
getIconByType(mimeClass(file.type))
|
||||
)
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<span aria-hidden title={file.name}>
|
||||
{this.ellideString(file.name)}
|
||||
</span>
|
||||
<ScreenReaderContent>{file.name}</ScreenReaderContent>
|
||||
<Button
|
||||
icon={IconTrash}
|
||||
id={file.id}
|
||||
margin="0 0 0 x-small"
|
||||
onClick={this.handleRemoveFile}
|
||||
size="small"
|
||||
>
|
||||
<ScreenReaderContent>
|
||||
{I18n.t('Remove %{filename}', {filename: file.name})}
|
||||
</ScreenReaderContent>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</GridCol>
|
||||
))}
|
||||
</GridRow>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
submissionState: null
|
||||
}
|
||||
|
||||
updateAssignmentCache = (cache, mutationResult) => {
|
||||
|
@ -253,119 +78,54 @@ export default class ContentUploadTab extends Component {
|
|||
})
|
||||
}
|
||||
|
||||
renderAlert = (data, error) => {
|
||||
if (error) {
|
||||
return (
|
||||
<AssignmentAlert
|
||||
errorMessage={I18n.t('Error sending submission')}
|
||||
onDismiss={() => this.setState({submissionFailed: false, uploadingFiles: false})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (data) {
|
||||
return <AssignmentAlert successMessage={I18n.t('Submission sent')} />
|
||||
}
|
||||
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 (
|
||||
<div style={outerFooterStyle}>
|
||||
<div style={innerFooterStyle}>
|
||||
<Button
|
||||
variant="primary"
|
||||
margin="xx-small 0"
|
||||
onClick={() => this.submitAssignment(createSubmission)}
|
||||
>
|
||||
{I18n.t('Submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AssignmentAlert
|
||||
errorMessage={I18n.t('Error sending submission')}
|
||||
onDismiss={() => this.updateSubmissionState(null)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderSuccessAlert = () => {
|
||||
return <AssignmentAlert successMessage={I18n.t('Submission sent')} />
|
||||
}
|
||||
|
||||
renderFileUpload = createSubmission => {
|
||||
switch (this.state.submissionState) {
|
||||
case 'error':
|
||||
return this.renderErrorAlert()
|
||||
case 'in-progress':
|
||||
return <LoadingIndicator />
|
||||
case 'success':
|
||||
default:
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderSuccessAlert()}
|
||||
<FileUpload
|
||||
assignment={this.props.assignment}
|
||||
createSubmission={createSubmission}
|
||||
submission={this.props.submission}
|
||||
updateSubmissionState={this.updateSubmissionState}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Mutation mutation={CREATE_SUBMISSION} update={this.updateAssignmentCache}>
|
||||
{(createSubmission, {data, error}) => (
|
||||
<React.Fragment>
|
||||
{this.renderAlert(data, error || this.state.submissionFailed)}
|
||||
{/* TODO: replace loading indicator with a progress bar */}
|
||||
{this.state.uploadingFiles && !error && !this.state.submissionFailed && (
|
||||
<LoadingIndicator />
|
||||
)}
|
||||
{!this.state.uploadingFiles && !this.state.submissionFailed && (
|
||||
<React.Fragment>
|
||||
<FileDrop
|
||||
accept={
|
||||
this.props.assignment.allowedExtensions.length
|
||||
? this.props.assignment.allowedExtensions
|
||||
: ''
|
||||
}
|
||||
allowMultiple
|
||||
enablePreview
|
||||
id="inputFileDrop"
|
||||
label={
|
||||
this.state.files.length ? this.renderUploadedFiles() : this.renderEmptyUpload()
|
||||
}
|
||||
messages={this.state.messages}
|
||||
onDropAccepted={this.handleDropAccepted}
|
||||
onDropRejected={this.handleDropRejected}
|
||||
/>
|
||||
{this.state.files.length !== 0 && this.renderSubmitButton(createSubmission)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Mutation
|
||||
mutation={CREATE_SUBMISSION}
|
||||
onCompleted={() => this.updateSubmissionState('success')}
|
||||
onError={() => this.updateSubmissionState('error')}
|
||||
update={this.updateAssignmentCache}
|
||||
>
|
||||
{this.renderFileUpload}
|
||||
</Mutation>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<div data-testid="empty-upload">
|
||||
<Billboard
|
||||
heading={I18n.t('Upload File')}
|
||||
hero={DEFAULT_ICON}
|
||||
message={
|
||||
<Flex direction="column">
|
||||
{this.props.assignment.allowedExtensions.length ? (
|
||||
<FlexItem>
|
||||
{I18n.t('File permitted: %{fileTypes}', {
|
||||
fileTypes: this.props.assignment.allowedExtensions
|
||||
.map(ext => ext.toUpperCase())
|
||||
.join(', ')
|
||||
})}
|
||||
</FlexItem>
|
||||
) : null}
|
||||
<FlexItem padding="small 0 0">
|
||||
<Text size="small">
|
||||
{I18n.t('Drag and drop, or click to browse your computer')}
|
||||
</Text>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderUploadedFiles() {
|
||||
const fileRows = chunk(this.state.files, 3)
|
||||
return (
|
||||
<div data-testid="non-empty-upload">
|
||||
<Grid>
|
||||
{fileRows.map(row => (
|
||||
<GridRow key={row.map(file => file.id).join()}>
|
||||
{row.map(file => (
|
||||
<GridCol key={file.id} vAlign="bottom">
|
||||
<Billboard
|
||||
heading={I18n.t('Uploaded')}
|
||||
headingLevel="h3"
|
||||
hero={
|
||||
this.shouldDisplayThumbnail(file) ? (
|
||||
<img
|
||||
alt={I18n.t('%{filename} preview', {filename: file.name})}
|
||||
height="75"
|
||||
src={file.preview}
|
||||
width="75"
|
||||
/>
|
||||
) : (
|
||||
getIconByType(mimeClass(file.type))
|
||||
)
|
||||
}
|
||||
message={
|
||||
<div>
|
||||
<span aria-hidden title={file.name}>
|
||||
{this.ellideString(file.name)}
|
||||
</span>
|
||||
<ScreenReaderContent>{file.name}</ScreenReaderContent>
|
||||
<Button
|
||||
icon={IconTrash}
|
||||
id={file.id}
|
||||
margin="0 0 0 x-small"
|
||||
onClick={this.handleRemoveFile}
|
||||
size="small"
|
||||
>
|
||||
<ScreenReaderContent>
|
||||
{I18n.t('Remove %{filename}', {filename: file.name})}
|
||||
</ScreenReaderContent>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</GridCol>
|
||||
))}
|
||||
</GridRow>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={outerFooterStyle}>
|
||||
<div style={innerFooterStyle}>
|
||||
<Button variant="primary" margin="xx-small 0" onClick={() => this.submitAssignment()}>
|
||||
{I18n.t('Submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FileDrop
|
||||
accept={
|
||||
this.props.assignment.allowedExtensions.length
|
||||
? this.props.assignment.allowedExtensions
|
||||
: ''
|
||||
}
|
||||
allowMultiple
|
||||
enablePreview
|
||||
id="inputFileDrop"
|
||||
label={this.state.files.length ? this.renderUploadedFiles() : this.renderEmptyUpload()}
|
||||
messages={this.state.messages}
|
||||
onDropAccepted={this.handleDropAccepted}
|
||||
onDropRejected={this.handleDropRejected}
|
||||
/>
|
||||
{this.state.files.length !== 0 && this.renderSubmitButton()}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -15,9 +15,10 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
const fileInput = container.querySelector('input[type="file"]')
|
||||
|
@ -195,7 +196,7 @@ describe('ContentUploadTab', () => {
|
|||
const filename = 'c'.repeat(21)
|
||||
const {container, getByText} = render(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
const fileInput = container.querySelector('input[type="file"]')
|
||||
|
@ -210,7 +211,7 @@ describe('ContentUploadTab', () => {
|
|||
const mockedAssignment = mockAssignment({allowedExtensions: ['jpg, png']})
|
||||
const {getByTestId, getByText} = render(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockedAssignment} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockedAssignment} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
const emptyRender = getByTestId('empty-upload')
|
||||
|
@ -233,7 +234,7 @@ describe('ContentUploadTab', () => {
|
|||
const mockedAssignment = mockAssignment({allowedExtensions: ['jpg']})
|
||||
const {container, getByText, queryByTestId} = render(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockedAssignment} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockedAssignment} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
const fileInput = container.querySelector('input[type="file"]')
|
||||
|
@ -249,7 +250,7 @@ describe('ContentUploadTab', () => {
|
|||
const mockedAssignment = mockAssignment({allowedExtensions: ['jpg']})
|
||||
const {container, getByTestId, getByText, queryByText} = render(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockedAssignment} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockedAssignment} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
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(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mockSubmission()} />
|
||||
</MockedProvider>
|
||||
)
|
||||
|
||||
|
@ -294,7 +295,7 @@ describe('ContentUploadTab', () => {
|
|||
|
||||
const {getByTestId, getByText} = render(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mocked_submission} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mocked_submission} />
|
||||
</MockedProvider>
|
||||
)
|
||||
const uploadRender = getByTestId('non-empty-upload')
|
||||
|
@ -316,7 +317,7 @@ describe('ContentUploadTab', () => {
|
|||
|
||||
const {container, getByTestId} = render(
|
||||
<MockedProvider>
|
||||
<ContentUploadTab assignment={mockAssignment()} submission={mocked_submission} />
|
||||
<FileUpload assignment={mockAssignment()} submission={mocked_submission} />
|
||||
</MockedProvider>
|
||||
)
|
||||
const uploadRender = getByTestId('non-empty-upload')
|
Loading…
Reference in New Issue