Commit 4787644f authored by Fatih Acet's avatar Fatih Acet

Merge branch 'ee-jprovazn-comment-thread' into 'master'

[EE] Epic comment thread

Closes gitlab-ce#42148

See merge request gitlab-org/gitlab-ee!4997
parents d0a8a2db 4b1d1a7e
......@@ -4,7 +4,7 @@ import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
......@@ -300,7 +300,7 @@ class AwardsHandler {
}
isInVueNoteablePage() {
return isInIssuePage() || this.isVueMRDiscussions();
return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
}
getVotesBlock() {
......
......@@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
......
......@@ -99,6 +99,10 @@ export default {
'js-note-target-reopen': !this.isOpen,
};
},
supportQuickActions() {
// Disable quick actions support for Epics
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
},
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
......@@ -355,7 +359,7 @@ Please check your network connection and try again.`;
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
data-supports-quick-actions="true"
:data-supports-quick-actions="supportQuickActions"
aria-label="Description"
v-model="note"
ref="textarea"
......
......@@ -50,7 +50,11 @@ export default {
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
return EPIC_NOTEABLE_TYPE;
}
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
......
......@@ -10,6 +10,7 @@ export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
export const EPIC_NOTEABLE_TYPE = 'epic';
export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post';
......@@ -12,8 +12,11 @@ document.addEventListener(
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id,
......@@ -25,7 +28,7 @@ document.addEventListener(
}
return {
noteableData: JSON.parse(notesDataset.noteableData),
noteableData,
currentUserData,
notesData: JSON.parse(notesDataset.notesData),
};
......
......@@ -14,6 +14,8 @@ export default {
return constants.MERGE_REQUEST_NOTEABLE_TYPE;
case 'Issue':
return constants.ISSUE_NOTEABLE_TYPE;
case 'Epic':
return constants.EPIC_NOTEABLE_TYPE;
default:
return '';
}
......
......@@ -88,11 +88,15 @@ module IssuableActions
discussions = Discussion.build_collection(notes, issuable)
render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self)
render json: discussion_serializer.represent(discussions, context: self)
end
private
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
def recaptcha_check_if_spammable(should_redirect = true, &block)
return yield unless issuable.is_a? Spammable
......
......@@ -212,7 +212,7 @@ module NotesActions
end
def note_serializer
NoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
end
def note_project
......
......@@ -43,7 +43,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
def render_json_with_discussions_serializer
render json:
DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user)
DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity)
.represent(discussion, context: self)
end
......
class NotesFinder
prepend EE::NotesFinder
FETCH_OVERLAP = 5.seconds
# Used to filter Notes
......
module AwardEmojiHelper
prepend EE::AwardEmojiHelper
def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
......
module NotesHelper
prepend EE::NotesHelper
def note_target_fields(note)
if note.noteable
hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
......@@ -151,16 +153,17 @@ module NotesHelper
}
end
def notes_data(issuable)
discussions_path =
if issuable.is_a?(Issue)
discussions_project_issue_path(@project, issuable, format: :json)
else
discussions_project_merge_request_path(@project, issuable, format: :json)
end
def discussions_path(issuable)
if issuable.is_a?(Issue)
discussions_project_issue_path(@project, issuable, format: :json)
else
discussions_project_merge_request_path(@project, issuable, format: :json)
end
end
def notes_data(issuable)
{
discussionsPath: discussions_path,
discussionsPath: discussions_path(issuable),
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'),
......@@ -170,7 +173,6 @@ module NotesHelper
notesPath: notes_url,
totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now.to_i
}.to_json
end
......
......@@ -387,12 +387,15 @@ class Note < ActiveRecord::Base
def expire_etag_cache
return unless noteable&.discussions_rendered_on_frontend?
key = Gitlab::Routing.url_helpers.project_noteable_notes_path(
Gitlab::EtagCaching::Store.new.touch(etag_key)
end
def etag_key
Gitlab::Routing.url_helpers.project_noteable_notes_path(
project,
target_type: noteable_type.underscore,
target_id: noteable_id
)
Gitlab::EtagCaching::Store.new.touch(key)
end
def touch(*args)
......
......@@ -4,7 +4,9 @@ class DiscussionEntity < Grape::Entity
expose :id, :reply_id
expose :expanded?, as: :expanded
expose :notes, using: NoteEntity
expose :notes do |discussion, opts|
request.note_entity.represent(discussion.notes, opts)
end
expose :individual_note?, as: :individual_note
expose :resolvable?, as: :resolvable
......@@ -12,7 +14,7 @@ class DiscussionEntity < Grape::Entity
expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
end
expose :resolve_with_issue_path do |discussion|
expose :resolve_with_issue_path, if: -> (d, _) { d.resolvable? } do |discussion|
new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
end
......
......@@ -5,10 +5,6 @@ class NoteEntity < API::Entities::Note
expose :author, using: NoteUserEntity
expose :human_access do |note|
note.project.team.human_max_access(note.author_id)
end
unexpose :note, as: :body
expose :note
......@@ -37,36 +33,10 @@ class NoteEntity < API::Entities::Note
expose :emoji_awardable?, as: :emoji_awardable
expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
if note.for_personal_snippet?
toggle_award_emoji_snippet_note_path(note.noteable, note)
else
toggle_award_emoji_project_note_path(note.project, note.id)
end
end
expose :report_abuse_path do |note|
new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
end
expose :path do |note|
if note.for_personal_snippet?
snippet_note_path(note.noteable, note)
else
project_note_path(note.project, note)
end
end
expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
end
expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
end
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
delete_attachment_project_note_path(note.project, note)
end
end
class NoteSerializer < BaseSerializer
entity NoteEntity
end
class ProjectNoteEntity < NoteEntity
expose :human_access do |note|
note.project.team.human_max_access(note.author_id)
end
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
toggle_award_emoji_project_note_path(note.project, note.id)
end
expose :path do |note|
project_note_path(note.project, note)
end
expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
end
expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
end
expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
delete_attachment_project_note_path(note.project, note)
end
end
class ProjectNoteSerializer < BaseSerializer
entity ProjectNoteEntity
end
......@@ -82,12 +82,17 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
resources :billings, only: [:index]
resources :epics do
resources :epics, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :discussions, format: :json
get :realtime_changes
end
resources :epic_issues, only: [:index, :create, :destroy, :update], as: 'issues', path: 'issues'
scope module: :epics do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ }
end
end
# On CE only index and show are needed
......
......@@ -97,3 +97,20 @@ You may also consult the [group permissions table][permissions].
[ee]: https://about.gitlab.com/products/
[permissions]: ../../permissions.md#group-members-permissions
## Thread
- Comments: collaborate on that epic by posting comments in its thread.
These text fields also fully support
[GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
## Comment, or start a discussion
Once you wrote your comment, you can either:
- Click "Comment" and your comment will be published.
- Click "Start discussion": start a thread within that epic's thread to discuss specific points.
## Award emoji
- You can [award an emoji](../../award_emojis.md) to that epic or its comments.
import ZenMode from '~/zen_mode';
import initEpicShow from 'ee/epics/epic_show/epic_show_bundle';
import '~/notes/index';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
......
class Groups::Epics::NotesController < Groups::ApplicationController
include NotesActions
include NotesHelper
include ToggleAwardEmoji
before_action :epic
before_action :authorize_create_note!, only: [:create]
private
def project
nil
end
def note
@note ||= noteable.notes.find(params[:id])
end
alias_method :awardable, :note
def epic
@epic ||= @group.epics.find_by(iid: params[:epic_id])
return render_404 unless can?(current_user, :read_epic, @epic)
@epic
end
alias_method :noteable, :epic
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: epic.id, target_type: 'epic', group_id: @group.id)
end
def authorize_create_note!
access_denied! unless can?(current_user, :create_note, noteable)
end
def note_serializer
EpicNoteSerializer.new(project: nil, noteable: noteable, current_user: current_user)
end
end
class Groups::EpicsController < Groups::ApplicationController
include IssuableActions
include IssuableCollections
include ToggleAwardEmoji
include RendersNotes
before_action :check_epics_available!
before_action :epic, except: [:index, :create]
......@@ -23,7 +25,7 @@ class Groups::EpicsController < Groups::ApplicationController
end
def create
@epic = Epics::CreateService.new(@group, current_user, epic_params).execute
@epic = ::Epics::CreateService.new(@group, current_user, epic_params).execute
if @epic.persisted?
render json: {
......@@ -48,6 +50,7 @@ class Groups::EpicsController < Groups::ApplicationController
@epic
end
alias_method :issuable, :epic
alias_method :awardable, :epic
def epic_params
params.require(:epic).permit(*epic_params_attributes)
......@@ -67,8 +70,12 @@ class Groups::EpicsController < Groups::ApplicationController
EpicSerializer.new(current_user: current_user)
end
def discussion_serializer
DiscussionSerializer.new(project: nil, noteable: issuable, current_user: current_user, note_entity: EpicNoteEntity)
end
def update_service
Epics::UpdateService.new(@group, current_user, epic_params)
::Epics::UpdateService.new(@group, current_user, epic_params)
end
def finder_type
......
module EE
module NotesFinder
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :noteables_for_type
def noteables_for_type(noteable_type)
if noteable_type == "epic"
return EpicsFinder.new(@current_user, group_id: @params[:group_id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
super
end
end
end
module EE
module AwardEmojiHelper
extend ::Gitlab::Utils::Override
override :toggle_award_url
def toggle_award_url(awardable)
if awardable.is_a?(Note) && awardable.for_epic?
return toggle_award_emoji_group_epic_note_path(awardable.noteable.group, awardable.noteable, awardable)
elsif awardable.is_a?(Epic)
return toggle_award_emoji_group_epic_path(awardable.group, awardable)
end
super
end
end
end
module EE
module NotesHelper
extend ::Gitlab::Utils::Override
override :notes_url
def notes_url(params = {})
return group_epic_notes_path(@epic.group, @epic) if @epic.is_a?(Epic)
super
end
override :discussions_path
def discussions_path(issuable)
return discussions_group_epic_path(issuable.group, issuable, format: :json) if issuable.is_a?(Epic)
super
end
end
end
......@@ -7,6 +7,7 @@ module EE
include Issuable
include Noteable
include Referable
include Awardable
belongs_to :assignee, class_name: "User"
belongs_to :group
......@@ -115,5 +116,9 @@ module EE
def mentionable_params
{ group: group }
end
def discussions_rendered_on_frontend?
true
end
end
end
......@@ -7,7 +7,6 @@ module EE
include ObjectStorage::BackgroundMove
end
override :for_project_noteable?
def for_epic?
noteable.is_a?(Epic)
end
......@@ -21,5 +20,21 @@ module EE
def can_create_todo?
!for_epic? && super
end
override :etag_key
def etag_key
if for_epic?
return ::Gitlab::Routing.url_helpers.group_epic_notes_path(noteable.group, noteable)
end
super
end
override :banzai_render_context
def banzai_render_context(field)
return super unless for_epic?
super.merge(group: noteable.group)
end
end
end
......@@ -12,4 +12,18 @@ class EpicEntity < IssuableEntity
group_epic_path(epic.group, epic)
end
expose :labels, using: LabelEntity
expose :current_user do
expose :can_create_note do |epic|
can?(request.current_user, :create_note, epic)
end
end
expose :create_note_path do |epic|
group_epic_notes_path(epic.group, epic)
end
expose :preview_note_path do |epic|
preview_markdown_path(epic.group, quick_actions_target_type: 'Epic', quick_actions_target_id: epic.id)
end
end
class EpicNoteEntity < NoteEntity
expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
toggle_award_emoji_group_epic_note_path(note.noteable.group, note.noteable, note)
end
expose :path do |note|
group_epic_note_path(note.noteable.group, note.noteable, note)
end
end
class EpicNoteSerializer < BaseSerializer
entity EpicNoteEntity
end
- @gfm_form = true
%section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@epic),
noteable_data: EpicSerializer.new(current_user: current_user).represent(@epic).to_json,
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json, noteable_type: 'epic' } }
......@@ -13,3 +13,11 @@
- page_card_attributes @epic.card_attributes
#epic-show-app{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
.content-block.emoji-block
.row
.col-sm-8.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @epic, inline: true
%section.issuable-discussion
= render 'discussion'
---
title: Add comment thread to Epics
merge_request:
author:
type: added
module EE
module Gitlab
module EtagCaching
module Router
module ClassMethods
def match(path)
epic_route = ::Gitlab::EtagCaching::Router::Route.new(
%r(^/groups/#{::Gitlab::PathRegex.full_namespace_route_regex}/-/epics/\d+/notes\z),
'epic_notes'
)
return epic_route if epic_route.regexp.match(path)
super
end
end
def self.prepended(base)
base.singleton_class.prepend ClassMethods
end
end
end
end
end
require 'spec_helper'
describe Groups::Epics::NotesController do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:epic) { create(:epic, group: group) }
let(:note) { create(:note, noteable: epic) }
let(:parsed_response) { JSON.parse(response.body).with_indifferent_access }
before do
stub_licensed_features(epics: true)
end
describe 'GET index' do
let(:request_params) do
{
group_id: group,
epic_id: epic.iid,
format: 'json'
}
end
let(:note_json) { parsed_response[:notes].first }
before do
group.add_developer(user)
sign_in(user)
note
end
it 'responds with array of notes' do
get :index, request_params
expect(parsed_response[:notes]).to be_an Array
expect(parsed_response[:notes].count).to eq(1)
end
context 'with cross-reference system note that is not visible to the current user', :request_store do
it "does not return any note" do
expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true)
get :index, request_params
expect(parsed_response[:notes].count).to eq(0)
end
end
end
describe 'POST create' do
let(:request_params) do
{
note: { note: 'some note', noteable_id: epic.id, noteable_type: 'Epic' },
group_id: group,
epic_id: epic.iid,
format: 'json'
}
end
before do
sign_in(user)
group.add_developer(user)
end
it "returns status 302 for html" do
post :create, request_params.merge(format: :html)
expect(response).to have_gitlab_http_status(302)
end
it "returns status 200 for json" do
post :create, request_params
expect(response).to have_gitlab_http_status(200)
expect(parsed_response[:id]).not_to be_nil
end
end
describe 'PUT update' do
let(:request_params) do
{
note: { note: 'updated note', noteable_id: epic.id, noteable_type: 'Epic' },
group_id: group,
epic_id: epic.iid,
id: note.id,
format: 'json'
}
end
before do
sign_in(note.author)
end
it "updates the note" do
expect { put :update, request_params }.to change { note.reload.note }
end
end
describe 'DELETE destroy' do
let(:request_params) do
{
group_id: group,
epic_id: epic.iid,
id: note.id,
format: 'js'
}
end
before do
group.add_developer(user)
end
context 'user is the author of a note' do
before do
sign_in(note.author)
end
it "returns status 200" do
delete :destroy, request_params
expect(response).to have_gitlab_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 the note' do
before do
sign_in(user)
end
it "returns status 404" do
delete :destroy, request_params
expect(response).to have_gitlab_http_status(404)
end
end
end
describe 'POST toggle_award_emoji' do
let(:request_params) do
{
group_id: group,
epic_id: epic,
id: note.id
}
end
before do
group.add_developer(user)
sign_in(user)
end
it "toggles the award emoji" do
expect do
post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { note.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
it "removes the already awarded emoji" do
post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
expect do
post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { AwardEmoji.count }.by(-1)
expect(response).to have_gitlab_http_status(200)
end
end
end
require 'spec_helper'
describe 'Discussion Comments Epic', :js do
let(:user) { create(:user) }
let(:epic) { create(:epic) }
before do
stub_licensed_features(epics: true)
epic.group.add_master(user)
sign_in(user)
visit group_epic_path(epic.group, epic)
end
it_behaves_like 'discussion comments', 'epic'
end
......@@ -46,7 +46,7 @@ feature 'Update Epic', :js do
fill_in 'issuable-title', with: 'New epic title'
fill_in 'issue-description', with: 'New epic description'
click_link('Preview')
page.within('.detail-page-description') { click_link('Preview') }
expect(find('.md-preview')).to have_content('New epic description')
click_button 'Save changes'
......@@ -56,7 +56,7 @@ feature 'Update Epic', :js do
end
it 'edits full screen' do
find('.js-zen-enter').click
page.within('.detail-page-description') { find('.js-zen-enter').click }
expect(page).to have_selector('.div-dropzone-wrapper.fullscreen')
end
......@@ -68,7 +68,7 @@ feature 'Update Epic', :js do
expect(page.find_field("issue-description").value).to have_content('banana_sample')
click_link('Preview')
page.within('.detail-page-description') { click_link('Preview') }
wait_for_requests
within('.md-preview') do
......
......@@ -49,7 +49,12 @@
"avatar_url": { "type": "uri" }
}
}
}
},
"current_user": {
"can_create_note": { "type": "boolean" }
},
"create_note_path": { "type": "string" },
"preview_note_path": { "type": "string" }
},
"additionalProperties": false
}
require 'spec_helper'
describe Gitlab::EtagCaching::Router do
it 'matches epic notes endpoint' do
result = described_class.match(
'/groups/my-group/and-subgroup/-/epics/1/notes'
)
expect(result).to be_present
expect(result.name).to eq 'epic_notes'
end
it 'does not match invalid epic notes endpoint' do
result = described_class.match(
'/groups/my-group/-/and-subgroup/-/epics/1/notes'
)
expect(result).to be_blank
end
end
require 'spec_helper'
describe EpicNoteEntity do
include Gitlab::Routing
let(:request) { double('request', current_user: user, noteable: note.noteable) }
let(:entity) { described_class.new(note, request: request) }
let(:epic) { create(:epic, author: user) }
let(:note) { create(:note, noteable: epic, author: user) }
let(:user) { create(:user) }
subject { entity.as_json }
it_behaves_like 'note entity'
it 'exposes epic-specific elements' do
expect(subject).to include(:toggle_award_path, :path)
end
end
module Gitlab
module EtagCaching
class Router
prepend EE::Gitlab::EtagCaching::Router
Route = Struct.new(:regexp, :name)
# We enable an ETag for every request matching the regex.
# To match a regex the path needs to match the following:
......
......@@ -974,7 +974,7 @@ describe Projects::IssuesController do
it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolve_with_issue_path resolved])
expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolved])
end
it 'filters notes that the user should not see' do
......
......@@ -6,7 +6,7 @@ describe DiscussionEntity do
let(:user) { create(:user) }
let(:note) { create(:discussion_note_on_merge_request) }
let(:discussion) { note.discussion }
let(:request) { double('request') }
let(:request) { double('request', note_entity: ProjectNoteEntity) }
let(:controller) { double('controller') }
let(:entity) { described_class.new(discussion, request: request, context: controller) }
......
......@@ -10,53 +10,5 @@ describe NoteEntity do
let(:user) { create(:user) }
subject { entity.as_json }
context 'basic note' do
it 'exposes correct elements' do
expect(subject).to include(:type, :author, :human_access, :note, :note_html, :current_user,
:discussion_id, :emoji_awardable, :award_emoji, :toggle_award_path, :report_abuse_path, :path, :attachment)
end
it 'does not expose elements for specific notes cases' do
expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name)
end
it 'exposes author correctly' do
expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path)
end
it 'does not expose web_url for author' do
expect(subject[:author]).not_to include(:web_url)
end
end
context 'when note was edited' do
before do
note.update(updated_at: 1.minute.from_now, updated_by: user)
end
it 'exposes last_edited_at and last_edited_by elements' do
expect(subject).to include(:last_edited_at, :last_edited_by)
end
end
context 'when note is a system note' do
before do
note.update(system: true)
end
it 'exposes system_note_icon_name element' do
expect(subject).to include(:system_note_icon_name)
end
end
context 'when note is part of resolvable discussion' do
before do
allow(note).to receive(:part_of_discussion?).and_return(true)
allow(note).to receive(:resolvable?).and_return(true)
end
it 'exposes paths to resolve note' do
expect(subject).to include(:resolve_path, :resolve_with_issue_path)
end
end
it_behaves_like 'note entity'
end
require 'spec_helper'
describe ProjectNoteEntity do
include Gitlab::Routing
let(:request) { double('request', current_user: user, noteable: note.noteable) }
let(:entity) { described_class.new(note, request: request) }
let(:note) { create(:note) }
let(:user) { create(:user) }
subject { entity.as_json }
it_behaves_like 'note entity'
it 'exposes project-specific elements' do
expect(subject).to include(:human_access, :toggle_award_path, :path)
end
context 'when note is part of resolvable discussion' do
before do
allow(note).to receive(:part_of_discussion?).and_return(true)
allow(note).to receive(:resolvable?).and_return(true)
end
it 'exposes paths to resolve note' do
expect(subject).to include(:resolve_path, :resolve_with_issue_path)
end
end
end
......@@ -81,7 +81,10 @@ shared_examples 'discussion comments' do |resource_name|
# on issues page, the menu closes when clicking anywhere, on other pages it will
# remain open if clicking divider or menu padding, but should not change button action
if resource_name == 'issue'
#
# if dropdown menu is not toggled (and also not present),
# it's "issue-type" dropdown
if first(menu_selector).nil?
expect(find(dropdown_selector)).to have_content 'Comment'
find(toggle_selector).click
......@@ -107,8 +110,10 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
button = find(submit_selector)
# on issues page, the submit input is a <button>, on other pages it is <input>
if resource_name == 'issue'
if button.tag_name == 'button'
expect(find(submit_selector)).to have_content 'Start discussion'
else
expect(find(submit_selector).value).to eq 'Start discussion'
......@@ -132,6 +137,8 @@ shared_examples 'discussion comments' do |resource_name|
describe 'creating a discussion' do
before do
find(submit_selector).click
wait_for_requests
find(comments_selector, match: :first)
end
......@@ -197,11 +204,13 @@ shared_examples 'discussion comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
button = find(submit_selector)
# on issues page, the submit input is a <button>, on other pages it is <input>
if resource_name == 'issue'
expect(find(submit_selector)).to have_content 'Comment'
if button.tag_name == 'button'
expect(button).to have_content 'Comment'
else
expect(find(submit_selector).value).to eq 'Comment'
expect(button.value).to eq 'Comment'
end
expect(page).not_to have_selector menu_selector
......
shared_examples 'note entity' do
subject { entity.as_json }
context 'basic note' do
it 'exposes correct elements' do
expect(subject).to include(:type, :author, :note, :note_html, :current_user,
:discussion_id, :emoji_awardable, :award_emoji, :report_abuse_path, :attachment)
end
it 'does not expose elements for specific notes cases' do
expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name)
end
it 'exposes author correctly' do
expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path)
end
it 'does not expose web_url for author' do
expect(subject[:author]).not_to include(:web_url)
end
end
context 'when note was edited' do
before do
note.update(updated_at: 1.minute.from_now, updated_by: user)
end
it 'exposes last_edited_at and last_edited_by elements' do
expect(subject).to include(:last_edited_at, :last_edited_by)
end
end
context 'when note is a system note' do
before do
note.update(system: true)
end
it 'exposes system_note_icon_name element' do
expect(subject).to include(:system_note_icon_name)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment