diff --git a/app/coffeescripts/react_files/components/CurrentUploads.coffee b/app/coffeescripts/react_files/components/CurrentUploads.coffee index bb58f41b1a6..b740d4e3b73 100644 --- a/app/coffeescripts/react_files/components/CurrentUploads.coffee +++ b/app/coffeescripts/react_files/components/CurrentUploads.coffee @@ -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 = '' diff --git a/app/coffeescripts/react_files/components/FileRenameForm.coffee b/app/coffeescripts/react_files/components/FileRenameForm.coffee index 20d4239af76..43bed4c07eb 100644 --- a/app/coffeescripts/react_files/components/FileRenameForm.coffee +++ b/app/coffeescripts/react_files/components/FileRenameForm.coffee @@ -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() diff --git a/app/coffeescripts/react_files/components/ShowFolder.coffee b/app/coffeescripts/react_files/components/ShowFolder.coffee index eecc9534fa3..87b1b6fdbe2 100644 --- a/app/coffeescripts/react_files/components/ShowFolder.coffee +++ b/app/coffeescripts/react_files/components/ShowFolder.coffee @@ -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') diff --git a/app/coffeescripts/react_files/components/UploadButton.coffee b/app/coffeescripts/react_files/components/UploadButton.coffee index cd659850df1..b527919c4d7 100644 --- a/app/coffeescripts/react_files/components/UploadButton.coffee +++ b/app/coffeescripts/react_files/components/UploadButton.coffee @@ -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() diff --git a/app/coffeescripts/react_files/components/UploadDropZone.coffee b/app/coffeescripts/react_files/components/UploadDropZone.coffee new file mode 100644 index 00000000000..088edff9f29 --- /dev/null +++ b/app/coffeescripts/react_files/components/UploadDropZone.coffee @@ -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 + @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() diff --git a/app/coffeescripts/react_files/components/UploadProgress.coffee b/app/coffeescripts/react_files/components/UploadProgress.coffee index e3fe8302b5f..97fb9a46918 100644 --- a/app/coffeescripts/react_files/components/UploadProgress.coffee +++ b/app/coffeescripts/react_files/components/UploadProgress.coffee @@ -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 {}, diff --git a/app/coffeescripts/react_files/modules/FileOptionsCollection.coffee b/app/coffeescripts/react_files/modules/FileOptionsCollection.coffee index 91b95f8d323..c7b063b0f9c 100644 --- a/app/coffeescripts/react_files/modules/FileOptionsCollection.coffee +++ b/app/coffeescripts/react_files/modules/FileOptionsCollection.coffee @@ -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() diff --git a/app/coffeescripts/react_files/modules/ZipUploader.coffee b/app/coffeescripts/react_files/modules/ZipUploader.coffee index f0220d399a8..ab5efb63ab6 100644 --- a/app/coffeescripts/react_files/modules/ZipUploader.coffee +++ b/app/coffeescripts/react_files/modules/ZipUploader.coffee @@ -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 diff --git a/app/stylesheets/pages/ember_files.scss b/app/stylesheets/pages/ember_files.scss index 26f160a4883..fd4c2923e1c 100644 --- a/app/stylesheets/pages/ember_files.scss +++ b/app/stylesheets/pages/ember_files.scss @@ -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; +} diff --git a/spec/coffeescripts/react_files/components/FileRenameFormSpec.coffee b/spec/coffeescripts/react_files/components/FileRenameFormSpec.coffee index 13fbac8215a..c4b0322c2b8 100644 --- a/spec/coffeescripts/react_files/components/FileRenameFormSpec.coffee +++ b/spec/coffeescripts/react_files/components/FileRenameFormSpec.coffee @@ -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()) diff --git a/spec/coffeescripts/react_files/components/UploadDropZoneSpec.coffee b/spec/coffeescripts/react_files/components/UploadDropZoneSpec.coffee new file mode 100644 index 00000000000..869fc8865b1 --- /dev/null +++ b/spec/coffeescripts/react_files/components/UploadDropZoneSpec.coffee @@ -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()