From 7b1a394d77921bd36de42a5c545e9091f2dadb95 Mon Sep 17 00:00:00 2001 From: Kinshuk Singh <kinsingh@gitlab.com> Date: Thu, 13 Mar 2025 12:49:38 -0400 Subject: [PATCH] Add GitLabDuo feedback string for diff review comments --- .../notes/components/note_awards_list.vue | 6 + .../notes/components/note_body.vue | 37 ++++- locale/gitlab.pot | 3 + .../notes/components/note_body_spec.js | 126 ++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 21f226cd20741..f968cc5a29980 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -30,6 +30,11 @@ export default { type: Boolean, required: true, }, + defaultAwards: { + type: Array, + required: false, + default: () => [], + }, }, computed: { ...mapGetters(['getUserData']), @@ -62,6 +67,7 @@ export default { :awards="awards" :can-award-emoji="canAwardEmoji" :current-user-id="getUserData.id" + :default-awards="defaultAwards" @award="handleAward($event)" /> </div> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 8e2c343de97bc..7c5d8525e9d90 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -3,7 +3,7 @@ import { escape } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; import SafeHtml from '~/vue_shared/directives/safe_html'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import { renderGFM } from '~/behaviors/markdown/render_gfm'; import NoteAttachment from './note_attachment.vue'; @@ -107,6 +107,33 @@ export default { return escape(suggestion); }, + isDuoFirstReviewComment() { + // Must be a Duo bot comment of type DiffNote + if (this.note.author.user_type !== 'duo_code_review_bot' || this.note.type !== 'DiffNote') { + return false; + } + // Get the discussion + const discussion = this.getDiscussion(this.note.discussion_id); + // If can't get discussion or this is not the first note, don't show feedback + return discussion?.notes?.length > 0 && discussion.notes[0].id === this.note.id; + }, + defaultAwardsList() { + return this.isDuoFirstReviewComment ? ['thumbsup', 'thumbsdown'] : []; + }, + duoFeedbackText() { + return sprintf( + __( + 'Rate this response %{separator} %{codeStart}%{botUser}%{codeEnd} in reply for more questions', + ), + { + separator: '•', + codeStart: '<code>', + botUser: '@GitLabDuo', + codeEnd: '</code>', + }, + false, + ); + }, }, watch: { note: { @@ -226,13 +253,19 @@ export default { :action-text="__('Edited')" class="note_edited_ago" /> + <div + v-if="isDuoFirstReviewComment" + v-safe-html:[$options.safeHtmlConfig]="duoFeedbackText" + class="gl-text-md gl-mt-4 gl-text-gray-500" + ></div> <note-awards-list - v-if="note.award_emoji && note.award_emoji.length" + v-if="isDuoFirstReviewComment || (note.award_emoji && note.award_emoji.length)" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" :toggle-award-path="note.toggle_award_path" :can-award-emoji="note.current_user.can_award_emoji" + :default-awards="defaultAwardsList" /> <note-attachment v-if="note.attachment" :attachment="note.attachment" /> </div> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e8466f339c2cc..4a17cf45ea800 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -48026,6 +48026,9 @@ msgstr "" msgid "Rate the review" msgstr "" +msgid "Rate this response %{separator} %{codeStart}%{botUser}%{codeEnd} in reply for more questions" +msgstr "" + msgid "Raw blob request rate limit per minute" msgstr "" diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index 6cb8474537af0..54c57678e39b3 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -167,3 +167,129 @@ describe('issue_note_body component', () => { ); }); }); + +describe('duo code review feedback text', () => { + const createMockStoreWithDiscussion = (discussionId, discussionNotes) => { + return new Vuex.Store({ + getters: { + getDiscussion: () => (id) => { + if (id === discussionId) { + return { notes: discussionNotes }; + } + return {}; + }, + suggestionsCount: () => 0, + getSuggestionsFilePaths: () => [], + }, + modules: { + diffs: { + namespaced: true, + getters: { + suggestionCommitMessage: () => () => '', + }, + }, + notes: { + state: { batchSuggestionsInfo: [] }, + }, + page: { + state: { failedToLoadMetadata: false }, + }, + }, + }); + }; + + const createDuoNote = (props = {}) => ({ + ...note, + id: '1', + type: 'DiffNote', + discussion_id: 'discussion1', + author: { + ...note.author, + user_type: 'duo_code_review_bot', + }, + ...props, + }); + + it('renders feedback text for the first DiffNote from GitLabDuo', () => { + const duoNote = createDuoNote(); + const mockStore = createMockStoreWithDiscussion('discussion1', [duoNote]); + + const wrapper = createComponent({ + props: { note: duoNote }, + store: mockStore, + }); + + const feedbackDiv = wrapper.find('.gl-text-md.gl-mt-4.gl-text-gray-500'); + expect(feedbackDiv.exists()).toBe(true); + }); + + it('does not render feedback text for non-DiffNote from GitLabDuo', () => { + const duoNote = createDuoNote({ type: 'DiscussionNote' }); + + const wrapper = createComponent({ + props: { note: duoNote }, + }); + + const feedbackDiv = wrapper.find('.gl-text-md.gl-mt-4.gl-text-gray-500'); + expect(feedbackDiv.exists()).toBe(false); + }); + + it('does not render feedback text for follow-up DiffNote from GitLabDuo', () => { + const duoNote = createDuoNote({ id: '2' }); + const mockStore = createMockStoreWithDiscussion('discussion1', [ + { id: '1' }, // First note has different ID + duoNote, + ]); + + const wrapper = createComponent({ + props: { note: duoNote }, + store: mockStore, + }); + + const feedbackDiv = wrapper.find('.gl-text-md.gl-mt-4.gl-text-gray-500'); + expect(feedbackDiv.exists()).toBe(false); + }); + + it('shows default awards list with thumbsup and thumbsdown for first DiffNote from GitLabDuo', () => { + const duoNote = createDuoNote(); + const mockStore = createMockStoreWithDiscussion('discussion1', [duoNote]); + + const wrapper = createComponent({ + props: { note: duoNote }, + store: mockStore, + }); + + const awardsList = wrapper.findComponent(NoteAwardsList); + expect(awardsList.exists()).toBe(true); + expect(awardsList.props('defaultAwards')).toEqual(['thumbsup', 'thumbsdown']); + }); + + it('uses empty default awards list for non-Duo comments', () => { + const regularNote = { + ...note, + id: '1', + author: { + ...note.author, + user_type: 'human', + }, + }; + + const wrapper = createComponent({ + props: { note: regularNote }, + }); + + const awardsList = wrapper.findComponent(NoteAwardsList); + expect(awardsList.props('defaultAwards')).toEqual([]); + }); + + describe('duoFeedbackText computed property', () => { + it('returns the expected feedback text', () => { + const wrapper = createComponent(); + + const result = wrapper.vm.duoFeedbackText; + expect(result).toContain('Rate this response'); + expect(result).toContain('@GitLabDuo'); + expect(result).toContain('in reply for more questions'); + }); + }); +}); -- GitLab