Skip to content
Snippets Groups Projects
Commit f3502c13 authored by Kerri Miller's avatar Kerri Miller
Browse files

Merge branch '389967-add-emojis-to-note-type' into 'master'

Add award emoji to GraphQL note type

See merge request !116196



Merged-by: default avatarKerri Miller <kerrizor@kerrizor.com>
Approved-by: default avatarMario Celi <mcelicalderon@gitlab.com>
Approved-by: default avatarKerri Miller <kerrizor@kerrizor.com>
Reviewed-by: Heinrich Lee Yu's avatarHeinrich Lee Yu <heinrich@gitlab.com>
Reviewed-by: default avatarMario Celi <mcelicalderon@gitlab.com>
Co-authored-by: Heinrich Lee Yu's avatarHeinrich Lee Yu <heinrich@gitlab.com>
parents 61b98cc4 51407fa2
No related branches found
No related tags found
1 merge request!116196Add award emoji to GraphQL note type
Pipeline #879030181 passed
Showing with 305 additions and 250 deletions
# frozen_string_literal: true
module Resolvers
module Noteable
class NotesResolver < BaseResolver
include LooksAhead
type Types::Notes::NoteType.connection_type, null: false
def resolve_with_lookahead(*)
apply_lookahead(object.notes.fresh)
end
def preloads
{
award_emoji: [:award_emoji]
}
end
end
end
end
......@@ -144,10 +144,6 @@ class AlertType < BaseObject
null: false,
description: 'URL of the alert.'
def notes
object.ordered_notes
end
def metrics_dashboard_url
return if Feature.enabled?(:remove_monitor_metrics)
......
......@@ -36,6 +36,10 @@ class NoteType < BaseObject
method: :note,
description: 'Content of the note.'
field :award_emoji, Types::AwardEmojis::AwardEmojiType.connection_type,
null: true,
description: 'List of award emojis associated with the note.'
field :confidential, GraphQL::Types::Boolean,
null: true,
description: 'Indicates if this note is confidential.',
......
......@@ -5,7 +5,7 @@ module Notes
module NoteableInterface
include Types::BaseInterface
field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes on this noteable."
field :notes, resolver: Resolvers::Noteable::NotesResolver, null: false, description: "All notes on this noteable."
field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable."
field :commenters, Types::UserType.connection_type, null: false, description: "All commenters on this noteable."
......
......@@ -18649,6 +18649,7 @@ Represents the network policy.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="noteauthor"></a>`author` | [`UserCore!`](#usercore) | User who wrote this note. |
| <a id="noteawardemoji"></a>`awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | List of award emojis associated with the note. (see [Connections](#connections)) |
| <a id="notebody"></a>`body` | [`String!`](#string) | Content of the note. |
| <a id="notebodyhtml"></a>`bodyHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `note`. |
| <a id="noteconfidential"></a>`confidential` **{warning-solid}** | [`Boolean`](#boolean) | **Deprecated** in 15.5. This was renamed. Use: `internal`. |
......@@ -164,171 +164,6 @@
end
end
describe 'fetching work item notes widget' do
let(:work_item) { create(:work_item, :issue, project: project) }
let(:item_filter_params) { { iid: work_item.iid.to_s } }
let(:fields) do
<<~GRAPHQL
edges {
node {
widgets {
type
... on WorkItemWidgetNotes {
system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
}
}
}
}
GRAPHQL
end
it 'fetches notes that require gitaly call to parse note' do
# this 9 digit long weight triggers a gitaly call when parsing the system note
create(:resource_weight_event, user: current_user, issue: work_item, weight: 123456789)
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_be_empty
end
context 'when fetching description version diffs' do
shared_examples 'description change diff' do |description_diffs_enabled: true|
it 'returns previous description change diff' do
post_graphql(query, current_user: current_user)
# check that system note is added
note = find_note(work_item, 'changed the description') # system note about changed description
expect(work_item.reload.description).to eq('updated description')
expect(note.note).to eq('changed the description')
# check that diff is returned
all_widgets = graphql_dig_at(items_data, :node, :widgets)
notes_widget = all_widgets.find { |x| x["type"] == "NOTES" }
system_notes = graphql_dig_at(notes_widget["system"], :nodes)
description_changed_note = graphql_dig_at(system_notes.first["notes"], :nodes).first
description_version = graphql_dig_at(description_changed_note['systemNoteMetadata'], :descriptionVersion)
id = GitlabSchema.parse_gid(description_version['id'], expected_type: ::DescriptionVersion).model_id
diff = description_version['diff']
diff_path = description_version['diffPath']
delete_path = description_version['deletePath']
can_delete = description_version['canDelete']
deleted = description_version['deleted']
url_helpers = ::Gitlab::Routing.url_helpers
url_args = [work_item.project, work_item, id]
if description_diffs_enabled
expect(diff).to eq("<span class=\"idiff addition\">updated description</span>")
expect(diff_path).to eq(url_helpers.description_diff_project_issue_path(*url_args))
expect(delete_path).to eq(url_helpers.delete_description_version_project_issue_path(*url_args))
expect(can_delete).to be true
else
expect(diff).to be_nil
expect(diff_path).to be_nil
expect(delete_path).to be_nil
expect(can_delete).to be_nil
end
expect(deleted).to be false
end
end
let(:fields) do
<<~GRAPHQL
edges {
node {
widgets {
type
... on WorkItemWidgetNotes {
system: discussions(filter: ONLY_ACTIVITY, first: 10) {
nodes {
id
notes {
nodes {
id
system
internal
body
systemNoteMetadata {
id
descriptionVersion {
id
diff(versionId: #{version_gid})
diffPath
deletePath
canDelete
deleted
}
}
}
}
}
}
}
}
}
}
GRAPHQL
end
let(:version_gid) { "null" }
let(:opts) { {} }
let(:spam_params) { double }
let(:widget_params) { { description_widget: { description: "updated description" } } }
let(:service) do
WorkItems::UpdateService.new(
container: project,
current_user: current_user,
params: opts,
spam_params: spam_params,
widget_params: widget_params
)
end
before do
stub_spam_services
project.add_developer(current_user)
service.execute(work_item)
end
it_behaves_like 'description change diff'
context 'with passed description version id' do
let(:version_gid) { "\"#{work_item.description_versions.first.to_global_id}\"" }
it_behaves_like 'description change diff'
end
context 'with description_diffs disabled' do
before do
stub_licensed_features(description_diffs: false)
end
it_behaves_like 'description change diff', description_diffs_enabled: false
end
context 'with description_diffs enabled through Registration Features' do
before do
stub_licensed_features(description_diffs: false)
stub_application_setting(usage_ping_features_enabled: true)
end
it_behaves_like 'description change diff', description_diffs_enabled: true
end
end
end
def find_note(work_item, starting_with)
work_item.notes.find do |note|
break note if note && note.note.start_with?(starting_with)
end
end
context 'with progress widget' do
let_it_be(:work_item1) { create(:work_item, :objective, project: project) }
let_it_be(:progress) { create(:progress, work_item: work_item1) }
......
......@@ -6,6 +6,7 @@
include GraphqlHelpers
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:iteration) { create(:iteration, iterations_cadence: create(:iterations_cadence, group: project.group)) }
......@@ -21,11 +22,12 @@
graphql_query_for('workItem', { 'id' => global_id }, work_item_fields)
end
context 'when the user can read the work item' do
before do
project.add_guest(guest)
end
before_all do
project.add_guest(guest)
project.add_developer(developer)
end
context 'when the user can read the work item' do
context 'when querying widgets' do
describe 'iteration widget' do
let(:work_item_fields) do
......@@ -426,6 +428,164 @@
end
end
end
describe 'notes widget' do
let(:work_item_fields) do
<<~GRAPHQL
id
widgets {
type
... on WorkItemWidgetNotes {
system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
}
}
GRAPHQL
end
it 'fetches notes that require gitaly call to parse note' do
# this 9 digit long weight triggers a gitaly call when parsing the system note
create(:resource_weight_event, user: current_user, issue: work_item, weight: 123456789)
post_graphql(query, current_user: current_user)
expect_graphql_errors_to_be_empty
end
context 'when fetching description version diffs' do
shared_examples 'description change diff' do |description_diffs_enabled: true|
it 'returns previous description change diff' do
post_graphql(query, current_user: developer)
# check that system note is added
note = find_note(work_item, 'changed the description') # system note about changed description
expect(work_item.reload.description).to eq('updated description')
expect(note.note).to eq('changed the description')
# check that diff is returned
all_widgets = graphql_dig_at(work_item_data, :widgets)
notes_widget = all_widgets.find { |x| x["type"] == "NOTES" }
system_notes = graphql_dig_at(notes_widget["system"], :nodes)
description_changed_note = graphql_dig_at(system_notes.first["notes"], :nodes).first
description_version = graphql_dig_at(description_changed_note['systemNoteMetadata'], :descriptionVersion)
id = GitlabSchema.parse_gid(description_version['id'], expected_type: ::DescriptionVersion).model_id
diff = description_version['diff']
diff_path = description_version['diffPath']
delete_path = description_version['deletePath']
can_delete = description_version['canDelete']
deleted = description_version['deleted']
url_helpers = ::Gitlab::Routing.url_helpers
url_args = [work_item.project, work_item, id]
if description_diffs_enabled
expect(diff).to eq("<span class=\"idiff addition\">updated description</span>")
expect(diff_path).to eq(url_helpers.description_diff_project_issue_path(*url_args))
expect(delete_path).to eq(url_helpers.delete_description_version_project_issue_path(*url_args))
expect(can_delete).to be true
else
expect(diff).to be_nil
expect(diff_path).to be_nil
expect(delete_path).to be_nil
expect(can_delete).to be_nil
end
expect(deleted).to be false
end
def find_note(work_item, starting_with)
work_item.notes.find do |note|
break note if note && note.note.start_with?(starting_with)
end
end
end
let_it_be_with_reload(:work_item) { create(:work_item, project: project) }
let(:work_item_fields) do
<<~GRAPHQL
id
widgets {
type
... on WorkItemWidgetNotes {
system: discussions(filter: ONLY_ACTIVITY, first: 10) {
nodes {
id
notes {
nodes {
id
system
internal
body
systemNoteMetadata {
id
descriptionVersion {
id
diff(versionId: #{version_gid})
diffPath
deletePath
canDelete
deleted
}
}
}
}
}
}
}
}
GRAPHQL
end
let(:version_gid) { "null" }
let(:opts) { {} }
let(:spam_params) { double }
let(:widget_params) { { description_widget: { description: "updated description" } } }
let(:service) do
WorkItems::UpdateService.new(
container: project,
current_user: developer,
params: opts,
spam_params: spam_params,
widget_params: widget_params
)
end
before do
stub_spam_services
service.execute(work_item)
end
it_behaves_like 'description change diff'
context 'with passed description version id' do
let(:version_gid) { "\"#{work_item.description_versions.first.to_global_id}\"" }
it_behaves_like 'description change diff'
end
context 'with description_diffs disabled' do
before do
stub_licensed_features(description_diffs: false)
end
it_behaves_like 'description change diff', description_diffs_enabled: false
end
context 'with description_diffs enabled through Registration Features' do
before do
stub_licensed_features(description_diffs: false)
stub_application_setting(usage_ping_features_enabled: true)
end
it_behaves_like 'description change diff', description_diffs_enabled: true
end
end
end
end
end
end
......@@ -8,6 +8,7 @@
author
body
body_html
award_emoji
confidential
internal
created_at
......
......@@ -288,60 +288,6 @@ def pagination_query(params)
end
end
describe 'fetching work item notes widget' do
let(:item_filter_params) { { iid: item2.iid.to_s } }
let(:fields) do
<<~GRAPHQL
edges {
node {
widgets {
type
... on WorkItemWidgetNotes {
system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
}
}
}
}
GRAPHQL
end
before_all do
create_notes(item1, "some note1")
create_notes(item2, "some note2")
end
shared_examples 'fetches work item notes' do |user_comments_count:, system_notes_count:|
it "fetches notes" do
post_graphql(query, current_user: current_user)
all_widgets = graphql_dig_at(items_data, :node, :widgets)
notes_widget = all_widgets.find { |x| x["type"] == "NOTES" }
all_notes = graphql_dig_at(notes_widget["all_notes"], :nodes)
system_notes = graphql_dig_at(notes_widget["system"], :nodes)
comments = graphql_dig_at(notes_widget["comments"], :nodes)
expect(comments.count).to eq(user_comments_count)
expect(system_notes.count).to eq(system_notes_count)
expect(all_notes.count).to eq(user_comments_count + system_notes_count)
end
end
context 'when user has permission to view internal notes' do
before do
project.add_developer(current_user)
end
it_behaves_like 'fetches work item notes', user_comments_count: 2, system_notes_count: 5
end
context 'when user cannot view internal notes' do
it_behaves_like 'fetches work item notes', user_comments_count: 1, system_notes_count: 5
end
end
context 'when fetching work item notifications widget' do
let(:fields) do
<<~GRAPHQL
......@@ -426,26 +372,4 @@ def query(params = item_filter_params)
query_graphql_field('workItems', params, fields)
)
end
def create_notes(work_item, note_body)
create(:note, system: true, project: work_item.project, noteable: work_item)
disc_start = create(:discussion_note_on_issue, noteable: work_item, project: work_item.project, note: note_body)
create(:note,
discussion_id: disc_start.discussion_id, noteable: work_item,
project: work_item.project, note: "reply on #{note_body}")
create(:resource_label_event, user: current_user, issue: work_item, label: label1, action: 'add')
create(:resource_label_event, user: current_user, issue: work_item, label: label1, action: 'remove')
create(:resource_milestone_event, issue: work_item, milestone: milestone1, action: 'add')
create(:resource_milestone_event, issue: work_item, milestone: milestone1, action: 'remove')
# confidential notes are currently available only on issues and epics
conf_disc_start = create(:discussion_note_on_issue, :confidential,
noteable: work_item, project: work_item.project, note: "confidential #{note_body}")
create(:note, :confidential,
discussion_id: conf_disc_start.discussion_id, noteable: work_item,
project: work_item.project, note: "reply on confidential #{note_body}")
end
end
......@@ -541,6 +541,95 @@
end
end
describe 'notes widget' do
let(:work_item_fields) do
<<~GRAPHQL
id
widgets {
type
... on WorkItemWidgetNotes {
system: discussions(filter: ONLY_ACTIVITY, first: 10) { nodes { id notes { nodes { id system internal body } } } },
comments: discussions(filter: ONLY_COMMENTS, first: 10) { nodes { id notes { nodes { id system internal body } } } },
all_notes: discussions(filter: ALL_NOTES, first: 10) { nodes { id notes { nodes { id system internal body } } } }
}
}
GRAPHQL
end
context 'when fetching award emoji from notes' do
let(:work_item_fields) do
<<~GRAPHQL
id
widgets {
type
... on WorkItemWidgetNotes {
discussions(filter: ONLY_COMMENTS, first: 10) {
nodes {
id
notes {
nodes {
id
body
awardEmoji {
nodes {
name
user {
name
}
}
}
}
}
}
}
}
}
GRAPHQL
end
let_it_be(:note) { create(:note, project: work_item.project, noteable: work_item) }
before_all do
create(:award_emoji, awardable: note, name: 'rocket', user: developer)
end
it 'returns award emoji data' do
all_widgets = graphql_dig_at(work_item_data, :widgets)
notes_widget = all_widgets.find { |x| x['type'] == 'NOTES' }
notes = graphql_dig_at(notes_widget['discussions'], :nodes).flat_map { |d| d['notes']['nodes'] }
note_with_emoji = notes.find { |n| n['id'] == note.to_gid.to_s }
expect(note_with_emoji).to include(
'awardEmoji' => {
'nodes' => include(
hash_including(
'name' => 'rocket',
'user' => {
'name' => developer.name
}
)
)
}
)
end
it 'avoids N+1 queries' do
post_graphql(query, current_user: developer)
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: developer) }
expect_graphql_errors_to_be_empty
another_note = create(:note, project: work_item.project, noteable: work_item)
create(:award_emoji, awardable: another_note, name: 'star', user: guest)
expect { post_graphql(query, current_user: developer) }.not_to exceed_query_limit(control)
expect_graphql_errors_to_be_empty
end
end
end
context 'when an Issue Global ID is provided' do
let(:global_id) { Issue.find(work_item.id).to_gid.to_s }
......
......@@ -20,6 +20,14 @@
edges {
node {
#{all_graphql_fields_for('Note', max_depth: 1)}
awardEmoji {
nodes {
name
user {
name
}
}
}
}
}
}
......@@ -40,6 +48,22 @@
expect(noteable_data['notes']['edges'].first['node']['body'])
.to eq(note.note)
end
it 'avoids N+1 queries' do
create(:award_emoji, awardable: note, name: 'star', user: user)
post_graphql(query, current_user: user)
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: user) }
expect_graphql_errors_to_be_empty
another_note = create(:note, project: note.project, noteable: noteable, author: user)
create(:award_emoji, awardable: another_note, name: 'star', user: user)
expect { post_graphql(query, current_user: user) }.not_to exceed_query_limit(control)
expect_graphql_errors_to_be_empty
end
end
context "for discussions" do
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment