Merge branch '12910-personal-snippets-notes-show' into 'master'
Display comments for personal snippets See merge request !10974
This commit is contained in:
commit
d32ecb23eb
|
@ -0,0 +1,136 @@
|
|||
module NotesActions
|
||||
include RendersNotes
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authorize_admin_note!, only: [:update, :destroy]
|
||||
end
|
||||
|
||||
def index
|
||||
current_fetched_at = Time.now.to_i
|
||||
|
||||
notes_json = { notes: [], last_fetched_at: current_fetched_at }
|
||||
|
||||
@notes = notes_finder.execute.inc_relations_for_view
|
||||
@notes = prepare_notes_for_rendering(@notes)
|
||||
|
||||
@notes.each do |note|
|
||||
next if note.cross_reference_not_visible_for?(current_user)
|
||||
|
||||
notes_json[:notes] << note_json(note)
|
||||
end
|
||||
|
||||
render json: notes_json
|
||||
end
|
||||
|
||||
def create
|
||||
create_params = note_params.merge(
|
||||
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
|
||||
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
|
||||
)
|
||||
@note = Notes::CreateService.new(project, current_user, create_params).execute
|
||||
|
||||
if @note.is_a?(Note)
|
||||
Banzai::NoteRenderer.render([@note], @project, current_user)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: note_json(@note) }
|
||||
format.html { redirect_back_or_default }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
|
||||
|
||||
if @note.is_a?(Note)
|
||||
Banzai::NoteRenderer.render([@note], @project, current_user)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: note_json(@note) }
|
||||
format.html { redirect_back_or_default }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if note.editable?
|
||||
Notes::DestroyService.new(project, current_user).execute(note)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def note_json(note)
|
||||
attrs = {
|
||||
commands_changes: note.commands_changes
|
||||
}
|
||||
|
||||
if note.persisted?
|
||||
attrs.merge!(
|
||||
valid: true,
|
||||
id: note.id,
|
||||
discussion_id: note.discussion_id(noteable),
|
||||
html: note_html(note),
|
||||
note: note.note
|
||||
)
|
||||
|
||||
discussion = note.to_discussion(noteable)
|
||||
unless discussion.individual_note?
|
||||
attrs.merge!(
|
||||
discussion_resolvable: discussion.resolvable?,
|
||||
|
||||
diff_discussion_html: diff_discussion_html(discussion),
|
||||
discussion_html: discussion_html(discussion)
|
||||
)
|
||||
end
|
||||
else
|
||||
attrs.merge!(
|
||||
valid: false,
|
||||
errors: note.errors
|
||||
)
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def authorize_admin_note!
|
||||
return access_denied! unless can?(current_user, :admin_note, note)
|
||||
end
|
||||
|
||||
def note_params
|
||||
params.require(:note).permit(
|
||||
:project_id,
|
||||
:noteable_type,
|
||||
:noteable_id,
|
||||
:commit_id,
|
||||
:noteable,
|
||||
:type,
|
||||
|
||||
:note,
|
||||
:attachment,
|
||||
|
||||
# LegacyDiffNote
|
||||
:line_code,
|
||||
|
||||
# DiffNote
|
||||
:position
|
||||
)
|
||||
end
|
||||
|
||||
def noteable
|
||||
@noteable ||= notes_finder.target
|
||||
end
|
||||
|
||||
def last_fetched_at
|
||||
request.headers['X-Last-Fetched-At']
|
||||
end
|
||||
|
||||
def notes_finder
|
||||
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
|
||||
end
|
||||
end
|
|
@ -10,6 +10,8 @@ module RendersNotes
|
|||
private
|
||||
|
||||
def preload_max_access_for_authors(notes, project)
|
||||
return nil unless project
|
||||
|
||||
user_ids = notes.map(&:author_id)
|
||||
project.team.max_member_access_for_user_ids(user_ids)
|
||||
end
|
||||
|
|
|
@ -22,7 +22,8 @@ module ToggleAwardEmoji
|
|||
def to_todoable(awardable)
|
||||
case awardable
|
||||
when Note
|
||||
awardable.noteable
|
||||
# we don't create todos for personal snippet comments for now
|
||||
awardable.for_personal_snippet? ? nil : awardable.noteable
|
||||
when MergeRequest, Issue
|
||||
awardable
|
||||
when Snippet
|
||||
|
|
|
@ -1,68 +1,22 @@
|
|||
class Projects::NotesController < Projects::ApplicationController
|
||||
include RendersNotes
|
||||
include NotesActions
|
||||
include ToggleAwardEmoji
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_read_note!
|
||||
before_action :authorize_create_note!, only: [:create]
|
||||
before_action :authorize_admin_note!, only: [:update, :destroy]
|
||||
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
|
||||
|
||||
def index
|
||||
current_fetched_at = Time.now.to_i
|
||||
|
||||
notes_json = { notes: [], last_fetched_at: current_fetched_at }
|
||||
|
||||
@notes = notes_finder.execute.inc_relations_for_view
|
||||
@notes = prepare_notes_for_rendering(@notes)
|
||||
|
||||
@notes.each do |note|
|
||||
next if note.cross_reference_not_visible_for?(current_user)
|
||||
|
||||
notes_json[:notes] << note_json(note)
|
||||
end
|
||||
|
||||
render json: notes_json
|
||||
end
|
||||
|
||||
#
|
||||
# This is a fix to make spinach feature tests passing:
|
||||
# Controller actions are returned from AbstractController::Base and methods of parent classes are
|
||||
# excluded in order to return only specific controller related methods.
|
||||
# That is ok for the app (no :create method in ancestors)
|
||||
# but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
|
||||
#
|
||||
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
|
||||
#
|
||||
def create
|
||||
create_params = note_params.merge(
|
||||
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
|
||||
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
|
||||
)
|
||||
@note = Notes::CreateService.new(project, current_user, create_params).execute
|
||||
|
||||
if @note.is_a?(Note)
|
||||
Banzai::NoteRenderer.render([@note], @project, current_user)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: note_json(@note) }
|
||||
format.html { redirect_back_or_default }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
|
||||
|
||||
if @note.is_a?(Note)
|
||||
Banzai::NoteRenderer.render([@note], @project, current_user)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json { render json: note_json(@note) }
|
||||
format.html { redirect_back_or_default }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if note.editable?
|
||||
Notes::DestroyService.new(project, current_user).execute(note)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.js { head :ok }
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def delete_attachment
|
||||
|
@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
|
||||
def note_html(note)
|
||||
render_to_string(
|
||||
"projects/notes/_note",
|
||||
"shared/notes/_note",
|
||||
layout: false,
|
||||
formats: [:html],
|
||||
locals: { note: note }
|
||||
|
@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def note_json(note)
|
||||
attrs = {
|
||||
commands_changes: note.commands_changes
|
||||
}
|
||||
|
||||
if note.persisted?
|
||||
attrs.merge!(
|
||||
valid: true,
|
||||
id: note.id,
|
||||
discussion_id: note.discussion_id(noteable),
|
||||
html: note_html(note),
|
||||
note: note.note
|
||||
)
|
||||
|
||||
discussion = note.to_discussion(noteable)
|
||||
unless discussion.individual_note?
|
||||
attrs.merge!(
|
||||
discussion_resolvable: discussion.resolvable?,
|
||||
|
||||
diff_discussion_html: diff_discussion_html(discussion),
|
||||
discussion_html: discussion_html(discussion)
|
||||
)
|
||||
end
|
||||
else
|
||||
attrs.merge!(
|
||||
valid: false,
|
||||
errors: note.errors
|
||||
)
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def authorize_admin_note!
|
||||
return access_denied! unless can?(current_user, :admin_note, note)
|
||||
def finder_params
|
||||
params.merge(last_fetched_at: last_fetched_at)
|
||||
end
|
||||
|
||||
def authorize_resolve_note!
|
||||
return access_denied! unless can?(current_user, :resolve_note, note)
|
||||
end
|
||||
|
||||
def note_params
|
||||
params.require(:note).permit(
|
||||
:project_id,
|
||||
:noteable_type,
|
||||
:noteable_id,
|
||||
:commit_id,
|
||||
:noteable,
|
||||
:type,
|
||||
|
||||
:note,
|
||||
:attachment,
|
||||
|
||||
# LegacyDiffNote
|
||||
:line_code,
|
||||
|
||||
# DiffNote
|
||||
:position
|
||||
)
|
||||
end
|
||||
|
||||
def notes_finder
|
||||
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
|
||||
end
|
||||
|
||||
def noteable
|
||||
@noteable ||= notes_finder.target
|
||||
end
|
||||
|
||||
def last_fetched_at
|
||||
request.headers['X-Last-Fetched-At']
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
class Snippets::NotesController < ApplicationController
|
||||
include NotesActions
|
||||
include ToggleAwardEmoji
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:index]
|
||||
before_action :snippet
|
||||
before_action :authorize_read_snippet!, only: [:show, :index, :create]
|
||||
|
||||
private
|
||||
|
||||
def note
|
||||
@note ||= snippet.notes.find(params[:id])
|
||||
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
|
||||
|
||||
def snippet
|
||||
PersonalSnippet.find_by(id: params[:snippet_id])
|
||||
end
|
||||
|
||||
def note_params
|
||||
super.merge(noteable_id: params[:snippet_id])
|
||||
end
|
||||
|
||||
def finder_params
|
||||
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
|
||||
end
|
||||
|
||||
def authorize_read_snippet!
|
||||
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
|
||||
end
|
||||
end
|
|
@ -1,4 +1,5 @@
|
|||
class SnippetsController < ApplicationController
|
||||
include RendersNotes
|
||||
include ToggleAwardEmoji
|
||||
include SpammableActions
|
||||
include SnippetsActions
|
||||
|
@ -64,6 +65,11 @@ class SnippetsController < ApplicationController
|
|||
blob = @snippet.blob
|
||||
override_max_blob_size(blob)
|
||||
|
||||
@noteable = @snippet
|
||||
|
||||
@discussions = @snippet.discussions
|
||||
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render 'show'
|
||||
|
|
|
@ -68,6 +68,8 @@ class NotesFinder
|
|||
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
|
||||
when "snippet", "project_snippet"
|
||||
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
|
||||
when "personal_snippet"
|
||||
PersonalSnippet.all
|
||||
else
|
||||
raise 'invalid target_type'
|
||||
end
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
module AwardEmojiHelper
|
||||
def toggle_award_url(awardable)
|
||||
return url_for([:toggle_award_emoji, awardable]) unless @project
|
||||
return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
|
||||
|
||||
if awardable.is_a?(Note)
|
||||
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
|
||||
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
|
||||
if awardable.for_personal_snippet?
|
||||
toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
|
||||
else
|
||||
toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
|
||||
end
|
||||
else
|
||||
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
|
||||
end
|
||||
|
|
|
@ -281,7 +281,7 @@ class TodoService
|
|||
|
||||
def attributes_for_target(target)
|
||||
attributes = {
|
||||
project_id: target.project.id,
|
||||
project_id: target&.project&.id,
|
||||
target_id: target.id,
|
||||
target_type: target.class.name,
|
||||
commit_id: nil
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.discussion-notes
|
||||
%ul.notes{ data: { discussion_id: discussion.id } }
|
||||
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
|
||||
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
|
||||
|
||||
- if current_user
|
||||
.discussion-reply-holder
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
- access = note_max_access_for_user(note)
|
||||
- if access
|
||||
%span.note-role= access
|
||||
|
||||
- if note.resolvable?
|
||||
- can_resolve = can?(current_user, :resolve_note, note)
|
||||
%resolve-btn{ "project-path" => project_path(note.project),
|
||||
"discussion-id" => note.discussion_id(@noteable),
|
||||
":note-id" => note.id,
|
||||
":resolved" => note.resolved?,
|
||||
":can-resolve" => can_resolve,
|
||||
":author-name" => "'#{j(note.author.name)}'",
|
||||
"author-avatar" => note.author.avatar_url,
|
||||
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
|
||||
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
|
||||
"v-show" => "#{can_resolve || note.resolved?}",
|
||||
"inline-template" => true,
|
||||
"ref" => "note_#{note.id}" }
|
||||
|
||||
%button.note-action-button.line-resolve-btn{ type: "button",
|
||||
class: ("is-disabled" unless can_resolve),
|
||||
":class" => "{ 'is-active': isResolved }",
|
||||
":aria-label" => "buttonText",
|
||||
"@click" => "resolve",
|
||||
":title" => "buttonText",
|
||||
":ref" => "'button'" }
|
||||
|
||||
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
|
||||
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
|
||||
|
||||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
- user_authored = note.user_authored?(current_user)
|
||||
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
|
||||
= icon('spinner spin')
|
||||
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
|
||||
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
|
||||
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
|
||||
|
||||
- if note_editable
|
||||
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
|
||||
= icon('pencil', class: 'link-highlight')
|
||||
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
|
||||
= icon('trash-o', class: 'danger-highlight')
|
|
@ -0,0 +1,3 @@
|
|||
.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
|
|
@ -1,101 +0,0 @@
|
|||
- return unless note.author
|
||||
- return if note.cross_reference_not_visible_for?(current_user)
|
||||
|
||||
- note_editable = note_editable?(note)
|
||||
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
|
||||
.timeline-entry-inner
|
||||
.timeline-icon
|
||||
- if note.system
|
||||
= icon_for_system_note(note)
|
||||
- else
|
||||
%a{ href: user_path(note.author) }
|
||||
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
|
||||
.timeline-content
|
||||
.note-header
|
||||
.note-header-info
|
||||
%a{ href: user_path(note.author) }
|
||||
%span.hidden-xs
|
||||
= sanitize(note.author.name)
|
||||
%span.note-headline-light
|
||||
= note.author.to_reference
|
||||
%span.note-headline-light
|
||||
%span.note-headline-meta
|
||||
- unless note.system
|
||||
commented
|
||||
- if note.system
|
||||
%span.system-note-message
|
||||
= note.redacted_note_html
|
||||
%a{ href: "##{dom_id(note)}" }
|
||||
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
|
||||
- unless note.system?
|
||||
.note-actions
|
||||
- access = note_max_access_for_user(note)
|
||||
- if access
|
||||
%span.note-role= access
|
||||
|
||||
- if note.resolvable?
|
||||
- can_resolve = can?(current_user, :resolve_note, note)
|
||||
%resolve-btn{ "project-path" => project_path(note.project),
|
||||
"discussion-id" => note.discussion_id(@noteable),
|
||||
":note-id" => note.id,
|
||||
":resolved" => note.resolved?,
|
||||
":can-resolve" => can_resolve,
|
||||
":author-name" => "'#{j(note.author.name)}'",
|
||||
"author-avatar" => note.author.avatar_url,
|
||||
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
|
||||
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
|
||||
"v-show" => "#{can_resolve || note.resolved?}",
|
||||
"inline-template" => true,
|
||||
"ref" => "note_#{note.id}" }
|
||||
|
||||
%button.note-action-button.line-resolve-btn{ type: "button",
|
||||
class: ("is-disabled" unless can_resolve),
|
||||
":class" => "{ 'is-active': isResolved }",
|
||||
":aria-label" => "buttonText",
|
||||
"@click" => "resolve",
|
||||
":title" => "buttonText",
|
||||
":ref" => "'button'" }
|
||||
|
||||
= icon("spin spinner", "v-show" => "loading", class: 'loading')
|
||||
%div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
|
||||
|
||||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
- user_authored = note.user_authored?(current_user)
|
||||
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
|
||||
= icon('spinner spin')
|
||||
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
|
||||
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
|
||||
%span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
|
||||
|
||||
- if note_editable
|
||||
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
|
||||
= icon('pencil', class: 'link-highlight')
|
||||
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
|
||||
= icon('trash-o', class: 'danger-highlight')
|
||||
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
|
||||
.note-text.md
|
||||
= note.redacted_note_html
|
||||
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
|
||||
- if note_editable
|
||||
.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
|
||||
.note-awards
|
||||
= render 'award_emoji/awards_block', awardable: note, inline: false
|
||||
- if note.system
|
||||
.system-note-commit-list-toggler
|
||||
Toggle commit list
|
||||
%i.fa.fa-angle-down
|
||||
- if note.attachment.url
|
||||
.note-attachment
|
||||
- if note.attachment.image?
|
||||
= link_to note.attachment.url, target: '_blank' do
|
||||
= image_tag note.attachment.url, class: 'note-image-attach'
|
||||
.attachment
|
||||
= link_to note.attachment.url, target: '_blank' do
|
||||
= icon('paperclip')
|
||||
= note.attachment_identifier
|
||||
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
|
||||
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
|
||||
= icon('trash-o', class: 'cred')
|
|
@ -1,5 +1,5 @@
|
|||
%ul#notes-list.notes.main-notes-list.timeline
|
||||
= render "projects/notes/notes"
|
||||
= render "shared/notes/notes"
|
||||
|
||||
= render 'projects/notes/edit_form'
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
- return unless note.author
|
||||
- return if note.cross_reference_not_visible_for?(current_user)
|
||||
|
||||
- note_editable = note_editable?(note)
|
||||
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
|
||||
.timeline-entry-inner
|
||||
.timeline-icon
|
||||
- if note.system
|
||||
= icon_for_system_note(note)
|
||||
- else
|
||||
%a{ href: user_path(note.author) }
|
||||
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
|
||||
.timeline-content
|
||||
.note-header
|
||||
.note-header-info
|
||||
%a{ href: user_path(note.author) }
|
||||
%span.hidden-xs
|
||||
= sanitize(note.author.name)
|
||||
%span.note-headline-light
|
||||
= note.author.to_reference
|
||||
%span.note-headline-light
|
||||
%span.note-headline-meta
|
||||
- unless note.system
|
||||
commented
|
||||
- if note.system
|
||||
%span.system-note-message
|
||||
= note.redacted_note_html
|
||||
%a{ href: "##{dom_id(note)}" }
|
||||
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
|
||||
- unless note.system?
|
||||
.note-actions
|
||||
- if note.for_personal_snippet?
|
||||
= render 'snippets/notes/actions', note: note, note_editable: note_editable
|
||||
- else
|
||||
= render 'projects/notes/actions', note: note, note_editable: note_editable
|
||||
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
|
||||
.note-text.md
|
||||
= 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
|
||||
.note-awards
|
||||
= render 'award_emoji/awards_block', awardable: note, inline: false
|
||||
- if note.system
|
||||
.system-note-commit-list-toggler
|
||||
Toggle commit list
|
||||
%i.fa.fa-angle-down
|
||||
- if note.attachment.url
|
||||
.note-attachment
|
||||
- if note.attachment.image?
|
||||
= link_to note.attachment.url, target: '_blank' do
|
||||
= image_tag note.attachment.url, class: 'note-image-attach'
|
||||
.attachment
|
||||
= link_to note.attachment.url, target: '_blank' do
|
||||
= icon('paperclip')
|
||||
= note.attachment_identifier
|
||||
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
|
||||
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
|
||||
= icon('trash-o', class: 'cred')
|
|
@ -1,8 +1,8 @@
|
|||
- if defined?(@discussions)
|
||||
- @discussions.each do |discussion|
|
||||
- if discussion.individual_note?
|
||||
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
|
||||
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
|
||||
- else
|
||||
= render 'discussions/discussion', discussion: discussion
|
||||
- else
|
||||
= render partial: "projects/notes/note", collection: @notes, as: :note
|
||||
= render partial: "shared/notes/note", collection: @notes, as: :note
|
|
@ -0,0 +1,13 @@
|
|||
- if current_user
|
||||
- if note.emoji_awardable?
|
||||
- user_authored = note.user_authored?(current_user)
|
||||
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
|
||||
= icon('spinner spin')
|
||||
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
|
||||
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
|
||||
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
|
||||
- if note_editable
|
||||
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
|
||||
= icon('pencil', class: 'link-highlight')
|
||||
= link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
|
||||
= icon('trash-o', class: 'danger-highlight')
|
|
@ -0,0 +1,2 @@
|
|||
%ul#notes-list.notes.main-notes-list.timeline
|
||||
= render "projects/notes/notes"
|
|
@ -7,3 +7,6 @@
|
|||
|
||||
.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'
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Display comments for personal snippets
|
||||
merge_request:
|
||||
author:
|
|
@ -5,6 +5,14 @@ resources :snippets, concerns: :awardable do
|
|||
post :mark_as_spam
|
||||
post :preview_markdown
|
||||
end
|
||||
|
||||
scope module: :snippets do
|
||||
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
|
||||
member do
|
||||
delete :delete_attachment
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get '/s/:username', to: redirect('/u/%{username}/snippets'),
|
||||
|
|
|
@ -167,6 +167,47 @@ describe Projects::NotesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
let(:request_params) do
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: note,
|
||||
format: :js
|
||||
}
|
||||
end
|
||||
|
||||
context 'user is the author of a note' do
|
||||
before do
|
||||
sign_in(note.author)
|
||||
project.team << [note.author, :developer]
|
||||
end
|
||||
|
||||
it "returns status 200 for html" do
|
||||
delete :destroy, request_params
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "deletes the note" do
|
||||
expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'user is not the author of a note' do
|
||||
before do
|
||||
sign_in(user)
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
it "returns status 404" do
|
||||
delete :destroy, request_params
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST toggle_award_emoji' do
|
||||
before do
|
||||
sign_in(user)
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Snippets::NotesController do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
let(:private_snippet) { create(:personal_snippet, :private) }
|
||||
let(:internal_snippet) { create(:personal_snippet, :internal) }
|
||||
let(:public_snippet) { create(:personal_snippet, :public) }
|
||||
|
||||
let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) }
|
||||
let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) }
|
||||
let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) }
|
||||
|
||||
describe 'GET index' do
|
||||
context 'when a snippet is public' do
|
||||
before do
|
||||
note_on_public
|
||||
|
||||
get :index, { snippet_id: public_snippet }
|
||||
end
|
||||
|
||||
it "returns status 200" do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "returns not empty array of notes" do
|
||||
expect(JSON.parse(response.body)["notes"].empty?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a snippet is internal' do
|
||||
before do
|
||||
note_on_internal
|
||||
end
|
||||
|
||||
context 'when user not logged in' do
|
||||
it "returns status 404" do
|
||||
get :index, { snippet_id: internal_snippet }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user logged in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "returns status 200" do
|
||||
get :index, { snippet_id: internal_snippet }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a snippet is private' do
|
||||
before do
|
||||
note_on_private
|
||||
end
|
||||
|
||||
context 'when user not logged in' do
|
||||
it "returns status 404" do
|
||||
get :index, { snippet_id: private_snippet }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user other than author logged in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it "returns status 404" do
|
||||
get :index, { snippet_id: private_snippet }
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when author logged in' do
|
||||
before do
|
||||
note_on_private
|
||||
|
||||
sign_in(private_snippet.author)
|
||||
end
|
||||
|
||||
it "returns status 200" do
|
||||
get :index, { snippet_id: private_snippet }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "returns 1 note" do
|
||||
get :index, { snippet_id: private_snippet }
|
||||
|
||||
expect(JSON.parse(response.body)['notes'].count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'dont show non visible notes' do
|
||||
before do
|
||||
note_on_public
|
||||
|
||||
sign_in(user)
|
||||
|
||||
expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true)
|
||||
end
|
||||
|
||||
it "does not return any note" do
|
||||
get :index, { snippet_id: public_snippet }
|
||||
|
||||
expect(JSON.parse(response.body)['notes'].count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE destroy' do
|
||||
let(:request_params) do
|
||||
{
|
||||
snippet_id: public_snippet,
|
||||
id: note_on_public,
|
||||
format: :js
|
||||
}
|
||||
end
|
||||
|
||||
context 'when user is the author of a note' do
|
||||
before do
|
||||
sign_in(note_on_public.author)
|
||||
end
|
||||
|
||||
it "returns status 200" do
|
||||
delete :destroy, request_params
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "deletes the note" do
|
||||
expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0)
|
||||
end
|
||||
|
||||
context 'system note' do
|
||||
before do
|
||||
expect_any_instance_of(Note).to receive(:system?).and_return(true)
|
||||
end
|
||||
|
||||
it "does not delete the note" do
|
||||
expect{ delete :destroy, request_params }.not_to change{ Note.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not the author of a note' do
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
note_on_public
|
||||
end
|
||||
|
||||
it "returns status 404" do
|
||||
delete :destroy, request_params
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
||||
it "does not update the note" do
|
||||
expect{ delete :destroy, request_params }.not_to change{ Note.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST toggle_award_emoji' do
|
||||
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") }
|
||||
|
||||
it "toggles the award emoji" do
|
||||
expect { subject }.to change { note.award_emoji.count }.by(1)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it "removes the already awarded emoji when it exists" do
|
||||
note.toggle_award_emoji('thumbsup', user) # create award emoji before
|
||||
|
||||
expect { subject }.to change { AwardEmoji.count }.by(-1)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ include ActionDispatch::TestProcess
|
|||
FactoryGirl.define do
|
||||
factory :note do
|
||||
project factory: :empty_project
|
||||
note "Note"
|
||||
note { generate(:title) }
|
||||
author
|
||||
on_issue
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Comments on personal snippets', feature: true do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:snippet) { create(:personal_snippet, :public) }
|
||||
let!(:snippet_notes) do
|
||||
[
|
||||
create(:note_on_personal_snippet, noteable: snippet, author: user),
|
||||
create(:note_on_personal_snippet, noteable: snippet)
|
||||
]
|
||||
end
|
||||
let!(:other_note) { create(:note_on_personal_snippet) }
|
||||
|
||||
before do
|
||||
login_as user
|
||||
visit snippet_path(snippet)
|
||||
end
|
||||
|
||||
subject { page }
|
||||
|
||||
context '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)
|
||||
|
||||
# comment authored by current user
|
||||
page.within("#notes-list li#note_#{snippet_notes[0].id}") do
|
||||
expect(page).to have_content(snippet_notes[0].note)
|
||||
expect(page).to have_selector('.js-note-delete')
|
||||
expect(page).to have_selector('.note-emoji-button')
|
||||
end
|
||||
|
||||
page.within("#notes-list li#note_#{snippet_notes[1].id}") do
|
||||
expect(page).to have_content(snippet_notes[1].note)
|
||||
expect(page).not_to have_selector('.js-note-delete')
|
||||
expect(page).to have_selector('.note-emoji-button')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -110,6 +110,15 @@ describe NotesFinder do
|
|||
expect(notes.count).to eq(1)
|
||||
end
|
||||
|
||||
it 'finds notes on personal snippets' do
|
||||
note = create(:note_on_personal_snippet)
|
||||
params = { target_type: 'personal_snippet', target_id: note.noteable_id }
|
||||
|
||||
notes = described_class.new(project, user, params).execute
|
||||
|
||||
expect(notes.count).to eq(1)
|
||||
end
|
||||
|
||||
it 'raises an exception for an invalid target_type' do
|
||||
params[:target_type] = 'invalid'
|
||||
expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AwardEmojiHelper do
|
||||
describe '.toggle_award_url' do
|
||||
context 'note on personal snippet' do
|
||||
let(:note) { create(:note_on_personal_snippet) }
|
||||
|
||||
it 'returns correct url' do
|
||||
expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji"
|
||||
|
||||
expect(helper.toggle_award_url(note)).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'note on project item' do
|
||||
let(:note) { create(:note_on_project_snippet) }
|
||||
|
||||
it 'returns correct url' do
|
||||
@project = note.noteable.project
|
||||
|
||||
expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji"
|
||||
|
||||
expect(helper.toggle_award_url(note)).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'personal snippet' do
|
||||
let(:snippet) { create(:personal_snippet) }
|
||||
|
||||
it 'returns correct url' do
|
||||
expected_url = "/snippets/#{snippet.id}/toggle_award_emoji"
|
||||
|
||||
expect(helper.toggle_award_url(snippet)).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'merge request' do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
|
||||
it 'returns correct url' do
|
||||
@project = merge_request.project
|
||||
|
||||
expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji"
|
||||
|
||||
expect(helper.toggle_award_url(merge_request)).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'issue' do
|
||||
let(:issue) { create(:issue) }
|
||||
|
||||
it 'returns correct url' do
|
||||
@project = issue.project
|
||||
|
||||
expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji"
|
||||
|
||||
expect(helper.toggle_award_url(issue)).to eq(expected_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue