From e2ece2bc350bf881e9716e51c01a59eecac65fc9 Mon Sep 17 00:00:00 2001
From: liyakun <liyakun127@gmail.com>
Date: Thu, 10 Sep 2015 16:18:40 +0200
Subject: [PATCH]     Add "Replace" and "Upload" features

    Refactor upload and replace functionality

    Rename file and move CSS

    Fix typo

    Make dropzone a div

    Remove unnecessary file

    Change color of "upload existing one"

    Add missing changes
---
 CHANGELOG                                     |  1 +
 .../blob/blob_file_dropzone.js.coffee         | 52 ++++++++++++
 app/assets/stylesheets/pages/tree.scss        | 12 +++
 app/controllers/projects/blob_controller.rb   | 28 ++++++-
 app/services/files/create_service.rb          |  4 +-
 app/views/projects/blob/_actions.html.haml    |  6 +-
 app/views/projects/blob/_replace.html.haml    | 28 +++++++
 app/views/projects/blob/_upload.html.haml     | 28 +++++++
 app/views/projects/blob/new.html.haml         | 10 ++-
 app/views/projects/blob/show.html.haml        |  1 +
 config/routes.rb                              | 10 +++
 features/project/source/browse_files.feature  | 23 +++++
 features/steps/project/source/browse_files.rb | 83 ++++++++++++++++++-
 13 files changed, 274 insertions(+), 12 deletions(-)
 create mode 100644 app/assets/javascripts/blob/blob_file_dropzone.js.coffee
 create mode 100644 app/views/projects/blob/_replace.html.haml
 create mode 100644 app/views/projects/blob/_upload.html.haml

diff --git a/CHANGELOG b/CHANGELOG
index b41237f8679..2b1edf54c42 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date.
 
 v 8.0.0 (unreleased)
   - Gracefully handle errors in syntax highlighting by leaving the block unformatted (Stan Hu)
+  - Add "replace" and "upload" functionalities to allow user replace existing file and upload new file into current repository
   - Fix URL construction for merge requests, issues, notes, and commits for relative URL config (Stan Hu)
   - Fix emoji URLs in Markdown when relative_url_root is used (Stan Hu)
   - Omit filename in Content-Disposition header in raw file download to avoid RFC 6266 encoding issues (Stan HU)
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
new file mode 100644
index 00000000000..090af9bb376
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
@@ -0,0 +1,52 @@
+class @BlobFileDropzone
+  constructor: (form, method) ->
+    form_dropzone = form.find('.dropzone')
+    Dropzone.autoDiscover = false
+    dropzone = form_dropzone.dropzone(
+      autoDiscover: false
+      autoProcessQueue: false
+      url: form.attr('action')
+      # Rails uses a hidden input field for PUT
+      # http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
+      method: method
+      clickable: true
+      uploadMultiple: false
+      paramName: "file"
+      maxFilesize: gon.max_file_size or 10
+      parallelUploads: 1
+      maxFiles: 1
+      addRemoveLinks: true
+      previewsContainer: '.dropzone-previews'
+      headers:
+        "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+
+      success: (header, response) ->
+        window.location.href = response.filePath
+        return
+
+      error: (temp, errorMessage) ->
+        stripped = $("<div/>").html(errorMessage).text();
+        $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show()
+        return
+
+      maxfilesexceeded: (file) ->
+        @removeFile file
+        return
+
+      removedfile: (file) ->
+        $('.dropzone-previews')[0].removeChild(file.previewTemplate)
+        $('.dropzone-alerts').html('').hide()
+        return true
+
+      sending: (file, xhr, formData) ->
+        formData.append('commit_message', form.find('#commit_message').val())
+        return
+    )
+
+    submitButton = form.find('#submit-all')[0]
+    submitButton.addEventListener 'click', (e) ->
+      e.preventDefault()
+      e.stopPropagation()
+      alert "Please select a file" if dropzone[0].dropzone.getQueuedFiles().length == 0
+      dropzone[0].dropzone.processQueue()
+      return false
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 71ca37c0cd7..df7fab07a57 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -116,3 +116,15 @@
 }
 
 #modal-remove-blob > .modal-dialog { width: 850px; }
+
+.blob-upload-dropzone-previews {
+  text-align: center;
+  border: 2px;
+  border-style: dashed;
+  min-height: 200px;
+}
+
+.upload-link {
+  font-weight: normal;
+  color: #0000EE;
+}
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 100d3d3b317..8776721d243 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -26,10 +26,16 @@ class Projects::BlobController < Projects::ApplicationController
 
     if result[:status] == :success
       flash[:notice] = "Your changes have been successfully committed"
-      redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path))
+      respond_to do |format|
+        format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }
+        format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } }
+      end
     else
       flash[:alert] = result[:message]
-      render :new
+      respond_to do |format|
+        format.html { render :new }
+        format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
+      end
     end
   end
 
@@ -45,10 +51,16 @@ class Projects::BlobController < Projects::ApplicationController
 
     if result[:status] == :success
       flash[:notice] = "Your changes have been successfully committed"
-      redirect_to after_edit_path
+      respond_to do |format|
+        format.html { redirect_to after_edit_path }
+        format.json { render json: { message: "success", filePath: after_edit_path } }
+      end
     else
       flash[:alert] = result[:message]
-      render :edit
+      respond_to do |format|
+        format.html { render :edit }
+        format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
+      end
     end
   end
 
@@ -146,11 +158,19 @@ class Projects::BlobController < Projects::ApplicationController
 
     @file_path =
       if action_name.to_s == 'create'
+        if params[:file].present?
+          params[:file_name] = params[:file].original_filename
+        end
         File.join(@path, File.basename(params[:file_name]))
       else
         @path
       end
 
+    if params[:file].present?
+      params[:content] = Base64.encode64(params[:file].read)
+      params[:encoding] = 'base64'
+    end
+
     @commit_params = {
       file_path: @file_path,
       current_branch: @current_branch,
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 91d715b2d63..ffbb5993279 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -19,10 +19,12 @@ module Files
       end
 
       unless project.empty_repo?
+        @file_path.slice!(0) if @file_path.start_with?('/')
+
         blob = repository.blob_at_branch(@current_branch, @file_path)
 
         if blob
-          raise_error("Your changes could not be committed, because file with such name exists")
+          raise_error("Your changes could not be committed because a file with the same name already exists")
         end
       end
     end
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index 13f8271b979..5b61846fe6d 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -17,6 +17,6 @@
         tree_join(@commit.sha, @path)), class: 'btn btn-sm'
 
 - if allowed_tree_edit?
-  = button_tag class: 'remove-blob btn btn-sm btn-remove',
-      'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do
-    Remove
+  .btn-group{:role => "group"}
+    %button.btn.btn-default{class: 'btn-primary', href: '#modal-replace-blob', 'data-target' => '#modal-replace-blob', 'data-toggle' => 'modal'} Replace
+    %button.btn.btn-default{class: 'btn-remove', href: '#modal-remove-blob', 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal'} Remove
diff --git a/app/views/projects/blob/_replace.html.haml b/app/views/projects/blob/_replace.html.haml
new file mode 100644
index 00000000000..84abf0303d0
--- /dev/null
+++ b/app/views/projects/blob/_replace.html.haml
@@ -0,0 +1,28 @@
+#modal-replace-blob.modal
+  .modal-dialog
+    .modal-content
+      .modal-header
+        %a.close{href: "#", "data-dismiss" => "modal"} ×
+        %h3.page-title Replace #{@blob.name}
+        %p.light
+          From branch
+          %strong= @ref
+      .modal-body
+        = form_tag namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'blob-file-upload-form-js form-horizontal' do
+          .dropzone
+            .dropzone-previews{class: "blob-upload-dropzone-previews"}
+              %p.dz-message{class: "hint"}<
+                Attach files by dragging & dropping or&nbsp;
+                %a{href: '#', class: "markdown-selector"}>click to upload
+          %br
+          .dropzone-alerts{class: "alert alert-danger data", "data-dismiss" => "alert", style: "display:none"}
+          = render 'shared/commit_message_container', params: params,
+            placeholder: 'Replace this file because...'
+          .form-group
+            .col-sm-offset-2.col-sm-10
+              = button_tag 'Replace file', class: 'btn btn-small btn-primary btn-replace-file', id: 'submit-all'
+              = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:coffeescript
+  disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-replace-file'
+  new BlobFileDropzone($('.blob-file-upload-form-js'), 'put')
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
new file mode 100644
index 00000000000..5a6a3358a17
--- /dev/null
+++ b/app/views/projects/blob/_upload.html.haml
@@ -0,0 +1,28 @@
+#modal-upload-blob.modal
+  .modal-dialog
+    .modal-content
+      .modal-header
+        %a.close{href: "#", "data-dismiss" => "modal"} ×
+        %h3.page-title Upload
+        %p.light
+          From branch
+          %strong= @ref
+      .modal-body
+        = form_tag namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'blob-file-upload-form-js form-horizontal' do
+          .dropzone
+            .dropzone-previews{class: "blob-upload-dropzone-previews"}
+              %p.dz-message{class: "hint"}<
+                Attach files by dragging & dropping or&nbsp;
+                %a{href: '#', class: "markdown-selector"}>click to upload
+          %br
+          .dropzone-alerts{class: "alert alert-danger data", "data-dismiss" => "alert", style: "display:none"}
+          = render 'shared/commit_message_container', params: params,
+            placeholder: 'Upload this file because...'
+          .form-group
+            .col-sm-offset-2.col-sm-10
+              = button_tag 'Upload file', class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all'
+              = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:coffeescript
+  disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'
+  new BlobFileDropzone($('.blob-file-upload-form-js'), 'post')
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 7c2a4fece94..6fb46ea2040 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,5 +1,11 @@
-- page_title "New File", @ref
-%h3.page-title New file
+%h3.page-title<
+  Create new file or&nbsp;
+  %a.upload-link{href: '#modal-upload-blob', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'}>upload existing one
+
+.file-title
+  = render 'projects/blob/upload'
+  %br
+
 .file-editor
   = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do
     = render 'projects/blob/editor', ref: @ref
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index bd2fc43633c..19e876ec34c 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -10,3 +10,4 @@
 
 - if allowed_tree_edit?
   = render 'projects/blob/remove'
+  = render 'projects/blob/replace'
diff --git a/config/routes.rb b/config/routes.rb
index 011af4825fa..ab1d8f7ce92 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -356,6 +356,16 @@ Gitlab::Application.routes.draw do
             to: 'blob#destroy',
             constraints: { id: /.+/, format: false }
           )
+          put(
+            '/blob/*id',
+            to: 'blob#update',
+            constraints: { id: /.+/, format: false }
+          )
+          post(
+            '/blob/*id',
+            to: 'blob#create',
+            constraints: { id: /.+/, format: false }
+          )
         end
 
         scope do
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index d3a77466a35..b5b6abe6aff 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -33,6 +33,29 @@ Feature: Project Source Browse Files
     And I click on "Commit Changes"
     Then I am redirected to the new file
     And I should see its new content
+    
+  @javascript
+  Scenario: I can upload file and commit
+    Given I click on "new file" link in repo
+    Then I can see new file page
+    And I can see "upload existing one"
+    And I click on "upload existing one"
+    And I upload a new text file
+    And I fill the upload file commit message
+    And I click on "Upload file"
+    Then I can see the new text file
+    And I can see the new commit message
+
+  @javascript
+  Scenario: I can replace file and commit
+    Given I click on ".gitignore" file in repo
+    And I see the ".gitignore"
+    And I click on "Replace"
+    And I replace it with a text file
+    And I fill the replace file commit message
+    And I click on "Replace file"
+    Then I can see the new text file
+    And I can see the replacement commit message
 
   @javascript
   Scenario: I can create and commit file and specify new branch
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 5cb085db207..7a0ee4df45e 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
 class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   include SharedAuthentication
   include SharedProject
@@ -78,7 +79,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I fill the commit message' do
-    fill_in :commit_message, with: 'Not yet a commit message.'
+    fill_in :commit_message, with: 'Not yet a commit message.', visible: true
   end
 
   step 'I click link "Diff"' do
@@ -97,6 +98,14 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
     click_button 'Remove file'
   end
 
+  step 'I click on "Replace"' do
+    click_button  "Replace"
+  end
+
+  step 'I click on "Replace file"' do
+    click_button  'Replace file'
+  end
+
   step 'I see diff' do
     expect(page).to have_css '.line_holder.new'
   end
@@ -106,10 +115,55 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   end
 
   step 'I can see new file page' do
-    expect(page).to have_content "New file"
+    expect(page).to have_content "new file"
     expect(page).to have_content "Commit message"
   end
 
+  step 'I can see "upload existing one"' do
+    expect(page).to have_content "upload existing one"
+  end
+
+  step 'I click on "upload existing one"' do
+    click_link 'upload existing one'
+  end
+
+  step 'I click on "Upload file"' do
+    click_button 'Upload file'
+  end
+
+  step 'I can see the new commit message' do
+    expect(page).to have_content "New upload commit message"
+  end
+
+  step 'I upload a new text file' do
+    drop_in_dropzone test_text_file
+  end
+
+  step 'I fill the upload file commit message' do
+    page.within('#modal-upload-blob') do
+      fill_in :commit_message, with: 'New upload commit message'
+    end
+  end
+
+  step 'I replace it with a text file' do
+    drop_in_dropzone test_text_file
+  end
+
+  step 'I fill the replace file commit message' do
+    page.within('#modal-replace-blob') do
+      fill_in :commit_message, with: 'Replacement file commit message'
+    end
+  end
+
+  step 'I can see the replacement commit message' do
+    expect(page).to have_content "Replacement file commit message"
+  end
+
+  step 'I can see the new text file' do
+    expect(page).to have_content "Lorem ipsum dolor sit amet"
+    expect(page).to have_content "Sed ut perspiciatis unde omnis"
+  end
+
   step 'I click on files directory' do
     click_link 'files'
   end
@@ -232,4 +286,29 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
   def new_file_name
     'not_a_file.md'
   end
+
+  def drop_in_dropzone(file_path)
+    # Generate a fake input selector
+    page.execute_script <<-JS
+      var fakeFileInput = window.$('<input/>').attr(
+        {id: 'fakeFileInput', type: 'file'}
+      ).appendTo('body');
+    JS
+    # Attach the file to the fake input selector with Capybara
+    attach_file("fakeFileInput", file_path)
+    # Add the file to a fileList array and trigger the fake drop event
+    page.execute_script <<-JS
+      var fileList = [$('#fakeFileInput')[0].files[0]];
+      var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
+      $('.dropzone')[0].dropzone.listeners[0].events.drop(e);
+    JS
+  end
+
+  def test_text_file
+    File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')
+  end
+
+  def test_image_file
+    File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
+  end
 end
-- 
GitLab