Commit ee890f2b2a66be925746f2238dc462a74b0fd219

Authored by Dmitriy Zaporozhets
2 parents f49fb5dc 7588186e

Merge branch 'master' into 6-0-dev

Conflicts:
	app/views/dashboard/projects.html.haml
	app/views/layouts/_head_panel.html.haml
	config/routes.rb
app/assets/javascripts/notes.js
@@ -56,6 +56,26 @@ var NoteList = { @@ -56,6 +56,26 @@ var NoteList = {
56 ".js-note-delete", 56 ".js-note-delete",
57 NoteList.removeNote); 57 NoteList.removeNote);
58 58
  59 + // show the edit note form
  60 + $(document).on("click",
  61 + ".js-note-edit",
  62 + NoteList.showEditNoteForm);
  63 +
  64 + // cancel note editing
  65 + $(document).on("click",
  66 + ".note-edit-cancel",
  67 + NoteList.cancelNoteEdit);
  68 +
  69 + // delete note attachment
  70 + $(document).on("click",
  71 + ".js-note-attachment-delete",
  72 + NoteList.deleteNoteAttachment);
  73 +
  74 + // update the note after editing
  75 + $(document).on("ajax:complete",
  76 + "form.edit_note",
  77 + NoteList.updateNote);
  78 +
59 // reset main target form after submit 79 // reset main target form after submit
60 $(document).on("ajax:complete", 80 $(document).on("ajax:complete",
61 ".js-main-target-form", 81 ".js-main-target-form",
@@ -63,12 +83,12 @@ var NoteList = { @@ -63,12 +83,12 @@ var NoteList = {
63 83
64 84
65 $(document).on("click", 85 $(document).on("click",
66 - ".js-choose-note-attachment-button",  
67 - NoteList.chooseNoteAttachment); 86 + ".js-choose-note-attachment-button",
  87 + NoteList.chooseNoteAttachment);
68 88
69 $(document).on("click", 89 $(document).on("click",
70 - ".js-show-outdated-discussion",  
71 - function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() }); 90 + ".js-show-outdated-discussion",
  91 + function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });
72 }, 92 },
73 93
74 94
@@ -107,8 +127,8 @@ var NoteList = { @@ -107,8 +127,8 @@ var NoteList = {
107 127
108 /** 128 /**
109 * Called when clicking the "Choose File" button. 129 * Called when clicking the "Choose File" button.
110 - *  
111 - * Opesn the file selection dialog. 130 + *
  131 + * Opens the file selection dialog.
112 */ 132 */
113 chooseNoteAttachment: function() { 133 chooseNoteAttachment: function() {
114 var form = $(this).closest("form"); 134 var form = $(this).closest("form");
@@ -143,7 +163,7 @@ var NoteList = { @@ -143,7 +163,7 @@ var NoteList = {
143 163
144 /** 164 /**
145 * Called in response to "cancel" on a diff note form. 165 * Called in response to "cancel" on a diff note form.
146 - * 166 + *
147 * Shows the reply button again. 167 * Shows the reply button again.
148 * Removes the form and if necessary it's temporary row. 168 * Removes the form and if necessary it's temporary row.
149 */ 169 */
@@ -187,6 +207,59 @@ var NoteList = { @@ -187,6 +207,59 @@ var NoteList = {
187 }, 207 },
188 208
189 /** 209 /**
  210 + * Called in response to clicking the edit note link
  211 + *
  212 + * Replaces the note text with the note edit form
  213 + * Adds a hidden div with the original content of the note to fill the edit note form with
  214 + * if the user cancels
  215 + */
  216 + showEditNoteForm: function(e) {
  217 + e.preventDefault();
  218 + var note = $(this).closest(".note");
  219 + note.find(".note-text").hide();
  220 +
  221 + // Show the attachment delete link
  222 + note.find(".js-note-attachment-delete").show();
  223 +
  224 + var form = note.find(".note-edit-form");
  225 + form.show();
  226 +
  227 +
  228 + var textarea = form.find("textarea");
  229 + var p = $("<p></p>").text(textarea.val());
  230 + var hidden_div = $('<div class="note-original-content"></div>').append(p);
  231 + form.append(hidden_div);
  232 + hidden_div.hide();
  233 + textarea.focus();
  234 + },
  235 +
  236 + /**
  237 + * Called in response to clicking the cancel button when editing a note
  238 + *
  239 + * Resets and hides the note editing form
  240 + */
  241 + cancelNoteEdit: function(e) {
  242 + e.preventDefault();
  243 + var note = $(this).closest(".note");
  244 + NoteList.resetNoteEditing(note);
  245 + },
  246 +
  247 +
  248 + /**
  249 + * Called in response to clicking the delete attachment link
  250 + *
  251 + * Removes the attachment wrapper view, including image tag if it exists
  252 + * Resets the note editing form
  253 + */
  254 + deleteNoteAttachment: function() {
  255 + var note = $(this).closest(".note");
  256 + note.find(".note-attachment").remove();
  257 + NoteList.resetNoteEditing(note);
  258 + NoteList.rewriteTimestamp(note.find(".note-last-update"));
  259 + },
  260 +
  261 +
  262 + /**
190 * Called when clicking on the "reply" button for a diff line. 263 * Called when clicking on the "reply" button for a diff line.
191 * 264 *
192 * Shows the note form below the notes. 265 * Shows the note form below the notes.
@@ -436,5 +509,65 @@ var NoteList = { @@ -436,5 +509,65 @@ var NoteList = {
436 votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes)); 509 votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));
437 votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes)); 510 votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));
438 } 511 }
  512 + },
  513 +
  514 + /**
  515 + * Called in response to the edit note form being submitted
  516 + *
  517 + * Updates the current note field.
  518 + * Hides the edit note form
  519 + */
  520 + updateNote: function(e, xhr, settings) {
  521 + response = JSON.parse(xhr.responseText);
  522 + if (response.success) {
  523 + var note_li = $("#note_" + response.id);
  524 + var note_text = note_li.find(".note-text");
  525 + note_text.html(response.note).show();
  526 +
  527 + var note_form = note_li.find(".note-edit-form");
  528 + note_form.hide();
  529 + note_form.find(".btn-save").enableButton();
  530 +
  531 + // Update the "Edited at xxx label" on the note to show it's just been updated
  532 + NoteList.rewriteTimestamp(note_li.find(".note-last-update"));
  533 + }
  534 + },
  535 +
  536 + /**
  537 + * Called in response to the 'cancel note' link clicked, or after deleting a note attachment
  538 + *
  539 + * Hides the edit note form and shows the note
  540 + * Resets the edit note form textarea with the original content of the note
  541 + */
  542 + resetNoteEditing: function(note) {
  543 + note.find(".note-text").show();
  544 +
  545 + // Hide the attachment delete link
  546 + note.find(".js-note-attachment-delete").hide();
  547 +
  548 + // Put the original content of the note back into the edit form textarea
  549 + var form = note.find(".note-edit-form");
  550 + var original_content = form.find(".note-original-content");
  551 + form.find("textarea").val(original_content.text());
  552 + original_content.remove();
  553 +
  554 + note.find(".note-edit-form").hide();
  555 + },
  556 +
  557 + /**
  558 + * Utility function to generate new timestamp text for a note
  559 + *
  560 + */
  561 + rewriteTimestamp: function(element) {
  562 + // Strip all newlines from the existing timestamp
  563 + var ts = element.text().replace(/\n/g, ' ').trim();
  564 +
  565 + // If the timestamp already has '(Edited xxx ago)' text, remove it
  566 + ts = ts.replace(new RegExp("\\(Edited [A-Za-z0-9 ]+\\)$", "gi"), "");
  567 +
  568 + // Append "(Edited just now)"
  569 + ts = (ts + " <small>(Edited just now)</small>");
  570 +
  571 + element.html(ts);
439 } 572 }
440 }; 573 };
app/assets/stylesheets/sections/header.scss
@@ -77,6 +77,7 @@ header { @@ -77,6 +77,7 @@ header {
77 top: -4px; 77 top: -4px;
78 img { 78 img {
79 width: 26px; 79 width: 26px;
  80 + height: 26px;
80 @include border-radius(4px); 81 @include border-radius(4px);
81 } 82 }
82 } 83 }
app/assets/stylesheets/sections/notes.scss
@@ -325,3 +325,32 @@ ul.notes { @@ -325,3 +325,32 @@ ul.notes {
325 float: left; 325 float: left;
326 } 326 }
327 } 327 }
  328 +
  329 +.note-edit-form {
  330 + display: none;
  331 +
  332 + .note_text {
  333 + border: 1px solid #DDD;
  334 + box-shadow: none;
  335 + font-size: 14px;
  336 + height: 80px;
  337 + width: 98.6%;
  338 + }
  339 +
  340 + .form-actions {
  341 + padding-left: 20px;
  342 +
  343 + .btn-save {
  344 + float: left;
  345 + }
  346 +
  347 + .note-form-option {
  348 + float: left;
  349 + padding: 2px 0 0 25px;
  350 + }
  351 + }
  352 +}
  353 +
  354 +.js-note-attachment-delete {
  355 + display: none;
  356 +}
app/controllers/projects/notes_controller.rb
@@ -38,6 +38,32 @@ class Projects::NotesController &lt; Projects::ApplicationController @@ -38,6 +38,32 @@ class Projects::NotesController &lt; Projects::ApplicationController
38 end 38 end
39 end 39 end
40 40
  41 + def update
  42 + @note = @project.notes.find(params[:id])
  43 + return access_denied! unless can?(current_user, :admin_note, @note)
  44 +
  45 + @note.update_attributes(params[:note])
  46 +
  47 + respond_to do |format|
  48 + format.js do
  49 + render js: { success: @note.valid?, id: @note.id, note: view_context.markdown(@note.note) }.to_json
  50 + end
  51 + format.html do
  52 + redirect_to :back
  53 + end
  54 + end
  55 + end
  56 +
  57 + def delete_attachment
  58 + @note = @project.notes.find(params[:id])
  59 + @note.remove_attachment!
  60 + @note.update_attribute(:attachment, nil)
  61 +
  62 + respond_to do |format|
  63 + format.js { render nothing: true }
  64 + end
  65 + end
  66 +
41 def preview 67 def preview
42 render text: view_context.markdown(params[:note]) 68 render text: view_context.markdown(params[:note])
43 end 69 end
app/helpers/notes_helper.rb
@@ -28,4 +28,11 @@ module NotesHelper @@ -28,4 +28,11 @@ module NotesHelper
28 def loading_new_notes? 28 def loading_new_notes?
29 params[:loading_new].present? 29 params[:loading_new].present?
30 end 30 end
  31 +
  32 + def note_timestamp(note)
  33 + # Shows the created at time and the updated at time if different
  34 + ts = "#{time_ago_in_words(note.created_at)} ago"
  35 + ts << content_tag(:small, " (Edited #{time_ago_in_words(note.updated_at)} ago)") if note.updated_at != note.created_at
  36 + ts.html_safe
  37 + end
31 end 38 end
app/views/layouts/_head_panel.html.haml
@@ -37,4 +37,4 @@ @@ -37,4 +37,4 @@
37 %i.icon-signout 37 %i.icon-signout
38 %li 38 %li
39 = link_to current_user, class: "profile-pic", id: 'profile-pic' do 39 = link_to current_user, class: "profile-pic", id: 'profile-pic' do
40 - = image_tag gravatar_icon(current_user.email, 26) 40 + = image_tag gravatar_icon(current_user.email, 26), alt: ''
app/views/projects/notes/_note.html.haml
@@ -6,13 +6,14 @@ @@ -6,13 +6,14 @@
6 Link here 6 Link here
7 &nbsp; 7 &nbsp;
8 - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project) 8 - if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
9 - = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, confirm: 'Are you sure you want to remove comment?', remote: true, class: "danger js-note-delete" do 9 + = link_to "#", title: "Edit comment", class: "js-note-edit" do
  10 + %i.icon-edit
  11 + = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, confirm: 'Are you sure you want to remove this comment?', remote: true, class: "danger js-note-delete" do
10 %i.icon-trash.cred 12 %i.icon-trash.cred
11 = image_tag gravatar_icon(note.author_email), class: "avatar s32" 13 = image_tag gravatar_icon(note.author_email), class: "avatar s32"
12 = link_to_member(@project, note.author, avatar: false) 14 = link_to_member(@project, note.author, avatar: false)
13 %span.note-last-update 15 %span.note-last-update
14 - = time_ago_in_words(note.updated_at)  
15 - ago 16 + = note_timestamp(note)
16 17
17 - if note.upvote? 18 - if note.upvote?
18 %span.vote.upvote.label.label-success 19 %span.vote.upvote.label.label-success
@@ -25,13 +26,37 @@ @@ -25,13 +26,37 @@
25 26
26 27
27 .note-body 28 .note-body
28 - = preserve do  
29 - = markdown(note.note) 29 + .note-text
  30 + = preserve do
  31 + = markdown(note.note)
  32 +
  33 + .note-edit-form
  34 + = form_for note, url: project_note_path(@project, note), method: :put, remote: true do |f|
  35 + = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on'
  36 +
  37 + .form-actions
  38 + = f.submit 'Save changes', class: "btn btn-primary btn-save"
  39 +
  40 + .note-form-option
  41 + %a.choose-btn.btn.btn-small.js-choose-note-attachment-button
  42 + %i.icon-paper-clip
  43 + %span Choose File ...
  44 + &nbsp;
  45 + %span.file_name.js-attachment-filename File name...
  46 + = f.file_field :attachment, class: "js-note-attachment-input hide"
  47 +
  48 + = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel"
  49 +
  50 +
30 - if note.attachment.url 51 - if note.attachment.url
31 - - if note.attachment.image?  
32 - = image_tag note.attachment.url, class: 'note-image-attach'  
33 - .attachment.pull-right  
34 - = link_to note.attachment.secure_url, target: "_blank" do  
35 - %i.icon-paper-clip  
36 - = note.attachment_identifier 52 + .note-attachment
  53 + - if note.attachment.image?
  54 + = image_tag note.attachment.url, class: 'note-image-attach'
  55 + .attachment.pull-right
  56 + = link_to note.attachment.secure_url, target: "_blank" do
  57 + %i.icon-paper-clip
  58 + = note.attachment_identifier
  59 + = link_to delete_attachment_project_note_path(@project, note),
  60 + title: "Delete this attachment", method: :delete, remote: true, confirm: 'Are you sure you want to remove the attachment?', class: "danger js-note-attachment-delete" do
  61 + %i.icon-trash.cred
37 .clear 62 .clear
app/views/snippets/_blob.html.haml
@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 .btn-group.tree-btn-group.pull-right 6 .btn-group.tree-btn-group.pull-right
7 - if @snippet.author == current_user 7 - if @snippet.author == current_user
8 = link_to "Edit", edit_snippet_path(@snippet), class: "btn btn-tiny", title: 'Edit Snippet' 8 = link_to "Edit", edit_snippet_path(@snippet), class: "btn btn-tiny", title: 'Edit Snippet'
  9 + = link_to "Delete", snippet_path(@snippet), method: :delete, confirm: "Are you sure?", class: "btn btn-tiny", title: 'Delete Snippet'
9 = link_to "Raw", raw_snippet_path(@snippet), class: "btn btn-tiny", target: "_blank" 10 = link_to "Raw", raw_snippet_path(@snippet), class: "btn btn-tiny", target: "_blank"
10 .file_content.code 11 .file_content.code
11 - unless @snippet.content.empty? 12 - unless @snippet.content.empty?
app/views/snippets/show.html.haml
1 %h3.page_title 1 %h3.page_title
2 - if @snippet.private? 2 - if @snippet.private?
3 - %i.icon-lock.cgreen 3 + %i{:class => "icon-lock cgreen has_bottom_tooltip", "data-original-title" => "Private snippet"}
4 - else 4 - else
5 - %i.icon-globe.cblue 5 + %i{:class => "icon-globe cblue has_bottom_tooltip", "data-original-title" => "Public snippet"}
6 6
7 = @snippet.title 7 = @snippet.title
8 8
config/gitlab.yml.example
@@ -18,6 +18,7 @@ production: &amp;base @@ -18,6 +18,7 @@ production: &amp;base
18 host: localhost 18 host: localhost
19 port: 80 19 port: 80
20 https: false 20 https: false
  21 + # WARNING: This feature is no longer supported
21 # Uncomment and customize to run in non-root path 22 # Uncomment and customize to run in non-root path
22 # Note that ENV['RAILS_RELATIVE_URL_ROOT'] in config/puma.rb may need to be changed 23 # Note that ENV['RAILS_RELATIVE_URL_ROOT'] in config/puma.rb may need to be changed
23 # relative_url_root: /gitlab 24 # relative_url_root: /gitlab
config/routes.rb
@@ -284,12 +284,16 @@ Gitlab::Application.routes.draw do @@ -284,12 +284,16 @@ Gitlab::Application.routes.draw do
284 end 284 end
285 end 285 end
286 286
287 - resources :notes, only: [:index, :create, :destroy] do 287 + resources :notes, only: [:index, :create, :destroy, :update] do
  288 + member do
  289 + delete :delete_attachment
  290 + end
  291 +
288 collection do 292 collection do
289 post :preview 293 post :preview
290 end 294 end
291 end 295 end
292 - end 296 + end
293 end 297 end
294 298
295 root to: "dashboard#show" 299 root to: "dashboard#show"
spec/factories.rb
  1 +include ActionDispatch::TestProcess
  2 +
1 FactoryGirl.define do 3 FactoryGirl.define do
2 sequence :sentence, aliases: [:title, :content] do 4 sequence :sentence, aliases: [:title, :content] do
3 Faker::Lorem.sentence 5 Faker::Lorem.sentence
@@ -127,6 +129,7 @@ FactoryGirl.define do @@ -127,6 +129,7 @@ FactoryGirl.define do
127 factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] 129 factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
128 factory :note_on_merge_request, traits: [:on_merge_request] 130 factory :note_on_merge_request, traits: [:on_merge_request]
129 factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] 131 factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
  132 + factory :note_on_merge_request_with_attachment, traits: [:on_merge_request, :with_attachment]
130 133
131 trait :on_commit do 134 trait :on_commit do
132 project factory: :project_with_code 135 project factory: :project_with_code
@@ -148,6 +151,10 @@ FactoryGirl.define do @@ -148,6 +151,10 @@ FactoryGirl.define do
148 noteable_id 1 151 noteable_id 1
149 noteable_type "Issue" 152 noteable_type "Issue"
150 end 153 end
  154 +
  155 + trait :with_attachment do
  156 + attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") }
  157 + end
151 end 158 end
152 159
153 factory :event do 160 factory :event do
spec/features/notes_on_merge_requests_spec.rb
@@ -3,6 +3,7 @@ require &#39;spec_helper&#39; @@ -3,6 +3,7 @@ require &#39;spec_helper&#39;
3 describe "On a merge request", js: true do 3 describe "On a merge request", js: true do
4 let!(:project) { create(:project_with_code) } 4 let!(:project) { create(:project_with_code) }
5 let!(:merge_request) { create(:merge_request, project: project) } 5 let!(:merge_request) { create(:merge_request, project: project) }
  6 + let!(:note) { create(:note_on_merge_request_with_attachment, project: project) }
6 7
7 before do 8 before do
8 login_as :user 9 login_as :user
@@ -72,6 +73,71 @@ describe &quot;On a merge request&quot;, js: true do @@ -72,6 +73,71 @@ describe &quot;On a merge request&quot;, js: true do
72 should_not have_css(".note") 73 should_not have_css(".note")
73 end 74 end
74 end 75 end
  76 +
  77 + describe "when editing a note", js: true do
  78 + it "should contain the hidden edit form" do
  79 + within("#note_#{note.id}") { should have_css(".note-edit-form", visible: false) }
  80 + end
  81 +
  82 + describe "editing the note" do
  83 + before do
  84 + find('.note').hover
  85 + find(".js-note-edit").click
  86 + end
  87 +
  88 + it "should show the note edit form and hide the note body" do
  89 + within("#note_#{note.id}") do
  90 + find(".note-edit-form", visible: true).should be_visible
  91 + find(".note-text", visible: false).should_not be_visible
  92 + end
  93 + end
  94 +
  95 + it "should reset the edit note form textarea with the original content of the note if cancelled" do
  96 + find('.note').hover
  97 + find(".js-note-edit").click
  98 +
  99 + within(".note-edit-form") do
  100 + fill_in "note[note]", with: "Some new content"
  101 + find(".btn-cancel").click
  102 + find(".js-note-text", visible: false).text.should == note.note
  103 + end
  104 + end
  105 +
  106 + it "appends the edited at time to the note" do
  107 + find('.note').hover
  108 + find(".js-note-edit").click
  109 +
  110 + within(".note-edit-form") do
  111 + fill_in "note[note]", with: "Some new content"
  112 + find(".btn-save").click
  113 + end
  114 +
  115 + within("#note_#{note.id}") do
  116 + should have_css(".note-last-update small")
  117 + find(".note-last-update small").text.should match(/Edited just now/)
  118 + end
  119 + end
  120 + end
  121 +
  122 + describe "deleting an attachment" do
  123 + before do
  124 + find('.note').hover
  125 + find(".js-note-edit").click
  126 + end
  127 +
  128 + it "shows the delete link" do
  129 + within(".note-attachment") do
  130 + should have_css(".js-note-attachment-delete")
  131 + end
  132 + end
  133 +
  134 + it "removes the attachment div and resets the edit form" do
  135 + find(".js-note-attachment-delete").click
  136 + should_not have_css(".note-attachment")
  137 + find(".note-edit-form", visible: false).should_not be_visible
  138 + end
  139 + end
  140 + end
75 end 141 end
76 142
77 describe "On a merge request diff", js: true, focus: true do 143 describe "On a merge request diff", js: true, focus: true do
spec/fixtures/dk.png 0 → 100644

1.12 KB

spec/routing/project_routing_spec.rb
@@ -237,7 +237,7 @@ end @@ -237,7 +237,7 @@ end
237 # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show 237 # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show
238 # PUT /:project_id/snippets/:id(.:format) snippets#update 238 # PUT /:project_id/snippets/:id(.:format) snippets#update
239 # DELETE /:project_id/snippets/:id(.:format) snippets#destroy 239 # DELETE /:project_id/snippets/:id(.:format) snippets#destroy
240 -describe Projects::SnippetsController, "routing" do 240 +describe SnippetsController, "routing" do
241 it "to #raw" do 241 it "to #raw" do
242 get("/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlabhq', id: '1') 242 get("/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlabhq', id: '1')
243 end 243 end