Commit 6a85cdf1 authored by Earle Bunao & Neil Calabroso's avatar Earle Bunao & Neil Calabroso Committed by erbunao

Implements drag and drop upload in creating issues

parent 696b9903
...@@ -69,6 +69,9 @@ gem "haml-rails" ...@@ -69,6 +69,9 @@ gem "haml-rails"
# Files attachments # Files attachments
gem "carrierwave" gem "carrierwave"
# Drag and Drop UI
gem 'dropzonejs-rails'
# for aws storage # for aws storage
gem "fog", "~> 1.14", group: :aws gem "fog", "~> 1.14", group: :aws
gem "unf", group: :aws gem "unf", group: :aws
......
...@@ -103,6 +103,8 @@ GEM ...@@ -103,6 +103,8 @@ GEM
diffy (3.0.3) diffy (3.0.3)
docile (1.1.1) docile (1.1.1)
dotenv (0.9.0) dotenv (0.9.0)
dropzonejs-rails (0.4.14)
rails (> 3.1)
email_spec (1.5.0) email_spec (1.5.0)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.2) mail (~> 2.2)
...@@ -579,6 +581,7 @@ DEPENDENCIES ...@@ -579,6 +581,7 @@ DEPENDENCIES
devise (= 3.0.4) devise (= 3.0.4)
devise-async (= 0.8.0) devise-async (= 0.8.0)
diffy (~> 3.0.3) diffy (~> 3.0.3)
dropzonejs-rails
email_spec email_spec
email_validator (~> 1.4.0) email_validator (~> 1.4.0)
enumerize enumerize
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
#= require underscore #= require underscore
#= require nprogress #= require nprogress
#= require nprogress-turbolinks #= require nprogress-turbolinks
#= require dropzone
#= require_tree . #= require_tree .
window.slugify = (text) -> window.slugify = (text) ->
......
$ -> $ ->
$("body").on "click", ".js-toggler-target", -> $("body").on "click", ".js-toggler-target", ->
container = $(@).closest(".js-toggler-container") container = $(".notes-container")
container.toggleClass("on") container.toggleClass("on")
# Toggle button. Show/hide content inside parent container. # Toggle button. Show/hide content inside parent container.
......
formatLink = (str) ->
"![" + str.alt + "](" + str.url + ")"
$(document).ready ->
alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"
alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""
divHover = "<div class=\"div-dropzone-hover\"></div>"
divSpinner = "<div class=\"div-dropzone-spinner\"></div>"
divAlert = "<div class=\"" + alertClass + "\"></div>"
iconPicture = "<i class=\"icon-picture div-dropzone-icon\"></i>"
iconSpinner = "<i class=\"icon-spinner icon-spin div-dropzone-icon\"></i>"
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"
project_image_path_upload = window.project_image_path_upload or null
$("textarea.markdown-area").wrap "<div class=\"div-dropzone\"></div>"
$(".div-dropzone").parent().addClass "div-dropzone-wrapper"
$(".div-dropzone").append divHover
$(".div-dropzone-hover").append iconPicture
$(".div-dropzone").append divSpinner
$(".div-dropzone-spinner").append iconSpinner
dropzone = $(".div-dropzone").dropzone(
url: project_image_path_upload
dictDefaultMessage: ""
clickable: true
paramName: "markdown_img"
maxFilesize: 10
uploadMultiple: false
acceptedFiles: "image/jpg,image/jpeg,image/gif,image/png"
headers:
"X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
previewContainer: false
processing: ->
$(".div-dropzone-alert").alert "close"
dragover: ->
$(".div-dropzone > textarea").addClass "div-dropzone-focus"
$(".div-dropzone-hover").css "opacity", 0.7
return
dragleave: ->
$(".div-dropzone > textarea").removeClass "div-dropzone-focus"
$(".div-dropzone-hover").css "opacity", 0
return
drop: ->
$(".div-dropzone > textarea").removeClass "div-dropzone-focus"
$(".div-dropzone-hover").css "opacity", 0
$(".div-dropzone > textarea").focus()
return
success: (header, response) ->
child = $(dropzone[0]).children("textarea")
$(child).val $(child).val() + formatLink(response.link) + "\n"
return
error: (temp, errorMessage) ->
checkIfMsgExists = $(".error-alert").children().length
if checkIfMsgExists is 0
$(".error-alert").append divAlert
$(".div-dropzone-alert").append btnAlert + errorMessage
return
sending: ->
$(".div-dropzone-spinner").css "opacity", 0.7
return
complete: ->
$(".dz-preview").remove()
$(".markdown-area").trigger "input"
$(".div-dropzone-spinner").css "opacity", 0
return
)
$(".markdown-selector").click (e) ->
e.preventDefault()
$(".div-dropzone").click()
return
return
\ No newline at end of file
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
*= require_self *= require_self
*= require nprogress *= require nprogress
*= require nprogress-bootstrap *= require nprogress-bootstrap
*= require dropzone/basic
*/ */
@import "main/*"; @import "main/*";
......
...@@ -5,10 +5,19 @@ ...@@ -5,10 +5,19 @@
.js-details-container.open .content { display: block; } .js-details-container.open .content { display: block; }
.js-details-container.open .content.hide { display: none; } .js-details-container.open .content.hide { display: none; }
// Toggler // Toggler
//-------- //--------
.js-toggler-container .turn-on { display: inherit; } .write-preview-btn .turn-on { display: inherit; }
.write-preview-btn .turn-off { display: none; }
.js-toggler-container .turn-off { display: none; } .js-toggler-container .turn-off { display: none; }
.js-toggler-container.on .turn-on { display: none; } .js-toggler-container.on .turn-on { display: none; }
.js-toggler-container.on .turn-off { display: inherit; } .js-toggler-container.on .turn-off { display: inherit; }
.js-toggler-container.on ~ .note-form-actions {
.write-preview-btn .turn-on { display: none; }
}
.js-toggler-container.on ~ .note-form-actions {
.write-preview-btn .turn-off { display: inherit; }
}
.div-dropzone-wrapper {
.div-dropzone {
position: relative;
padding: 0;
border: 0;
margin-bottom: 5px;
.div-dropzone-focus {
border-color: #66afe9 !important;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6) !important;
outline: 0 !important;
}
.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-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-icon {
display: block;
text-align: center;
font-size: inherit;
}
.dz-preview {
display: none;
}
}
.hint {
float: left;
padding: 0;
margin: 0;
}
}
.div-dropzone-alert {
margin-top: 5px;
margin-bottom: 0;
transition: opacity 200ms ease-in-out;
}
...@@ -272,21 +272,16 @@ ul.notes { ...@@ -272,21 +272,16 @@ ul.notes {
margin-bottom: 0; margin-bottom: 0;
} }
.note_text_and_preview { .note_text_and_preview {
// makes the "absolute" position for links relative to this
position: relative;
// preview/edit buttons
> a {
position: absolute;
right: 5px;
bottom: -60px;
}
.note_preview { .note_preview {
background: #f5f5f5; background: #f5f5f5;
border: 1px solid #ddd; border: 1px solid #ddd;
@include border-radius(4px); @include border-radius(4px);
min-height: 80px; min-height: 80px;
padding: 4px 6px; padding: 4px 6px;
> p {
overflow-x: auto;
}
} }
.note_text { .note_text {
border: 1px solid #DDD; border: 1px solid #DDD;
...@@ -310,7 +305,6 @@ ul.notes { ...@@ -310,7 +305,6 @@ ul.notes {
float: none; float: none;
} }
.common-note-form { .common-note-form {
margin: 0; margin: 0;
background: #F9F9F9; background: #F9F9F9;
...@@ -318,7 +312,6 @@ ul.notes { ...@@ -318,7 +312,6 @@ ul.notes {
border: 1px solid #DDD; border: 1px solid #DDD;
} }
.note-form-actions { .note-form-actions {
background: #F9F9F9; background: #F9F9F9;
height: 45px; height: 45px;
...@@ -333,6 +326,18 @@ ul.notes { ...@@ -333,6 +326,18 @@ ul.notes {
.js-notify-commit-author { .js-notify-commit-author {
float: left; float: left;
} }
.write-preview-btn {
// makes the "absolute" position for links relative to this
position: relative;
// preview/edit buttons
> a {
position: absolute;
right: 5px;
top: 8px;
}
}
} }
.note-edit-form { .note-edit-form {
...@@ -367,3 +372,8 @@ ul.notes { ...@@ -367,3 +372,8 @@ ul.notes {
.parallel-comment { .parallel-comment {
padding: 6px; padding: 6px;
} }
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
}
...@@ -14,4 +14,3 @@ class FilesController < ApplicationController ...@@ -14,4 +14,3 @@ class FilesController < ApplicationController
end end
end end
end end
...@@ -69,7 +69,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -69,7 +69,9 @@ class Projects::IssuesController < Projects::ApplicationController
render :new render :new
end end
end end
format.js format.js do |format|
@link = @issue.attachment.url.to_js
end
end end
end end
......
...@@ -162,8 +162,28 @@ class ProjectsController < ApplicationController ...@@ -162,8 +162,28 @@ class ProjectsController < ApplicationController
end end
end end
def upload_image
uploader = FileUploader.new('uploads', upload_path, accepted_images)
alt = params['markdown_img'].original_filename
uploader.store!(params['markdown_img'])
link = { 'alt' => File.basename(alt, '.*'),
'url' => File.join(root_url, uploader.url) }
respond_to do |format|
format.json { render json: { link: link } }
end
end
private private
def upload_path
base_dir = FileUploader.generate_dir
File.join(repository.path_with_namespace, base_dir)
end
def accepted_images
%w(png jpg jpeg gif)
end
def set_title def set_title
@title = 'New Project' @title = 'New Project'
end end
......
...@@ -15,8 +15,12 @@ ...@@ -15,8 +15,12 @@
# milestone_id :integer # milestone_id :integer
# state :string(255) # state :string(255)
# iid :integer # iid :integer
# attachment :string(255)
# #
require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Issue < ActiveRecord::Base class Issue < ActiveRecord::Base
include Issuable include Issuable
include InternalId include InternalId
......
# 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)
file.original_filename = self.class.generate_filename(file)
super
end
def self.generate_filename(file)
original_filename = File.basename(file.original_filename, '.*')
extension = File.extname(file.original_filename)
new_filename = Digest::MD5.hexdigest(original_filename) + extension
end
def self.generate_dir
SecureRandom.hex(5)
end
end
...@@ -10,3 +10,4 @@ ...@@ -10,3 +10,4 @@
.container .container
.content= yield .content= yield
= yield :embedded_scripts
\ No newline at end of file
...@@ -14,3 +14,4 @@ ...@@ -14,3 +14,4 @@
.container .container
.content= yield .content= yield
= yield :embedded_scripts
\ No newline at end of file
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
- contribution_guide_url = project_blob_path(@project, tree_join(@repository.root_ref, @repository.contribution_guide.name)) - contribution_guide_url = project_blob_path(@project, tree_join(@repository.root_ref, @repository.contribution_guide.name))
.alert.alert-info.col-sm-10.col-sm-offset-2 .alert.alert-info.col-sm-10.col-sm-offset-2
="Please review the <strong>#{link_to "guidelines for contribution", contribution_guide_url}</strong> to this repository.".html_safe ="Please review the <strong>#{link_to "guidelines for contribution", contribution_guide_url}</strong> to this repository.".html_safe
= form_for [@project, @issue], html: { class: 'form-horizontal issue-form' } do |f| = form_for [@project, @issue], html: { class: 'form-horizontal issue-form' } do |f|
-if @issue.errors.any? -if @issue.errors.any?
.alert.alert-danger .alert.alert-danger
...@@ -19,8 +20,12 @@ ...@@ -19,8 +20,12 @@
.form-group .form-group
= f.label :description, 'Description', class: 'control-label' = f.label :description, 'Description', class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_area :description, class: "form-control js-gfm-input", rows: 14 = f.text_area :description, class: 'form-control js-gfm-input markdown-area', rows: 14
%p.hint Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. .col-sm-12.hint
.pull-left Issues are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
%hr %hr
.form-group .form-group
.issue-assignee .issue-assignee
...@@ -57,9 +62,6 @@ ...@@ -57,9 +62,6 @@
- cancel_path = @issue.new_record? ? project_issues_path(@project) : project_issue_path(@project, @issue) - cancel_path = @issue.new_record? ? project_issues_path(@project) : project_issue_path(@project, @issue)
= link_to "Cancel", cancel_path, class: 'btn btn-cancel' = link_to "Cancel", cancel_path, class: 'btn btn-cancel'
:javascript :javascript
$("#issue_label_list") $("#issue_label_list")
.bind( "keydown", function( event ) { .bind( "keydown", function( event ) {
...@@ -94,3 +96,5 @@ ...@@ -94,3 +96,5 @@
$('#issue_assignee_id').val("#{current_user.id}").trigger("change"); $('#issue_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault(); e.preventDefault();
}); });
window.project_image_path_upload = "#{upload_image_project_path @project}";
...@@ -73,4 +73,4 @@ ...@@ -73,4 +73,4 @@
= label.name = label.name
&nbsp; &nbsp;
.voting_notes#notes= render "projects/notes/notes_with_form" .voting_notes#notes= render "projects/notes/notes_with_form"
\ No newline at end of file
...@@ -22,8 +22,12 @@ ...@@ -22,8 +22,12 @@
.form-group .form-group
= f.label :description, "Description", class: 'control-label' = f.label :description, "Description", class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_area :description, class: "form-control js-gfm-input", rows: 14 = f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 14
%p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. .col-sm-12.hint
.pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
%hr %hr
.form-group .form-group
.issue-assignee .issue-assignee
...@@ -98,3 +102,5 @@ ...@@ -98,3 +102,5 @@
return false; return false;
} }
}); });
window.project_image_path_upload = "#{upload_image_project_path @project}";
...@@ -23,8 +23,12 @@ ...@@ -23,8 +23,12 @@
.form-group .form-group
.light .light
= f.label :description, "Description" = f.label :description, "Description"
= f.text_area :description, class: "form-control js-gfm-input", rows: 10 = f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 10
%p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. .col-sm-12.hint
.pull-left Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
.form-group .form-group
.issue-assignee .issue-assignee
= f.label :assignee_id do = f.label :assignee_id do
...@@ -80,3 +84,5 @@ ...@@ -80,3 +84,5 @@
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change"); $('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault(); e.preventDefault();
}); });
window.project_image_path_upload = "#{upload_image_project_path @project}";
...@@ -21,8 +21,12 @@ ...@@ -21,8 +21,12 @@
.form-group .form-group
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= f.text_area :description, maxlength: 2000, class: "form-control", rows: 10 = f.text_area :description, maxlength: 2000, class: "form-control markdown-area", rows: 10
%p.hint Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. .hint
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
.col-md-6 .col-md-6
.form-group .form-group
= f.label :due_date, "Due Date", class: "control-label" = f.label :due_date, "Due Date", class: "control-label"
...@@ -45,3 +49,5 @@ ...@@ -45,3 +49,5 @@
dateFormat: "yy-mm-dd", dateFormat: "yy-mm-dd",
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val()));
window.project_image_path_upload = "#{upload_image_project_path @project}";
...@@ -5,20 +5,15 @@ ...@@ -5,20 +5,15 @@
= f.hidden_field :noteable_id = f.hidden_field :noteable_id
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
.note_text_and_preview.js-toggler-container .note_text_and_preview.js-toggler-container.notes-container
%a.btn.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", data: {url: preview_project_notes_path(@project)} } = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on markdown-area'
%i.icon-eye-open
Preview
%a.btn.btn-primary.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;" }
%i.icon-edit
Write
= f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on'
.note_preview.js-note-preview.turn-off .note_preview.js-note-preview.turn-off
.hint .hint
.pull-right Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}. .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix .clearfix
.error-alert
.note-form-actions .note-form-actions
.buttons .buttons
...@@ -35,4 +30,14 @@ ...@@ -35,4 +30,14 @@
%span.file_name.js-attachment-filename File name... %span.file_name.js-attachment-filename File name...
= f.file_field :attachment, class: "js-note-attachment-input hidden" = f.file_field :attachment, class: "js-note-attachment-input hidden"
.write-preview-btn
%a.btn.js-note-preview-button.js-toggler-target.turn-off{ href: "javascript:;", data: {url: preview_project_notes_path(@project)} }
%i.icon-eye-open
Preview
%a.btn.btn-primary.js-note-edit-button.js-toggler-target.turn-off{ href: "javascript:;" }
%i.icon-edit
Write
.clearfix .clearfix
:javascript
window.project_image_path_upload = "#{upload_image_project_path @project}";
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
.col-sm-2 .col-sm-2
.col-sm-10 .col-sm-10
%p.cgray %p.cgray
Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
To link to a (new) page you can just type To link to a (new) page you can just type
%code [Link Title](page-slug) %code [Link Title](page-slug)
\. \.
...@@ -23,8 +22,12 @@ ...@@ -23,8 +22,12 @@
.form-group .form-group
= f.label :content, class: 'control-label' = f.label :content, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_area :content, class: 'form-control js-gfm-input', rows: 18 = f.text_area :content, class: 'form-control js-gfm-input markdown-area', rows: 18
.col-sm-12.hint
.pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
.pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
.clearfix
.error-alert
.form-group .form-group
= f.label :commit_message, class: 'control-label' = f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18 .col-sm-10= f.text_field :message, class: 'form-control', rows: 18
...@@ -36,3 +39,7 @@ ...@@ -36,3 +39,7 @@
- else - else