implement drag and drop upload
closes CNVS-11668 test plan: - enable new files UI feature flag - in the new files interface: - drag a file from your OS to the folder area in the canvas UI - UI should update to reflect a dropable zone - drop the file - UI should return to normal state - previous upload process should take over Change-Id: Ifbf66eec86f431c5d29a50b3e6e05e21b20bc410 Reviewed-on: https://gerrit.instructure.com/41670 Reviewed-by: Ryan Shaw <ryan@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Clare Strong <clare@instructure.com> Product-Review: Jason Madsen <jmadsen@instructure.com>
This commit is contained in:
parent
bf2d25d962
commit
596848acc0
|
@ -1,11 +1,10 @@
|
|||
define [
|
||||
'i18n!current_uploads'
|
||||
'react'
|
||||
'compiled/react/shared/utils/withReactDOM'
|
||||
'../modules/UploadQueue'
|
||||
'./UploadProgress'
|
||||
'compiled/jquery.rails_flash_notifications'
|
||||
], (I18n, React, withReactDOM, UploadQueue, UploadProgress) ->
|
||||
], (React, withReactDOM, UploadQueue, UploadProgress) ->
|
||||
|
||||
CurrentUploads = React.createClass
|
||||
displayName: 'CurrentUploads'
|
||||
|
@ -35,23 +34,6 @@ define [
|
|||
percent = currentUploader.roundProgress()
|
||||
$.screenReaderFlashMessage "#{name} - #{percent}%"
|
||||
|
||||
buildInstructions: ->
|
||||
div className: 'current_uploads__instructions',
|
||||
a
|
||||
role: 'button'
|
||||
'aria-label': I18n.t('close', 'close')
|
||||
onClick: @handleCloseClick
|
||||
className: 'current_uploads__instructions__close',
|
||||
'\u2A09'
|
||||
i className: 'icon-upload current_uploads__instructions__icon-upload'
|
||||
div {},
|
||||
p className: 'current_uploads__instructions__drag',
|
||||
I18n.t('drag_files_here', 'Drag Folders and Files here')
|
||||
a
|
||||
role: 'button'
|
||||
onClick: @handleBrowseClick,
|
||||
I18n.t('click_to_browse', 'or click to browse your computer')
|
||||
|
||||
shouldDisplay: ->
|
||||
!!@state.isOpen || @state.currentUploads.length
|
||||
|
||||
|
@ -65,7 +47,7 @@ define [
|
|||
if @state.currentUploads.length
|
||||
@buildProgressViews()
|
||||
else if !!@state.isOpen
|
||||
@buildInstructions()
|
||||
div {}, ''
|
||||
|
||||
render: withReactDOM ->
|
||||
divName = ''
|
||||
|
|
|
@ -29,11 +29,24 @@ define [
|
|||
handleBackClick: ->
|
||||
@setState isEditing: false
|
||||
|
||||
# pass back expandZip to preserve options that was possibly already made
|
||||
# in a previous modal
|
||||
handleReplaceClick: ->
|
||||
@props.onNameConflictResolved({file: @state.fileOptions.file, dup: 'overwrite'})
|
||||
@props.onNameConflictResolved({
|
||||
file: @state.fileOptions.file
|
||||
dup: 'overwrite'
|
||||
expandZip: @state.fileOptions.expandZip
|
||||
})
|
||||
|
||||
# pass back expandZip to preserve options that was possibly already made
|
||||
# in a previous modal
|
||||
handleChangeClick: ->
|
||||
@props.onNameConflictResolved({file: @state.fileOptions.file, dup: 'rename', name: @refs.newName.getDOMNode().value})
|
||||
@props.onNameConflictResolved({
|
||||
file: @state.fileOptions.file
|
||||
dup: 'rename'
|
||||
name: @refs.newName.getDOMNode().value
|
||||
expandZip: @state.fileOptions.expandZip
|
||||
})
|
||||
|
||||
handleFormSubmit: (e) ->
|
||||
e.preventDefault()
|
||||
|
|
|
@ -11,7 +11,8 @@ define [
|
|||
'../utils/updateAPIQuerySortParams'
|
||||
'compiled/models/Folder'
|
||||
'./CurrentUploads'
|
||||
], (_, React, I18n, withReactDOM, filesEnv, ColumnHeaders, LoadingIndicator, FolderChild, getAllPages, updateAPIQuerySortParams, Folder, CurrentUploads) ->
|
||||
'./UploadDropZone'
|
||||
], (_, React, I18n, withReactDOM, filesEnv, ColumnHeaders, LoadingIndicator, FolderChild, getAllPages, updateAPIQuerySortParams, Folder, CurrentUploads, UploadDropZone) ->
|
||||
|
||||
LEADING_SLASH_TILL_BUT_NOT_INCLUDING_NEXT_SLASH = /^\/[^\/]*/
|
||||
|
||||
|
@ -79,6 +80,7 @@ define [
|
|||
role: 'grid'
|
||||
'aria-label': I18n.t('main_file_browser_pane', 'Main file browser pane')
|
||||
},
|
||||
UploadDropZone(currentFolder: @props.currentFolder)
|
||||
CurrentUploads({})
|
||||
ColumnHeaders {
|
||||
to: (if @props.params.splat then 'folder' else 'rootFolder')
|
||||
|
|
|
@ -9,6 +9,8 @@ define [
|
|||
'../modules/FileOptionsCollection'
|
||||
], (I18n, React, withReactDOM, _, FileRenameForm, customPropTypes, ZipFileOptionsForm, FileOptionsCollection) ->
|
||||
|
||||
resolvedUserAction = false
|
||||
|
||||
UploadButton = React.createClass
|
||||
displayName: 'UploadButton'
|
||||
|
||||
|
@ -28,6 +30,7 @@ define [
|
|||
this.refs.addFileInput.getDOMNode().click()
|
||||
|
||||
handleFilesInputChange: (e) ->
|
||||
resolvedUserAction = false
|
||||
files = this.refs.addFileInput.getDOMNode().files
|
||||
FileOptionsCollection.setFolder(@props.currentFolder)
|
||||
FileOptionsCollection.setOptionsFromFiles(files)
|
||||
|
@ -35,18 +38,50 @@ define [
|
|||
|
||||
onNameConflictResolved: (fileNameOptions) ->
|
||||
FileOptionsCollection.onNameConflictResolved(fileNameOptions)
|
||||
resolvedUserAction = true
|
||||
@setState(FileOptionsCollection.getState())
|
||||
|
||||
onZipOptionsResolved: (fileNameOptions) ->
|
||||
FileOptionsCollection.onZipOptionsResolved(fileNameOptions)
|
||||
resolvedUserAction = true
|
||||
@setState(FileOptionsCollection.getState())
|
||||
|
||||
onClose: ->
|
||||
@refs.form.getDOMNode().reset()
|
||||
if !resolvedUserAction
|
||||
# user dismissed zip or name conflict modal without resolving things
|
||||
# reset state to dump previously selected files
|
||||
FileOptionsCollection.resetState()
|
||||
@setState(FileOptionsCollection.getState())
|
||||
resolvedUserAction = false
|
||||
|
||||
componentDidUpdate: (prevState) ->
|
||||
if @state.nameCollisions.length == 0 && @state.resolvedNames.length > 0 && FileOptionsCollection.hasNewOptions()
|
||||
@queueUploads()
|
||||
else
|
||||
resolvedUserAction = false
|
||||
|
||||
componentWillMount: ->
|
||||
FileOptionsCollection.onChange = @setStateFromOptions
|
||||
|
||||
componentWillUnMount: ->
|
||||
FileOptionsCollection.onChange = null
|
||||
|
||||
setStateFromOptions: ->
|
||||
@setState(FileOptionsCollection.getState())
|
||||
|
||||
buildPotentialModal: ->
|
||||
if @state.zipOptions.length
|
||||
ZipFileOptionsForm
|
||||
fileOptions: @state.zipOptions[0]
|
||||
onZipOptionsResolved: @onZipOptionsResolved
|
||||
onClose: @onClose
|
||||
else if @state.nameCollisions.length
|
||||
FileRenameForm
|
||||
fileOptions: @state.nameCollisions[0]
|
||||
onNameConflictResolved: @onNameConflictResolved
|
||||
onClose: @onClose
|
||||
|
||||
|
||||
render: withReactDOM ->
|
||||
span {},
|
||||
|
@ -65,11 +100,4 @@ define [
|
|||
i className: 'icon-upload'
|
||||
span className: ('hidden-phone' if @props.showingButtons),
|
||||
I18n.t('upload', 'Upload')
|
||||
FileRenameForm
|
||||
fileOptions: @state.nameCollisions[0]
|
||||
onNameConflictResolved: @onNameConflictResolved
|
||||
onClose: @onClose
|
||||
ZipFileOptionsForm
|
||||
fileOptions: @state.zipOptions[0]
|
||||
onZipOptionsResolved: @onZipOptionsResolved
|
||||
onClose: @onClose
|
||||
@buildPotentialModal()
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
define [
|
||||
'i18n!upload_drop_zone'
|
||||
'react'
|
||||
'compiled/react/shared/utils/withReactDOM'
|
||||
'../modules/FileOptionsCollection'
|
||||
'compiled/models/Folder'
|
||||
], (I18n, React, withReactDOM, FileOptionsCollection, Folder) ->
|
||||
|
||||
UploadDropZone = React.createClass
|
||||
|
||||
displayName: 'UploadDropZone'
|
||||
|
||||
propTypes:
|
||||
currentFolder: React.PropTypes.instanceOf(Folder)
|
||||
|
||||
getInitialState: ->
|
||||
active: false
|
||||
|
||||
componentDidMount: ->
|
||||
@getParent().addEventListener('dragenter', @onParentDragEnter)
|
||||
document.addEventListener('dragenter', @killWindowDropDisplay)
|
||||
document.addEventListener('dragover', @killWindowDropDisplay)
|
||||
document.addEventListener('drop', @killWindowDrop)
|
||||
|
||||
componentWillUnmount: ->
|
||||
@getParent().removeEventListener('dragenter', @onParentDragEnter)
|
||||
document.removeEventListener('dragenter', @killWindowDropDisplay)
|
||||
document.removeEventListener('dragover', @killWindowDropDisplay)
|
||||
document.removeEventListener('drop', @killWindowDrop)
|
||||
|
||||
onDragEnter: (e) ->
|
||||
if @shouldAcceptDrop(e.dataTransfer.types)
|
||||
if !this.state.active
|
||||
@setState({active: true})
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
e.preventDefault()
|
||||
e.stopPropagation() # keep event from getting to document
|
||||
false
|
||||
else
|
||||
true
|
||||
|
||||
onDragLeave: (e) ->
|
||||
@setState({active: false})
|
||||
|
||||
onDrop: (e) ->
|
||||
FileOptionsCollection.setFolder(@props.currentFolder)
|
||||
FileOptionsCollection.setOptionsFromFiles(e.dataTransfer.files, true)
|
||||
@setState({active: false})
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
false
|
||||
|
||||
# when you drag a file over the parent, make drop zone active
|
||||
# remainder of drag-n-drop events happen on dropzone
|
||||
onParentDragEnter: (e)->
|
||||
if @shouldAcceptDrop(e.dataTransfer.types)
|
||||
if !this.state.active
|
||||
@setState({active: true})
|
||||
|
||||
killWindowDropDisplay: (e) ->
|
||||
if e.target != @getParent()
|
||||
e.preventDefault()
|
||||
|
||||
killWindowDrop: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
shouldAcceptDrop: (types) ->
|
||||
# event.dataTransfer.types doesn't respond to array methods in Firefox like
|
||||
# the other browsers even though it has length, and array access?
|
||||
i = 0
|
||||
found = false
|
||||
while i<types.length
|
||||
type = types[i]
|
||||
found = (type == 'Files')
|
||||
if (found)
|
||||
break
|
||||
i++
|
||||
found
|
||||
|
||||
getParent: ->
|
||||
@getDOMNode().parentElement
|
||||
|
||||
buildNonActiveDropZone: ->
|
||||
div
|
||||
className: 'UploadDropZone',
|
||||
''
|
||||
|
||||
buildInstructions: ->
|
||||
div className: 'UploadDropZone__instructions',
|
||||
i className: 'icon-upload UploadDropZone__instructions--icon-upload'
|
||||
div {},
|
||||
p className: 'UploadDropZone__instructions--drag',
|
||||
I18n.t('drop_to_upload', 'Drop items to upload')
|
||||
|
||||
buildDropZone: ->
|
||||
div
|
||||
className: 'UploadDropZone UploadDropZone__active'
|
||||
onDrop: this.onDrop
|
||||
onDragLeave: this.onDragLeave
|
||||
onDragOver: this.onDragEnter
|
||||
onDragEnter: this.onDragEnter,
|
||||
@buildInstructions()
|
||||
|
||||
render: withReactDOM ->
|
||||
if @state.active
|
||||
@buildDropZone()
|
||||
else
|
||||
@buildNonActiveDropZone()
|
|
@ -9,7 +9,10 @@ define [
|
|||
displayName: 'UploadProgress'
|
||||
|
||||
propTypes:
|
||||
uploader: React.PropTypes.instanceOf(FileUploader).isRequired
|
||||
uploader: React.PropTypes.shape({
|
||||
getFileName: React.PropTypes.func.isRequired
|
||||
roundProgress: React.PropTypes.func.inRequired
|
||||
})
|
||||
|
||||
getLabel: withReactDOM ->
|
||||
span {},
|
||||
|
|
|
@ -55,11 +55,11 @@ define [
|
|||
while i < selectedFiles.length
|
||||
fileOptions = selectedFiles[i]
|
||||
nameToTest = fileOptions.name || fileOptions.file.name
|
||||
# only mark as collision if it is a collision that hasn't been resolved, or is is a zip that will be expanded
|
||||
if @fileNameExists(nameToTest) && (fileOptions.dup != 'overwrite' && (!fileOptions.expandZip? || fileOptions.expandZip == false))
|
||||
collisions.push fileOptions
|
||||
else if (@isZipFile(fileOptions.file) && fileOptions.expandZip == undefined)
|
||||
if (@isZipFile(fileOptions.file) && fileOptions.expandZip == undefined)
|
||||
zips.push fileOptions
|
||||
# only mark as collision if it is a collision that hasn't been resolved, or is is a zip that will be expanded
|
||||
else if @fileNameExists(nameToTest) && (fileOptions.dup != 'overwrite' && (!fileOptions.expandZip? || fileOptions.expandZip == false))
|
||||
collisions.push fileOptions
|
||||
else
|
||||
resolved.push fileOptions
|
||||
i++
|
||||
|
@ -101,10 +101,12 @@ define [
|
|||
{resolved, collisions, zips} = @segregateOptionBuckets(allOptions)
|
||||
@setState({nameCollisions: collisions, resolvedNames: resolved, zipOptions: zips})
|
||||
|
||||
setOptionsFromFiles: (files) ->
|
||||
setOptionsFromFiles: (files, notifyChange) ->
|
||||
allOptions = @toFilesOptionArray(files)
|
||||
{resolved, collisions, zips} = @segregateOptionBuckets(allOptions)
|
||||
@setState({nameCollisions: collisions, resolvedNames: resolved, zipOptions: zips, newOptions: true})
|
||||
if notifyChange && @onChange
|
||||
@onChange()
|
||||
|
||||
hasNewOptions: ->
|
||||
return @state.newOptions
|
||||
|
@ -124,4 +126,7 @@ define [
|
|||
resetState: ->
|
||||
@state = @buildDefaultState()
|
||||
|
||||
onChange: ->
|
||||
#noop
|
||||
|
||||
new FileOptionsCollection()
|
||||
|
|
|
@ -32,7 +32,7 @@ define [
|
|||
@_actualUpload()
|
||||
|
||||
onUploadPosted: (uploadResults) =>
|
||||
if (event.target.status >= 400)
|
||||
if (uploadResults.target && uploadResults.target.status >= 400)
|
||||
@deferred.reject()
|
||||
return
|
||||
|
||||
|
@ -41,7 +41,7 @@ define [
|
|||
$.getJSON(url).then (results) =>
|
||||
@getContentMigration()
|
||||
else
|
||||
results = $.parseJSON(event.target.response)
|
||||
results = $.parseJSON(uploadResults.target.response)
|
||||
@getContentMigration()
|
||||
|
||||
# get the content migration when ready and use progress api to pull migration progress
|
||||
|
|
|
@ -93,6 +93,7 @@ $ef-thumbnail-size: 36px;
|
|||
}
|
||||
}
|
||||
.ef-directory{
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
flex: 3 3 0;
|
||||
display: flex;
|
||||
|
@ -246,27 +247,6 @@ $ef-thumbnail-size: 36px;
|
|||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.current_uploads__instructions {
|
||||
text-align: center;
|
||||
}
|
||||
.current_uploads__instructions__close {
|
||||
float: right;
|
||||
color: $canvas-neutral;
|
||||
font-size: -1.8em;
|
||||
pointer: cursor;
|
||||
}
|
||||
i[class*=current_uploads__instructions__icon-upload]:before {
|
||||
font-size: 100px;
|
||||
}
|
||||
i[class*=current_uploads__instructions__icon-upload] {
|
||||
color: $canvas-primary;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.current_uploads__instructions__drag {
|
||||
font-size: 1.8em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ef-breadcrumb-popover {
|
||||
display: block;
|
||||
|
@ -289,7 +269,44 @@ i[class*=current_uploads__instructions__icon-upload] {
|
|||
}
|
||||
}
|
||||
|
||||
.UploadDropZone {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background: rgba(254, 254, 254, 0);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.UploadDropZone__active {
|
||||
background: rgba(254, 254, 254, 0.9);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.UploadDropZone__instructions {
|
||||
margin-top: 70px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.UploadDropZone__instructions__close {
|
||||
float: right;
|
||||
color: $canvas-neutral;
|
||||
font-size: -1.8em;
|
||||
pointer: cursor;
|
||||
}
|
||||
i[class*=UploadDropZone__instructions--icon-upload]:before {
|
||||
font-size: 100px;
|
||||
}
|
||||
i[class*=UploadDropZone__instructions--icon-upload] {
|
||||
color: $canvas-primary;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.UploadDropZone__instructions--drag {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -58,3 +58,29 @@ define [
|
|||
Simulate.click(@form.refs.renameBtn.getDOMNode())
|
||||
ok(@form.state.isEditing)
|
||||
Simulate.click(@form.refs.commitChangeBtn.getDOMNode())
|
||||
|
||||
test 'onNameConflicResolved preserves expandZip option when renaming', ->
|
||||
expect(2)
|
||||
@form.setProps(
|
||||
fileOptions:
|
||||
file:
|
||||
name: 'file_name.md'
|
||||
expandZip: 'true'
|
||||
onNameConflictResolved: (options) ->
|
||||
equal(options.expandZip, 'true')
|
||||
)
|
||||
Simulate.click(@form.refs.renameBtn.getDOMNode())
|
||||
ok(@form.state.isEditing)
|
||||
Simulate.click(@form.refs.commitChangeBtn.getDOMNode())
|
||||
|
||||
test 'onNameConflicResolved preserves expandZip option when replacing', ->
|
||||
expect(1)
|
||||
@form.setProps(
|
||||
fileOptions:
|
||||
file:
|
||||
name: 'file_name.md'
|
||||
expandZip: 'true'
|
||||
onNameConflictResolved: (options) ->
|
||||
equal(options.expandZip, 'true')
|
||||
)
|
||||
Simulate.click(@form.refs.replaceBtn.getDOMNode())
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
define [
|
||||
'react'
|
||||
'compiled/react_files/components/UploadDropZone'
|
||||
], (React, UploadDropZone) ->
|
||||
|
||||
Simulate = React.addons.TestUtils.Simulate
|
||||
|
||||
node = document.querySelector('#fixtures')
|
||||
|
||||
module 'UploadDropZone',
|
||||
setup: ->
|
||||
@uploadZone = React.renderComponent(UploadDropZone({}), node)
|
||||
|
||||
teardown: ->
|
||||
React.unmountComponentAtNode(node)
|
||||
|
||||
test 'displays nothing by default', ->
|
||||
displayText = @uploadZone.getDOMNode().innerHTML.trim()
|
||||
equal(displayText, '')
|
||||
|
||||
test 'displays dropzone when active', ->
|
||||
@uploadZone.setState({active: true})
|
||||
ok(@uploadZone.getDOMNode().querySelector('.UploadDropZone__instructions'))
|
||||
|
||||
test 'handles drop event on target', ->
|
||||
sinon.stub(@uploadZone, 'onDrop')
|
||||
|
||||
@uploadZone.setState({active: true})
|
||||
dataTransfer = {
|
||||
types: ['Files']
|
||||
}
|
||||
|
||||
n = @uploadZone.getDOMNode()
|
||||
Simulate.dragEnter(n, {dataTransfer: dataTransfer})
|
||||
Simulate.dragOver(n, {dataTransfer: dataTransfer})
|
||||
Simulate.drop(n)
|
||||
ok(@uploadZone.onDrop.calledOnce, 'handles file drops')
|
||||
|
||||
@uploadZone.onDrop.restore()
|
Loading…
Reference in New Issue