Commit fc861658 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge pull request #7011 from erbunao/drag-and-drop_markdown

Implements drag and drop upload of image in every markdown-area
parents 696b9903 6a85cdf1
......@@ -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
context "with invalid file" do
before do
post :upload_image, id: project.to_param, markdown_img: @img
end
it "returns an error" do
expect(response.status).to eq(404)
end
end
context "with valid file" do
before do
post :upload_image, id: project.to_param, markdown_img: @img
end
it "returns a content with original filename and new link." do
link = { alt: 'rails_sample', link: '' }.to_json
expect(response.body).to have_content link
end
end
end
end
\ No newline at end of file
......@@ -201,7 +201,7 @@ FactoryGirl.define do
end
trait :with_attachment do
attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") }
attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
end
end
......
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment