Merge branch '12910-personal-snippets-notes' into 'master'

Support comments for personal snippets

Closes #12910

See merge request !11090
This commit is contained in:
Sean McGivern 2017-05-05 15:12:00 +00:00
commit d4733aa025
34 changed files with 359 additions and 92 deletions

View File

@ -65,6 +65,15 @@ module NotesActions
private
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def note_json(note)
attrs = {
commands_changes: note.commands_changes
@ -98,6 +107,41 @@ module NotesActions
attrs
end
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
if params[:view] == 'parallel'
template = "discussions/_parallel_diff_discussion"
locals =
if params[:line_type] == 'old'
{ discussions_left: [discussion], discussions_right: nil }
else
{ discussions_left: nil, discussions_right: [discussion] }
end
else
template = "discussions/_diff_discussion"
locals = { discussions: [discussion] }
end
render_to_string(
template,
layout: false,
formats: [:html],
locals: locals
)
end
def discussion_html(discussion)
return if discussion.individual_note?
render_to_string(
"discussions/_discussion",
layout: false,
formats: [:html],
locals: { discussion: discussion }
)
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end

View File

@ -62,50 +62,6 @@ class Projects::NotesController < Projects::ApplicationController
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def discussion_html(discussion)
return if discussion.individual_note?
render_to_string(
"discussions/_discussion",
layout: false,
formats: [:html],
locals: { discussion: discussion }
)
end
def diff_discussion_html(discussion)
return unless discussion.diff_discussion?
if params[:view] == 'parallel'
template = "discussions/_parallel_diff_discussion"
locals =
if params[:line_type] == 'old'
{ discussions_left: [discussion], discussions_right: nil }
else
{ discussions_left: nil, discussions_right: [discussion] }
end
else
template = "discussions/_diff_discussion"
locals = { discussions: [discussion] }
end
render_to_string(
template,
layout: false,
formats: [:html],
locals: locals
)
end
def finder_params
params.merge(last_fetched_at: last_fetched_at)
end

View File

@ -13,15 +13,6 @@ class Snippets::NotesController < ApplicationController
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def project
nil
end

View File

@ -64,6 +64,7 @@ class SnippetsController < ApplicationController
blob = @snippet.blob
override_max_blob_size(blob)
@note = Note.new(noteable: @snippet)
@noteable = @snippet
@discussions = @snippet.discussions

View File

@ -123,7 +123,11 @@ module GitlabRoutingHelper
end
def preview_markdown_path(project, *args)
preview_markdown_namespace_project_path(project.namespace, project, *args)
if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippet_path(@snippet)
else
preview_markdown_namespace_project_path(project.namespace, project, *args)
end
end
def toggle_subscription_path(entity, *args)

View File

@ -76,4 +76,47 @@ module NotesHelper
namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: anchor)
end
end
def notes_url
if @snippet.is_a?(PersonalSnippet)
snippet_notes_path(@snippet)
else
namespace_project_noteable_notes_path(
namespace_id: @project.namespace,
project_id: @project,
target_id: @noteable.id,
target_type: @noteable.class.name.underscore
)
end
end
def note_url(note)
if note.noteable.is_a?(PersonalSnippet)
snippet_note_path(note.noteable, note)
else
namespace_project_note_path(@project.namespace, @project, note)
end
end
def form_resources
if @snippet.is_a?(PersonalSnippet)
[@note]
else
[@project.namespace.becomes(Namespace), @project, @note]
end
end
def new_form_url
return nil unless @snippet.is_a?(PersonalSnippet)
snippet_notes_path(@snippet)
end
def can_create_note?
if @snippet.is_a?(PersonalSnippet)
can?(current_user, :comment_personal_snippet, @snippet)
else
can?(current_user, :create_note, @project)
end
end
end

View File

@ -3,8 +3,8 @@ module Notes
def execute
in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id)
if project && in_reply_to_discussion_id.present?
discussion = project.notes.find_discussion(in_reply_to_discussion_id)
if in_reply_to_discussion_id.present?
discussion = find_discussion(in_reply_to_discussion_id)
unless discussion
note = Note.new
@ -21,5 +21,19 @@ module Notes
note
end
def find_discussion(discussion_id)
if project
project.notes.find_discussion(discussion_id)
else
# only PersonalSnippets can have discussions without project association
discussion = Note.find_discussion(discussion_id)
noteable = discussion.noteable
return nil unless noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
discussion
end
end
end
end

View File

@ -1,3 +1,9 @@
- header_title "Snippets", snippets_path
- content_for :page_specific_javascripts do
- if @snippet&.persisted? && current_user
:javascript
window.uploads_path = "#{upload_path('personal_snippet', @snippet)}";
window.preview_markdown_path = "#{preview_markdown_snippet_path(@snippet)}";
= render template: "layouts/application"

View File

@ -13,7 +13,7 @@
.block-connector
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form"
= render "shared/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
= render "projects/commit/change", type: type, commit: @commit, title: @commit.title

View File

@ -4,4 +4,4 @@
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
= render 'shared/notes/notes_with_form'

View File

@ -8,4 +8,4 @@
%button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
#notes= render "projects/notes/notes_with_form"
#notes= render "shared/notes/notes_with_form"

View File

@ -11,7 +11,7 @@
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
= render 'projects/notes/hints'
= render 'shared/notes/hints'
.clearfix
.error-alert
= render "shared/milestones/form_dates", f: f

View File

@ -1,3 +0,0 @@
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note

View File

@ -13,7 +13,7 @@
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
= render 'shared/notes/hints'
.error-alert
.prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save'

View File

@ -9,4 +9,4 @@
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
#notes= render "projects/notes/notes_with_form"
#notes= render "shared/notes/notes_with_form"

View File

@ -30,7 +30,7 @@
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
= render 'shared/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3

View File

@ -14,7 +14,7 @@
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: namespace_project_wiki_preview_markdown_path(@project.namespace, @project, @page.slug) } do
= render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: 'Write your content or drag files here...'
= render 'projects/notes/hints'
= render 'shared/notes/hints'
.clearfix
.error-alert

View File

@ -17,6 +17,6 @@
classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...",
supports_slash_commands: supports_slash_commands
= render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
= render 'shared/notes/hints', supports_slash_commands: supports_slash_commands
.clearfix
.error-alert

View File

@ -0,0 +1,3 @@
.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note

View File

@ -4,7 +4,7 @@
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do
= render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..."
= render 'projects/notes/hints'
= render 'shared/notes/hints'
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-finish-edit-warning

View File

@ -4,7 +4,7 @@
- else
- preview_url = preview_markdown_path(@project)
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= form_for form_resources, url: new_form_url, remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
@ -28,11 +28,11 @@
classes: 'note-textarea js-note-text',
placeholder: "Write a comment or drag your files here...",
supports_slash_commands: supports_slash_commands
= render 'projects/notes/hints', supports_slash_commands: supports_slash_commands
= render 'shared/notes/hints', supports_slash_commands: supports_slash_commands
.error-alert
.note-form-actions.clearfix
= render partial: 'projects/notes/comment_button'
= render partial: 'shared/notes/comment_button'
= yield(:note_actions)

View File

@ -42,10 +42,7 @@
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
- if note.for_personal_snippet?
= render 'snippets/notes/edit', note: note
- else
= render 'projects/notes/edit', note: note
= render 'shared/notes/edit', note: note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system

View File

@ -1,18 +1,18 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "shared/notes/notes"
= render 'projects/notes/edit_form', project: @project
= render 'shared/notes/edit_form', project: @project
%ul.notes.notes-form.timeline
%li.timeline-entry
.flash-container.timeline-content
- if can? current_user, :create_note, @project
- if can_create_note?
.timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "projects/notes/form", view: diff_view
= render "shared/notes/form", view: diff_view
- elsif !current_user
.disabled-comment.text-center
.disabled-comment-text.inline
@ -23,4 +23,4 @@
to post a comment
:javascript
var notes = new Notes("#{namespace_project_noteable_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")

View File

@ -1,2 +0,0 @@
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"

View File

@ -2,11 +2,11 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
= render 'shared/snippets/blob'
.personal-snippets
%article.file-holder.snippet-file-content
= render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
%ul#notes-list.notes.main-notes-list.timeline
#notes= render 'shared/notes/notes'
#notes= render "shared/notes/notes_with_form"

View File

@ -0,0 +1,4 @@
---
title: Support comments for personal snippets
merge_request:
author:

View File

@ -29,6 +29,8 @@ FactoryGirl.define do
factory :discussion_note_on_commit, traits: [:on_commit], class: DiscussionNote
factory :discussion_note_on_personal_snippet, traits: [:on_personal_snippet], class: DiscussionNote
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do

View File

@ -1,6 +1,6 @@
require 'spec_helper'
describe 'Comments on personal snippets', feature: true do
describe 'Comments on personal snippets', :js, feature: true do
let!(:user) { create(:user) }
let!(:snippet) { create(:personal_snippet, :public) }
let!(:snippet_notes) do
@ -18,7 +18,7 @@ describe 'Comments on personal snippets', feature: true do
subject { page }
context 'viewing the snippet detail page' do
context 'when viewing the snippet detail page' do
it 'contains notes for a snippet with correct action icons' do
expect(page).to have_selector('#notes-list li', count: 2)
@ -36,4 +36,64 @@ describe 'Comments on personal snippets', feature: true do
end
end
end
context 'when submitting a note' do
it 'shows a valid form' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form .js-comment-button').value).
to eq('Comment')
page.within('.js-main-target-form') do
expect(page).not_to have_link('Cancel')
end
end
it 'previews a note' do
fill_in 'note[note]', with: 'This is **awesome**!'
find('.js-md-preview-button').click
page.within('.new-note .md-preview') do
expect(page).to have_content('This is awesome!')
expect(page).to have_selector('strong')
end
end
it 'creates a note' do
fill_in 'note[note]', with: 'This is **awesome**!'
click_button 'Comment'
expect(find('div#notes')).to have_content('This is awesome!')
end
end
context 'when editing a note' do
it 'changes the text' do
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
click_on 'Edit comment'
end
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'new content'
find('.btn-save').click
end
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
expect(page).to have_css('.note_edited_ago')
expect(page).to have_content('new content')
expect(find('.note_edited_ago').text).to match(/less than a minute ago/)
end
end
end
context 'when deleting a note' do
it 'removes the note from the snippet detail page' do
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
click_on 'Remove comment'
end
wait_for_ajax
expect(page).not_to have_selector("#notes-list li#note_#{snippet_notes[0].id}")
end
end
end

View File

@ -175,4 +175,79 @@ describe NotesHelper do
end
end
end
describe '#notes_url' do
it 'return snippet notes path for personal snippet' do
@snippet = create(:personal_snippet)
expect(helper.notes_url).to eq("/snippets/#{@snippet.id}/notes")
end
it 'return project notes path for project snippet' do
namespace = create(:namespace, path: 'nm')
@project = create(:empty_project, path: 'test', namespace: namespace)
@snippet = create(:project_snippet, project: @project)
@noteable = @snippet
expect(helper.notes_url).to eq("/nm/test/noteable/project_snippet/#{@noteable.id}/notes")
end
it 'return project notes path for other noteables' do
namespace = create(:namespace, path: 'nm')
@project = create(:empty_project, path: 'test', namespace: namespace)
@noteable = create(:issue, project: @project)
expect(helper.notes_url).to eq("/nm/test/noteable/issue/#{@noteable.id}/notes")
end
end
describe '#note_url' do
it 'return snippet notes path for personal snippet' do
note = create(:note_on_personal_snippet)
expect(helper.note_url(note)).to eq("/snippets/#{note.noteable.id}/notes/#{note.id}")
end
it 'return project notes path for project snippet' do
namespace = create(:namespace, path: 'nm')
@project = create(:empty_project, path: 'test', namespace: namespace)
note = create(:note_on_project_snippet, project: @project)
expect(helper.note_url(note)).to eq("/nm/test/notes/#{note.id}")
end
it 'return project notes path for other noteables' do
namespace = create(:namespace, path: 'nm')
@project = create(:empty_project, path: 'test', namespace: namespace)
note = create(:note_on_issue, project: @project)
expect(helper.note_url(note)).to eq("/nm/test/notes/#{note.id}")
end
end
describe '#form_resurces' do
it 'returns note for personal snippet' do
@snippet = create(:personal_snippet)
@note = create(:note_on_personal_snippet)
expect(helper.form_resources).to eq([@note])
end
it 'returns namespace, project and note for project snippet' do
namespace = create(:namespace, path: 'nm')
@project = create(:empty_project, path: 'test', namespace: namespace)
@snippet = create(:project_snippet, project: @project)
@note = create(:note_on_personal_snippet)
expect(helper.form_resources).to eq([@project.namespace, @project, @note])
end
it 'returns namespace, project and note path for other noteables' do
namespace = create(:namespace, path: 'nm')
@project = create(:empty_project, path: 'test', namespace: namespace)
@note = create(:note_on_issue, project: @project)
expect(helper.form_resources).to eq([@project.namespace, @project, @note])
end
end
end

View File

@ -29,10 +29,82 @@ describe Notes::BuildService, services: true do
expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
end
end
context 'personal snippet note' do
def reply(note, user = nil)
user ||= create(:user)
described_class.new(nil,
user,
note: 'Test',
in_reply_to_discussion_id: note.discussion_id).execute
end
let(:snippet_author) { create(:user) }
context 'when a snippet is public' do
it 'creates a reply note' do
snippet = create(:personal_snippet, :public)
note = create(:discussion_note_on_personal_snippet, noteable: snippet)
new_note = reply(note)
expect(new_note).to be_valid
expect(new_note.in_reply_to?(note)).to be_truthy
end
end
context 'when a snippet is private' do
let(:snippet) { create(:personal_snippet, :private, author: snippet_author) }
let(:note) { create(:discussion_note_on_personal_snippet, noteable: snippet) }
it 'creates a reply note when the author replies' do
new_note = reply(note, snippet_author)
expect(new_note).to be_valid
expect(new_note.in_reply_to?(note)).to be_truthy
end
it 'sets an error when another user replies' do
new_note = reply(note)
expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
end
end
context 'when a snippet is internal' do
let(:snippet) { create(:personal_snippet, :internal, author: snippet_author) }
let(:note) { create(:discussion_note_on_personal_snippet, noteable: snippet) }
it 'creates a reply note when the author replies' do
new_note = reply(note, snippet_author)
expect(new_note).to be_valid
expect(new_note.in_reply_to?(note)).to be_truthy
end
it 'creates a reply note when a regular user replies' do
new_note = reply(note)
expect(new_note).to be_valid
expect(new_note.in_reply_to?(note)).to be_truthy
end
it 'sets an error when an external user replies' do
new_note = reply(note, create(:user, :external))
expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found')
end
end
end
end
it 'builds a note without saving it' do
new_note = described_class.new(project, author, noteable_type: note.noteable_type, noteable_id: note.noteable_id, note: 'Test').execute
new_note = described_class.new(project,
author,
noteable_type: note.noteable_type,
noteable_id: note.noteable_id,
note: 'Test').execute
expect(new_note).to be_valid
expect(new_note).not_to be_persisted
end

View File

@ -1,6 +1,6 @@
require 'spec_helper'
describe 'projects/notes/_form' do
describe 'shared/notes/_form' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }