summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2013-07-02 11:47:09 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2013-07-02 11:47:09 +0300
commitee890f2b2a66be925746f2238dc462a74b0fd219 (patch)
treebb2e74eb834d5c69f66e42c7a80af1705e616ea9
parentf49fb5dca1ecf2b1ae6415920de09b4d95c14bb1 (diff)
parent7588186e8173f53a7f9b86d2b6959d7f94a3caac (diff)
downloadgitlab-ce-ee890f2b2a66be925746f2238dc462a74b0fd219.tar.gz
Merge branch 'master' into 6-0-dev
Conflicts: app/views/dashboard/projects.html.haml app/views/layouts/_head_panel.html.haml config/routes.rb
-rw-r--r--app/assets/javascripts/notes.js147
-rw-r--r--app/assets/stylesheets/sections/header.scss1
-rw-r--r--app/assets/stylesheets/sections/notes.scss29
-rw-r--r--app/controllers/projects/notes_controller.rb26
-rw-r--r--app/helpers/notes_helper.rb7
-rw-r--r--app/views/layouts/_head_panel.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml47
-rw-r--r--app/views/snippets/_blob.html.haml1
-rw-r--r--app/views/snippets/show.html.haml4
-rw-r--r--config/gitlab.yml.example1
-rw-r--r--config/routes.rb8
-rw-r--r--spec/factories.rb7
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb66
-rw-r--r--spec/fixtures/dk.pngbin0 -> 1143 bytes
-rw-r--r--spec/routing/project_routing_spec.rb2
15 files changed, 324 insertions, 24 deletions
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index e0715f45417..85d86f3f0bd 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -56,6 +56,26 @@ var NoteList = {
".js-note-delete",
NoteList.removeNote);
+ // show the edit note form
+ $(document).on("click",
+ ".js-note-edit",
+ NoteList.showEditNoteForm);
+
+ // cancel note editing
+ $(document).on("click",
+ ".note-edit-cancel",
+ NoteList.cancelNoteEdit);
+
+ // delete note attachment
+ $(document).on("click",
+ ".js-note-attachment-delete",
+ NoteList.deleteNoteAttachment);
+
+ // update the note after editing
+ $(document).on("ajax:complete",
+ "form.edit_note",
+ NoteList.updateNote);
+
// reset main target form after submit
$(document).on("ajax:complete",
".js-main-target-form",
@@ -63,12 +83,12 @@ var NoteList = {
$(document).on("click",
- ".js-choose-note-attachment-button",
- NoteList.chooseNoteAttachment);
+ ".js-choose-note-attachment-button",
+ NoteList.chooseNoteAttachment);
$(document).on("click",
- ".js-show-outdated-discussion",
- function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });
+ ".js-show-outdated-discussion",
+ function(e) { $(this).next('.outdated-discussion').show(); e.preventDefault() });
},
@@ -107,8 +127,8 @@ var NoteList = {
/**
* Called when clicking the "Choose File" button.
- *
- * Opesn the file selection dialog.
+ *
+ * Opens the file selection dialog.
*/
chooseNoteAttachment: function() {
var form = $(this).closest("form");
@@ -143,7 +163,7 @@ var NoteList = {
/**
* Called in response to "cancel" on a diff note form.
- *
+ *
* Shows the reply button again.
* Removes the form and if necessary it's temporary row.
*/
@@ -187,6 +207,59 @@ var NoteList = {
},
/**
+ * Called in response to clicking the edit note link
+ *
+ * Replaces the note text with the note edit form
+ * Adds a hidden div with the original content of the note to fill the edit note form with
+ * if the user cancels
+ */
+ showEditNoteForm: function(e) {
+ e.preventDefault();
+ var note = $(this).closest(".note");
+ note.find(".note-text").hide();
+
+ // Show the attachment delete link
+ note.find(".js-note-attachment-delete").show();
+
+ var form = note.find(".note-edit-form");
+ form.show();
+
+
+ var textarea = form.find("textarea");
+ var p = $("<p></p>").text(textarea.val());
+ var hidden_div = $('<div class="note-original-content"></div>').append(p);
+ form.append(hidden_div);
+ hidden_div.hide();
+ textarea.focus();
+ },
+
+ /**
+ * Called in response to clicking the cancel button when editing a note
+ *
+ * Resets and hides the note editing form
+ */
+ cancelNoteEdit: function(e) {
+ e.preventDefault();
+ var note = $(this).closest(".note");
+ NoteList.resetNoteEditing(note);
+ },
+
+
+ /**
+ * Called in response to clicking the delete attachment link
+ *
+ * Removes the attachment wrapper view, including image tag if it exists
+ * Resets the note editing form
+ */
+ deleteNoteAttachment: function() {
+ var note = $(this).closest(".note");
+ note.find(".note-attachment").remove();
+ NoteList.resetNoteEditing(note);
+ NoteList.rewriteTimestamp(note.find(".note-last-update"));
+ },
+
+
+ /**
* Called when clicking on the "reply" button for a diff line.
*
* Shows the note form below the notes.
@@ -436,5 +509,65 @@ var NoteList = {
votes.find(".upvotes").text(votes.find(".upvotes").text().replace(/\d+/, upvotes));
votes.find(".downvotes").text(votes.find(".downvotes").text().replace(/\d+/, downvotes));
}
+ },
+
+ /**
+ * Called in response to the edit note form being submitted
+ *
+ * Updates the current note field.
+ * Hides the edit note form
+ */
+ updateNote: function(e, xhr, settings) {
+ response = JSON.parse(xhr.responseText);
+ if (response.success) {
+ var note_li = $("#note_" + response.id);
+ var note_text = note_li.find(".note-text");
+ note_text.html(response.note).show();
+
+ var note_form = note_li.find(".note-edit-form");
+ note_form.hide();
+ note_form.find(".btn-save").enableButton();
+
+ // Update the "Edited at xxx label" on the note to show it's just been updated
+ NoteList.rewriteTimestamp(note_li.find(".note-last-update"));
+ }
+ },
+
+ /**
+ * Called in response to the 'cancel note' link clicked, or after deleting a note attachment
+ *
+ * Hides the edit note form and shows the note
+ * Resets the edit note form textarea with the original content of the note
+ */
+ resetNoteEditing: function(note) {
+ note.find(".note-text").show();
+
+ // Hide the attachment delete link
+ note.find(".js-note-attachment-delete").hide();
+
+ // Put the original content of the note back into the edit form textarea
+ var form = note.find(".note-edit-form");
+ var original_content = form.find(".note-original-content");
+ form.find("textarea").val(original_content.text());
+ original_content.remove();
+
+ note.find(".note-edit-form").hide();
+ },
+
+ /**
+ * Utility function to generate new timestamp text for a note
+ *
+ */
+ rewriteTimestamp: function(element) {
+ // Strip all newlines from the existing timestamp
+ var ts = element.text().replace(/\n/g, ' ').trim();
+
+ // If the timestamp already has '(Edited xxx ago)' text, remove it
+ ts = ts.replace(new RegExp("\\(Edited [A-Za-z0-9 ]+\\)$", "gi"), "");
+
+ // Append "(Edited just now)"
+ ts = (ts + " <small>(Edited just now)</small>");
+
+ element.html(ts);
}
};
diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss
index 46a5a489958..ba92e8554cb 100644
--- a/app/assets/stylesheets/sections/header.scss
+++ b/app/assets/stylesheets/sections/header.scss
@@ -77,6 +77,7 @@ header {
top: -4px;
img {
width: 26px;
+ height: 26px;
@include border-radius(4px);
}
}
diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss
index d4bb4872ac7..bae1ac3aa9a 100644
--- a/app/assets/stylesheets/sections/notes.scss
+++ b/app/assets/stylesheets/sections/notes.scss
@@ -325,3 +325,32 @@ ul.notes {
float: left;
}
}
+
+.note-edit-form {
+ display: none;
+
+ .note_text {
+ border: 1px solid #DDD;
+ box-shadow: none;
+ font-size: 14px;
+ height: 80px;
+ width: 98.6%;
+ }
+
+ .form-actions {
+ padding-left: 20px;
+
+ .btn-save {
+ float: left;
+ }
+
+ .note-form-option {
+ float: left;
+ padding: 2px 0 0 25px;
+ }
+ }
+}
+
+.js-note-attachment-delete {
+ display: none;
+}
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index ef15e419dbe..8214163c315 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -38,6 +38,32 @@ class Projects::NotesController < Projects::ApplicationController
end
end
+ def update
+ @note = @project.notes.find(params[:id])
+ return access_denied! unless can?(current_user, :admin_note, @note)
+
+ @note.update_attributes(params[:note])
+
+ respond_to do |format|
+ format.js do
+ render js: { success: @note.valid?, id: @note.id, note: view_context.markdown(@note.note) }.to_json
+ end
+ format.html do
+ redirect_to :back
+ end
+ end
+ end
+
+ def delete_attachment
+ @note = @project.notes.find(params[:id])
+ @note.remove_attachment!
+ @note.update_attribute(:attachment, nil)
+
+ respond_to do |format|
+ format.js { render nothing: true }
+ end
+ end
+
def preview
render text: view_context.markdown(params[:note])
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index fbd0f01e5d4..a3ec4cca59d 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -28,4 +28,11 @@ module NotesHelper
def loading_new_notes?
params[:loading_new].present?
end
+
+ def note_timestamp(note)
+ # Shows the created at time and the updated at time if different
+ ts = "#{time_ago_in_words(note.created_at)} ago"
+ ts << content_tag(:small, " (Edited #{time_ago_in_words(note.updated_at)} ago)") if note.updated_at != note.created_at
+ ts.html_safe
+ end
end
diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml
index 2fbac5fff37..5644c89016b 100644
--- a/app/views/layouts/_head_panel.html.haml
+++ b/app/views/layouts/_head_panel.html.haml
@@ -37,4 +37,4 @@
%i.icon-signout
%li
= link_to current_user, class: "profile-pic", id: 'profile-pic' do
- = image_tag gravatar_icon(current_user.email, 26)
+ = image_tag gravatar_icon(current_user.email, 26), alt: ''
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 6a1159bc8f0..6fa7a1c3c78 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -6,13 +6,14 @@
Link here
&nbsp;
- if(note.author_id == current_user.id) || can?(current_user, :admin_note, @project)
- = 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
+ = link_to "#", title: "Edit comment", class: "js-note-edit" do
+ %i.icon-edit
+ = 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
%i.icon-trash.cred
= image_tag gravatar_icon(note.author_email), class: "avatar s32"
= link_to_member(@project, note.author, avatar: false)
%span.note-last-update
- = time_ago_in_words(note.updated_at)
- ago
+ = note_timestamp(note)
- if note.upvote?
%span.vote.upvote.label.label-success
@@ -25,13 +26,37 @@
.note-body
- = preserve do
- = markdown(note.note)
+ .note-text
+ = preserve do
+ = markdown(note.note)
+
+ .note-edit-form
+ = form_for note, url: project_note_path(@project, note), method: :put, remote: true do |f|
+ = f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on'
+
+ .form-actions
+ = f.submit 'Save changes', class: "btn btn-primary btn-save"
+
+ .note-form-option
+ %a.choose-btn.btn.btn-small.js-choose-note-attachment-button
+ %i.icon-paper-clip
+ %span Choose File ...
+ &nbsp;
+ %span.file_name.js-attachment-filename File name...
+ = f.file_field :attachment, class: "js-note-attachment-input hide"
+
+ = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel"
+
+
- if note.attachment.url
- - if note.attachment.image?
- = image_tag note.attachment.url, class: 'note-image-attach'
- .attachment.pull-right
- = link_to note.attachment.secure_url, target: "_blank" do
- %i.icon-paper-clip
- = note.attachment_identifier
+ .note-attachment
+ - if note.attachment.image?
+ = image_tag note.attachment.url, class: 'note-image-attach'
+ .attachment.pull-right
+ = link_to note.attachment.secure_url, target: "_blank" do
+ %i.icon-paper-clip
+ = note.attachment_identifier
+ = link_to delete_attachment_project_note_path(@project, note),
+ 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
+ %i.icon-trash.cred
.clear
diff --git a/app/views/snippets/_blob.html.haml b/app/views/snippets/_blob.html.haml
index 6f62ea05205..c538da0bee5 100644
--- a/app/views/snippets/_blob.html.haml
+++ b/app/views/snippets/_blob.html.haml
@@ -6,6 +6,7 @@
.btn-group.tree-btn-group.pull-right
- if @snippet.author == current_user
= link_to "Edit", edit_snippet_path(@snippet), class: "btn btn-tiny", title: 'Edit Snippet'
+ = link_to "Delete", snippet_path(@snippet), method: :delete, confirm: "Are you sure?", class: "btn btn-tiny", title: 'Delete Snippet'
= link_to "Raw", raw_snippet_path(@snippet), class: "btn btn-tiny", target: "_blank"
.file_content.code
- unless @snippet.content.empty?
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index f425c4bd51e..ac6daed56b6 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,8 +1,8 @@
%h3.page_title
- if @snippet.private?
- %i.icon-lock.cgreen
+ %i{:class => "icon-lock cgreen has_bottom_tooltip", "data-original-title" => "Private snippet"}
- else
- %i.icon-globe.cblue
+ %i{:class => "icon-globe cblue has_bottom_tooltip", "data-original-title" => "Public snippet"}
= @snippet.title
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 9f50aab3c28..ad24ba97c6f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -18,6 +18,7 @@ production: &base
host: localhost
port: 80
https: false
+ # WARNING: This feature is no longer supported
# Uncomment and customize to run in non-root path
# Note that ENV['RAILS_RELATIVE_URL_ROOT'] in config/puma.rb may need to be changed
# relative_url_root: /gitlab
diff --git a/config/routes.rb b/config/routes.rb
index 4e0c010840f..f319ef221df 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -284,12 +284,16 @@ Gitlab::Application.routes.draw do
end
end
- resources :notes, only: [:index, :create, :destroy] do
+ resources :notes, only: [:index, :create, :destroy, :update] do
+ member do
+ delete :delete_attachment
+ end
+
collection do
post :preview
end
end
- end
+ end
end
root to: "dashboard#show"
diff --git a/spec/factories.rb b/spec/factories.rb
index 272623440e1..793bd2434e8 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -1,3 +1,5 @@
+include ActionDispatch::TestProcess
+
FactoryGirl.define do
sequence :sentence, aliases: [:title, :content] do
Faker::Lorem.sentence
@@ -127,6 +129,7 @@ FactoryGirl.define do
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff]
+ factory :note_on_merge_request_with_attachment, traits: [:on_merge_request, :with_attachment]
trait :on_commit do
project factory: :project_with_code
@@ -148,6 +151,10 @@ FactoryGirl.define do
noteable_id 1
noteable_type "Issue"
end
+
+ trait :with_attachment do
+ attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") }
+ end
end
factory :event do
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 24f5437efff..d7bc66dd9c8 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe "On a merge request", js: true do
let!(:project) { create(:project_with_code) }
let!(:merge_request) { create(:merge_request, project: project) }
+ let!(:note) { create(:note_on_merge_request_with_attachment, project: project) }
before do
login_as :user
@@ -72,6 +73,71 @@ describe "On a merge request", js: true do
should_not have_css(".note")
end
end
+
+ describe "when editing a note", js: true do
+ it "should contain the hidden edit form" do
+ within("#note_#{note.id}") { should have_css(".note-edit-form", visible: false) }
+ end
+
+ describe "editing the note" do
+ before do
+ find('.note').hover
+ find(".js-note-edit").click
+ end
+
+ it "should show the note edit form and hide the note body" do
+ within("#note_#{note.id}") do
+ find(".note-edit-form", visible: true).should be_visible
+ find(".note-text", visible: false).should_not be_visible
+ end
+ end
+
+ it "should reset the edit note form textarea with the original content of the note if cancelled" do
+ find('.note').hover
+ find(".js-note-edit").click
+
+ within(".note-edit-form") do
+ fill_in "note[note]", with: "Some new content"
+ find(".btn-cancel").click
+ find(".js-note-text", visible: false).text.should == note.note
+ end
+ end
+
+ it "appends the edited at time to the note" do
+ find('.note').hover
+ find(".js-note-edit").click
+
+ within(".note-edit-form") do
+ fill_in "note[note]", with: "Some new content"
+ find(".btn-save").click
+ end
+
+ within("#note_#{note.id}") do
+ should have_css(".note-last-update small")
+ find(".note-last-update small").text.should match(/Edited just now/)
+ end
+ end
+ end
+
+ describe "deleting an attachment" do
+ before do
+ find('.note').hover
+ find(".js-note-edit").click
+ end
+
+ it "shows the delete link" do
+ within(".note-attachment") do
+ should have_css(".js-note-attachment-delete")
+ end
+ end
+
+ it "removes the attachment div and resets the edit form" do
+ find(".js-note-attachment-delete").click
+ should_not have_css(".note-attachment")
+ find(".note-edit-form", visible: false).should_not be_visible
+ end
+ end
+ end
end
describe "On a merge request diff", js: true, focus: true do
diff --git a/spec/fixtures/dk.png b/spec/fixtures/dk.png
new file mode 100644
index 00000000000..87ce25e877a
--- /dev/null
+++ b/spec/fixtures/dk.png
Binary files differ
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 35de757b18b..831e464f9cb 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -237,7 +237,7 @@ end
# project_snippet GET /:project_id/snippets/:id(.:format) snippets#show
# PUT /:project_id/snippets/:id(.:format) snippets#update
# DELETE /:project_id/snippets/:id(.:format) snippets#destroy
-describe Projects::SnippetsController, "routing" do
+describe SnippetsController, "routing" do
it "to #raw" do
get("/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlabhq', id: '1')
end