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"
# Files attachments
gem "carrierwave"
# Drag and Drop UI
gem 'dropzonejs-rails'
# for aws storage
gem "fog", "~> 1.14", group: :aws
gem "unf", group: :aws
......
......@@ -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)
......@@ -579,6 +581,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
......
......@@ -29,6 +29,7 @@
#= require underscore
#= require nprogress
#= require nprogress-turbolinks
#= require dropzone
#= require_tree .
window.slugify = (text) ->
......
$ ->
$("body").on "click", ".js-toggler-target", ->
container = $(@).closest(".js-toggler-container")
container = $(".notes-container")
container.toggleClass("on")
# 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 @@
*= require_self
*= require nprogress
*= require nprogress-bootstrap
*= require dropzone/basic
*/
@import "main/*";
......
......@@ -5,10 +5,19 @@
.js-details-container.open .content { display: block; }
.js-details-container.open .content.hide { display: none; }
// 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.on .turn-on { display: none; }
.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 {
margin-bottom: 0;
}
.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 {
background: #f5f5f5;
border: 1px solid #ddd;
@include border-radius(4px);
min-height: 80px;
padding: 4px 6px;
> p {
overflow-x: auto;
}
}
.note_text {
border: 1px solid #DDD;
......@@ -310,7 +305,6 @@ ul.notes {
float: none;
}
.common-note-form {
margin: 0;
background: #F9F9F9;
......@@ -318,7 +312,6 @@ ul.notes {
border: 1px solid #DDD;
}
.note-form-actions {
background: #F9F9F9;
height: 45px;
......@@ -333,6 +326,18 @@ ul.notes {
.js-notify-commit-author {
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 {
......@@ -367,3 +372,8 @@ ul.notes {
.parallel-comment {
padding: 6px;
}
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
}
......@@ -14,4 +14,3 @@ class FilesController < ApplicationController
end
end
end
......@@ -69,7 +69,9 @@ class Projects::IssuesController < Projects::ApplicationController
render :new
end
end
format.js
format.js do |format|
@link = @issue.attachment.url.to_js
end
end
end
......
......@@ -162,8 +162,28 @@ class ProjectsController < ApplicationController
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
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
@title = 'New Project'
end
......
......@@ -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
......
# 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 @@
.container
.content= yield
= yield :embedded_scripts
\ No newline at end of file
......@@ -14,3 +14,4 @@
.container
.content= yield
= yield :embedded_scripts
\ No newline at end of file
......@@ -5,6 +5,7 @@
- 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
="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|
-if @issue.errors.any?
.alert.alert-danger
......@@ -19,8 +20,12 @@
.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'}.
= f.text_area :description, class: 'form-control js-gfm-input markdown-area', rows: 14
.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
.form-group
.issue-assignee
......@@ -57,9 +62,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 +96,5 @@
$('#issue_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
window.project_image_path_upload = "#{upload_image_project_path @project}";
......@@ -73,4 +73,4 @@
= label.name
&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 @@
.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 Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
= f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 14
.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
.form-group
.issue-assignee
......@@ -98,3 +102,5 @@
return false;
}
});
window.project_image_path_upload = "#{upload_image_project_path @project}";
......@@ -23,8 +23,12 @@
.form-group
.light
= f.label :description, "Description"
= f.text_area :description, class: "form-control js-gfm-input", rows: 10
%p.hint Description is parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
= f.text_area :description, class: "form-control js-gfm-input markdown-area", rows: 10
.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
.issue-assignee
= f.label :assignee_id do
......@@ -80,3 +84,5 @@
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
window.project_image_path_upload = "#{upload_image_project_path @project}";
......@@ -21,8 +21,12 @@
.form-group
= f.label :description, "Description", class: "control-label"
.col-sm-10
= f.text_area :description, maxlength: 2000, class: "form-control", rows: 10
%p.hint Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_markdown_path, target: '_blank'}.
= f.text_area :description, maxlength: 2000, class: "form-control markdown-area", rows: 10
.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
.form-group
= f.label :due_date, "Due Date", class: "control-label"
......@@ -45,3 +49,5 @@
dateFormat: "yy-mm-dd",
onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) }
}).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 @@
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
.note_text_and_preview.js-toggler-container
%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
= f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on'
.note_text_and_preview.js-toggler-container.notes-container
= f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on markdown-area'
.note_preview.js-note-preview.turn-off
.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
.error-alert
.note-form-actions
.buttons
......@@ -35,4 +30,14 @@
%span.file_name.js-attachment-filename File name...
= 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
:javascript
window.project_image_path_upload = "#{upload_image_project_path @project}";
......@@ -15,7 +15,6 @@
.col-sm-2
.col-sm-10
%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
%code [Link Title](page-slug)
\.
......@@ -23,8 +22,12 @@
.form-group
= f.label :content, class: 'control-label'
.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
= f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18
......@@ -36,3 +39,7 @@
- else
= f.submit 'Create page', class: "btn-create btn"
= link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel"
:javascript
window.project_image_path_upload = "#{upload_image_project_path @project}";
......@@ -136,8 +136,6 @@ Gitlab::Application.routes.draw do
match "/u/:username" => "users#show", as: :user, constraints: { username: /.*/ }, via: :get
#
# Dashboard Area
#
......@@ -178,6 +176,7 @@ Gitlab::Application.routes.draw do
post :fork
post :archive
post :unarchive
post :upload_image
get :autocomplete_sources
get :import
put :retry_import
......
......@@ -6,8 +6,7 @@ describe Projects::CommitsController do
before do
sign_in(user)
project.team << [user, :master]
project.creator = user
end
describe "GET show" do
......
require('spec_helper')
describe ProjectsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:png) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') }
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:gif) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
describe "POST #upload_image" do
before do
sign_in(user)
end
context "without params['markdown_img']" do
it "returns an error" do
post :upload_image, id: project.to_param
expect(response.status).to eq(404)
end
end