diff --git a/Gemfile b/Gemfile index 7ff7515143c7..e66f7e23f29d 100644 --- a/Gemfile +++ b/Gemfile @@ -69,6 +69,9 @@ gem "haml-rails" # Files attachments gem "carrierwave" +# Drag and Drop UI +gem 'dropzonejs-rails' + # for aws storage gem "fog", "~> 1.14", group: :aws gem "unf", group: :aws diff --git a/Gemfile.lock b/Gemfile.lock index f5f31105e184..90bf1cd1f36c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,6 +103,8 @@ GEM diffy (3.0.3) docile (1.1.1) dotenv (0.9.0) + dropzonejs-rails (0.4.14) + rails (> 3.1) email_spec (1.5.0) launchy (~> 2.1) mail (~> 2.2) @@ -577,6 +579,7 @@ DEPENDENCIES devise (= 3.0.4) devise-async (= 0.8.0) diffy (~> 3.0.3) + dropzonejs-rails email_spec email_validator (~> 1.4.0) enumerize diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index a22ff6dec319..72ce08b65501 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -29,6 +29,7 @@ #= require underscore #= require nprogress #= require nprogress-turbolinks +#= require dropzone #= require_tree . window.slugify = (text) -> diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c53873f95a2a..0b372a87a111 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -10,6 +10,7 @@ *= require_self *= require nprogress *= require nprogress-bootstrap + *= require dropzone/basic */ @import "main/*"; diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index 02c9123178f1..57d1618291d2 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -168,3 +168,58 @@ form.edit-issue { } } } + +#div-dropzone { + position: relative; + + #div-dropzone-hover { + position: absolute; + top: 50%; + left: 50%; + margin-top: -0.5em; + margin-left: -0.6em; + opacity: 0; + font-size: 50px; + transition: opacity 200ms ease-in-out; + } + + #div-dropzone-error { + position: absolute; + top: 50%; + width: 100%; + background-color: #e74c3c; + margin-top: -1em; + opacity: 0; + font-size: 20px; + text-align: center; + transition: opacity 200ms ease-in-out; + } + + #div-dropzone-spinner { + position: absolute; + top: 100%; + left: 100%; + margin-top: -1.1em; + margin-left: -1.1em; + opacity: 0; + font-size: 30px; + transition: opacity 200ms ease-in-out; + } +} + +#div-dropzone { + padding: 0; + border: 0; +} + +.div-dropzone-icon { + display: block; + text-align: center; + font-size: inherit; +} + +.div-dropzone-icon-error { + display: inline; + text-align: center; + font-size: inherit; +} diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index bf30de565ed0..d9a6adee6a8b 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -13,5 +13,4 @@ def download redirect_to uploader.url end end -end - +end \ No newline at end of file diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4e7a716bfe41..e1aedc3acf6f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,7 +60,6 @@ def show def create @issue = Issues::CreateService.new(project, current_user, params[:issue]).execute - respond_to do |format| format.html do if @issue.valid? @@ -69,7 +68,11 @@ def create render :new end end - format.js + format.js do |format| + # issue = Issues.create() + + @link = @issue.attachment.url.to_js + end end end @@ -77,7 +80,7 @@ def update @issue = Issues::UpdateService.new(project, current_user, params[:issue]).execute(issue) respond_to do |format| - format.js + format.js format.html do if @issue.valid? redirect_to [@project, @issue] @@ -93,6 +96,24 @@ def bulk_update redirect_to :back, notice: "#{result[:count]} issues updated" end + def upload_image + @main_dir = FileUploader.generate_dir + upload_path = File.join(project.namespace.path, @project.path, 'issues', @main_dir) + accepted_types = %w(png jpg jpeg gif) + uploader = FileUploader.new('uploads', upload_path, accepted_types) + links = [] + params['issue-imgs'].each do |img| + alt = uploader.store!(img) + links << { 'alt' => File.basename(alt, '.*'), + 'url' => File.join(root_url, uploader.url) } + end + + + respond_to do |format| + format.json { render json: { links: links } } + end + end + protected def issue @@ -128,7 +149,6 @@ def issues_filtered # def redirect_old issue = @project.issues.find_by(id: params[:id]) - if issue redirect_to project_issue_path(@project, issue) return diff --git a/app/models/issue.rb b/app/models/issue.rb index 16d51345e5a1..b4666edb19df 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -15,8 +15,12 @@ # milestone_id :integer # state :string(255) # iid :integer +# attachment :string(255) # +require 'carrierwave/orm/activerecord' +require 'file_size_validator' + class Issue < ActiveRecord::Base include Issuable include InternalId @@ -30,7 +34,8 @@ class Issue < ActiveRecord::Base scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } attr_accessible :title, :assignee_id, :position, :description, - :milestone_id, :label_list, :state_event + :milestone_id, :label_list, :author_id_of_changes, + :state_event acts_as_taggable_on :labels diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index d6137833bb9d..cbb9e4dfa09e 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -3,14 +3,12 @@ class CreateService < Issues::BaseService def execute issue = project.issues.new(params) issue.author = current_user - if issue.save notification_service.new_issue(issue, current_user) event_service.open_issue(issue, current_user) issue.create_cross_references!(issue.project, current_user) execute_hooks(issue) end - issue end end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index b122b6c8658c..9ed9754f03f8 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -33,4 +33,4 @@ def file_storage? def reset_events_cache(file) model.reset_events_cache if model.is_a?(User) end -end +end \ No newline at end of file diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb new file mode 100644 index 000000000000..0463af0b4d8c --- /dev/null +++ b/app/uploaders/file_uploader.rb @@ -0,0 +1,41 @@ +# encoding: utf-8 +class FileUploader < CarrierWave::Uploader::Base + storage :file + + def initialize(base_dir, path, allowed_extensions = nil) + @base_dir = base_dir + @path = path + @allowed_extensions = allowed_extensions + end + + def base_dir + @base_dir + end + + def store_dir + File.join(base_dir, @path) + end + + def cache_dir + File.join(base_dir, 'tmp', @path) + end + + def extension_white_list + @allowed_extensions + end + + def store!(file) + original_filename = file.original_filename + file.original_filename = self.class.generate_filename(file) + File.extname(original_filename) + super + original_filename + end + + def self.generate_filename(file) + Digest::MD5.hexdigest(File.basename(file.original_filename, '.*')) + end + + def self.generate_dir + SecureRandom.hex(5) + end +end diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 84703229fe6b..df885aebb7b7 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -19,8 +19,19 @@ .form-group = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = f.text_area :description, class: "form-control js-gfm-input", rows: 14 - %p.hint Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + #div-dropzone + = f.text_area :description, class: 'form-control js-gfm-input', rows: 14 + #div-dropzone-hover + %i.icon-picture.div-dropzone-icon + #div-dropzone-error + %i.icon-remove.div-dropzone-icon-error + %div#dropzone-error-message{style:'display:inline'} + Can't Attach File + #div-dropzone-spinner + %i.icon-spinner.icon-spin.div-dropzone-icon + %p.hint{style: "display: inline"} Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. + %p.hint.pull-right{style: "display: inline"} Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + %hr .form-group .issue-assignee @@ -46,8 +57,6 @@ = f.text_field :label_list, maxlength: 2000, class: "form-control" %p.hint Separate labels with commas. - - .form-actions - if @issue.new_record? = f.submit 'Submit new issue', class: "btn btn-create" @@ -57,9 +66,6 @@ - cancel_path = @issue.new_record? ? project_issues_path(@project) : project_issue_path(@project, @issue) = link_to "Cancel", cancel_path, class: 'btn btn-cancel' - - - :javascript $("#issue_label_list") .bind( "keydown", function( event ) { @@ -94,3 +100,73 @@ $('#issue_assignee_id').val("#{current_user.id}").trigger("change"); e.preventDefault(); }); + + function handlePaste(e) { + clipboardData = e.originalEvent.clipboardData; + for (var i = 0 ; i < clipboardData.items.length ; i++) { + var item = clipboardData.items[i]; + console.log(item); + if (item.type.indexOf("image") != -1) { + uploadFile(item.getAsFile()); + } + else { + console.log("Discarding non-image paste data"); + } + } + } + + function formatLinks(links) { + var md_links = []; + links.forEach(function (str) { + md_links.push('![' + str.alt + '](' + str.url + ')'); + }) + return md_links; + } + + $('#div-dropzone').dropzone({ + url: "#{upload_image_project_issues_path}", + dictDefaultMessage: "", + paramName: "issue-imgs", + maxFilesize: 10, + uploadMultiple: true, + acceptedFiles: "image/jpg,image/jpeg,image/gif,image/png", + headers: { "X-CSRF-Token": $('meta[name="csrf-token"]').attr('content')}, + + success: function(header,response){ + $("#issue_description").val($("#issue_description").val() + formatLinks(response.links).join("\n") + "\n"); + }, + + error: function(temp, errorMessage) { + $('#dropzone-error-message').text(errorMessage); + $('#div-dropzone-error').css('opacity', 0.7); + $('#div-dropzone-error').delay(500).fadeTo("slow", 0); + }, + + sending: function() { + $('#div-dropzone-spinner').css('opacity', 0.7); + }, + + complete: function() { + $('#div-dropzone-spinner').css('opacity',0); + } + }); + + $('#div-dropzone').on('dragenter',function(){ + $(this).children('textarea').focus(); + $('#div-dropzone-hover').css('opacity',0.7); + }); + + $('body').on('dragover',function(){ + $('#div-dropzone').children('textarea').blur(); + $('#div-dropzone-hover').css('opacity',0); + }); + + $('#div-dropzone').on('drop',function(){ + $('.dz-preview').remove(); + $('#div-dropzone-hover').css('opacity',0); + }); + + $('.markdown-selector').click(function() { + $('#div-dropzone').click(); + }); + diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index b1bc3ba0eba6..de8a34167ef1 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1 +1 @@ -= render "form" += render "form" \ No newline at end of file diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b6d3a8edf4da..fee5c165118e 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -49,6 +49,7 @@ .wiki = preserve do = markdown @issue.description + .context %cite.cgray = render partial: 'issue_context', locals: { issue: @issue } @@ -73,4 +74,4 @@ = label.name   -.voting_notes#notes= render "projects/notes/notes_with_form" +.voting_notes#notes= render "projects/notes/notes_with_form" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 7641fe430889..40d695176c1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,7 +67,6 @@ # Attachments serving # get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } - # # Admin Area # @@ -136,8 +135,6 @@ match "/u/:username" => "users#show", as: :user, constraints: { username: /.*/ }, via: :get - - # # Dashboard Area # @@ -303,6 +300,7 @@ resources :issues, constraints: {id: /\d+/}, except: [:destroy] do collection do post :bulk_update + post :upload_image end end diff --git a/db/schema.rb b/db/schema.rb index 93837337afc3..4d3a6b821947 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -87,6 +87,7 @@ t.integer "milestone_id" t.string "state" t.integer "iid" + t.string "attachment" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree diff --git a/features/support/env.rb b/features/support/env.rb deleted file mode 100644 index a5b297775db5..000000000000 --- a/features/support/env.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'simplecov' unless ENV['CI'] - -if ENV['TRAVIS'] - require 'coveralls' - Coveralls.wear! -end - -ENV['RAILS_ENV'] = 'test' -require './config/environment' - -require 'rspec' -require 'rspec/expectations' -require 'database_cleaner' -require 'spinach/capybara' -require 'sidekiq/testing/inline' - - -%w(valid_commit valid_commit_with_alt_email big_commits select2_helper test_env).each do |f| - require Rails.root.join('spec', 'support', f) -end - -Dir["#{Rails.root}/features/steps/shared/*.rb"].each {|file| require file} - -WebMock.allow_net_connect! -# -# JS driver -# -require 'capybara/poltergeist' -Capybara.javascript_driver = :poltergeist -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, :js_errors => false, :timeout => 60) -end -Spinach.hooks.on_tag("javascript") do - ::Capybara.current_driver = ::Capybara.javascript_driver -end -Capybara.default_wait_time = 60 -Capybara.ignore_hidden_elements = false - -DatabaseCleaner.strategy = :truncation - -Spinach.hooks.before_scenario do - TestEnv.setup_stubs - DatabaseCleaner.start -end - -Spinach.hooks.after_scenario do - DatabaseCleaner.clean -end - -Spinach.hooks.before_run do - TestEnv.init(mailer: false, init_repos: true, repos: false) - RSpec::Mocks::setup self - - include FactoryGirl::Syntax::Methods -end diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index bfb74960c480..000000000000 Binary files a/public/favicon.ico and /dev/null differ