Skip to content
Snippets Groups Projects
Verified Commit 935718e2 authored by Phil Hughes's avatar Phil Hughes
Browse files

Added finish review dropdown

Adds the ability to add a comment when submitting a review
on a merge request.

Closes #244044
parent 29b9decb
No related branches found
No related tags found
1 merge request!88937Added finish review dropdown
Showing
with 309 additions and 142 deletions
......@@ -6,13 +6,20 @@ export default {
components: {
GlBadge,
},
props: {
variant: {
type: String,
required: false,
default: 'info',
},
},
computed: {
...mapGetters('batchComments', ['draftsCount']),
},
};
</script>
<template>
<gl-badge size="sm" variant="info" class="gl-ml-2">
<gl-badge size="sm" :variant="variant" class="gl-ml-2">
{{ draftsCount }}
<span class="sr-only"> {{ n__('draft', 'drafts', draftsCount) }} </span>
</gl-badge>
......
<script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PreviewItem from './preview_item.vue';
import DraftsCount from './drafts_count.vue';
export default {
components: {
......@@ -9,7 +11,9 @@ export default {
GlDropdownItem,
GlIcon,
PreviewItem,
DraftsCount,
},
mixins: [glFeatureFlagMixin()],
computed: {
...mapState('diffs', ['viewDiffsFileByFile']),
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
......@@ -39,6 +43,7 @@ export default {
>
<template #button-content>
{{ __('Pending comments') }}
<drafts-count v-if="glFeatures.mrReviewSubmitComment" variant="neutral" />
<gl-icon class="dropdown-chevron" name="chevron-up" />
</template>
<gl-dropdown-item
......
<script>
import { mapActions, mapGetters } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants';
import PreviewDropdown from './preview_dropdown.vue';
import PublishButton from './publish_button.vue';
import SubmitDropdown from './submit_dropdown.vue';
export default {
components: {
PreviewDropdown,
PublishButton,
SubmitDropdown,
},
mixins: [glFeatureFlagMixin()],
computed: {
...mapGetters(['isNotesFetched']),
},
......@@ -38,7 +42,8 @@ export default {
data-qa-selector="review_bar_content"
>
<preview-dropdown />
<publish-button class="gl-ml-3" show-count />
<publish-button v-if="!glFeatures.mrReviewSubmitComment" class="gl-ml-3" show-count />
<submit-dropdown v-else />
</div>
</nav>
</div>
......
<script>
import { GlDropdown, GlButton, GlIcon } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
export default {
components: {
GlDropdown,
GlButton,
GlIcon,
MarkdownField,
},
data() {
return {
isSubmitting: false,
note: '',
};
},
computed: {
...mapGetters(['getNotesData', 'getNoteableData', 'noteableType']),
},
methods: {
...mapActions('batchComments', ['publishReview']),
async submitReview() {
const noteData = {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
note: this.note,
};
this.isSubmitting = true;
await this.publishReview(noteData);
this.isSubmitting = false;
},
},
restrictedToolbarItems: ['full-screen'],
};
</script>
<template>
<gl-dropdown right class="submit-review-dropdown" variant="info">
<template #button-content>
{{ __('Finish review') }}
<gl-icon class="dropdown-chevron" name="chevron-up" />
</template>
<form @submit.prevent="submitReview">
<label for="review-note-body" class="gl-font-weight-bold gl-mb-3">{{
__('Summary comment (optional)')
}}</label>
<div class="common-note-form gfm-form">
<div
class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100"
>
<markdown-field
ref="markdownField"
:is-submitting="isSubmitting"
:add-spacing-classes="false"
:textarea-value="note"
:markdown-preview-path="getNoteableData.preview_note_path"
:markdown-docs-path="getNotesData.markdownDocsPath"
:quick-actions-docs-path="getNotesData.quickActionsDocsPath"
:restricted-tool-bar-items="$options.restrictedToolbarItems"
:force-autosize="false"
>
<template #textarea>
<textarea
id="review-note-body"
ref="textarea"
v-model="note"
dir="auto"
:disabled="isSubmitting"
name="review[note]"
class="note-textarea js-gfm-input markdown-area"
data-supports-quick-actions="true"
:aria-label="__('Comment')"
:placeholder="__('Write a comment or drag your files here…')"
@keydown.meta.enter="submitReview"
@keydown.ctrl.enter="submitReview"
></textarea>
</template>
</markdown-field>
</div>
</div>
<div class="gl-display-flex gl-mt-5">
<gl-button
:loading="isSubmitting"
variant="info"
type="submit"
class="gl-ml-auto js-no-auto-disable"
>
{{ __('Submit review') }}
</gl-button>
</div>
</form>
</gl-dropdown>
</template>
......@@ -19,8 +19,8 @@ export default {
fetchDrafts(endpoint) {
return axios.get(endpoint);
},
publish(endpoint) {
return axios.post(endpoint);
publish(endpoint, noteData) {
return axios.post(endpoint, noteData);
},
discard(endpoint) {
return axios.delete(endpoint);
......
......@@ -77,11 +77,11 @@ export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
.catch(() => commit(types.RECEIVE_PUBLISH_DRAFT_ERROR, draftId));
};
export const publishReview = ({ commit, dispatch, getters }) => {
export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
commit(types.REQUEST_PUBLISH_REVIEW);
return service
.publish(getters.getNotesData.draftsPublishPath)
.publish(getters.getNotesData.draftsPublishPath, noteData)
.then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR));
......
......@@ -121,6 +121,11 @@ export default {
required: false,
default: () => [],
},
forceAutosize: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -250,7 +255,7 @@ export default {
vulnerabilities: this.enableAutocomplete,
contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete,
},
true,
this.forceAutosize,
);
},
beforeDestroy() {
......
......@@ -754,3 +754,23 @@ $tabs-holder-z-index: 250;
}
}
}
.submit-review-dropdown {
.dropdown-menu {
width: 650px;
max-width: 650px;
.gl-new-dropdown-inner {
max-height: none !important;
}
}
.gl-new-dropdown-contents {
padding: $gl-spacing-scale-4 !important;
}
.md-preview-holder {
max-height: 180px;
height: 180px;
}
}
......@@ -49,6 +49,10 @@ def destroy
def publish
result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
if Feature.enabled?(:mr_review_submit_comment, @project) && create_note_params[:note]
Notes::CreateService.new(@project, current_user, create_note_params).execute
end
if result[:status] == :success
head :ok
else
......@@ -102,6 +106,15 @@ def draft_note_params
end
end
def create_note_params
params.permit(
:note
).tap do |create_params|
create_params[:noteable_type] = merge_request.class.name
create_params[:noteable_id] = merge_request.id
end
end
def prepare_notes_for_rendering(notes)
return [] unless notes
......
......@@ -49,6 +49,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:remove_diff_header_icons, project)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:paginated_mr_discussions, project)
push_frontend_feature_flag(:mr_review_submit_comment, project)
end
before_action do
......
---
name: mr_review_submit_comment
introduced_by_url:
rollout_issue_url:
milestone: '15.1'
type: development
group: group::code review
default_enabled: false
......@@ -16106,6 +16106,9 @@ msgstr ""
msgid "Finish editing this message first!"
msgstr ""
 
msgid "Finish review"
msgstr ""
msgid "Finish setting up your dedicated account for %{group_name}."
msgstr ""
 
......@@ -36874,6 +36877,9 @@ msgstr ""
msgid "Summary / Note"
msgstr ""
 
msgid "Summary comment (optional)"
msgstr ""
msgid "Sunday"
msgstr ""
 
......
......@@ -18,219 +18,218 @@
project.add_maintainer(user)
sign_in(user)
visit_diffs
end
context 'Feature is enabled' do
before do
visit_diffs
end
it 'adds draft note' do
write_diff_comment
it 'adds draft note' do
write_diff_comment
expect(find('.draft-note-component')).to have_content('Line is wrong')
expect(find('.draft-note-component')).to have_content('Line is wrong')
expect(page).to have_selector('[data-testid="review_bar_component"]')
expect(page).to have_selector('[data-testid="review_bar_component"]')
expect(find('[data-testid="review_bar_component"] .gl-badge')).to have_content('1')
end
expect(find('[data-testid="review_bar_component"] .btn-confirm')).to have_content('1')
it 'publishes review' do
write_diff_comment
page.within('.review-bar-content') do
click_button 'Finish review'
click_button 'Submit review'
end
it 'publishes review' do
write_diff_comment
wait_for_requests
page.within('.review-bar-content') do
click_button 'Submit review'
end
expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong')
wait_for_requests
expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong')
end
expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong')
it 'publishes single comment' do
write_diff_comment
expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong')
end
click_button 'Add comment now'
it 'publishes single comment' do
write_diff_comment
wait_for_requests
click_button 'Add comment now'
expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong')
wait_for_requests
expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong')
end
expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong')
it 'deletes draft note' do
write_diff_comment
expect(page).to have_selector('.note:not(.draft-note)', text: 'Line is wrong')
find('.js-note-delete').click
page.within('.modal') do
click_button('Delete Comment', match: :first)
end
it 'deletes draft note' do
write_diff_comment
wait_for_requests
find('.js-note-delete').click
expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong')
end
page.within('.modal') do
click_button('Delete Comment', match: :first)
end
it 'edits draft note' do
write_diff_comment
wait_for_requests
find('.js-note-edit').click
expect(page).not_to have_selector('.draft-note-component', text: 'Line is wrong')
end
# make sure comment form is in view
execute_script("window.scrollBy(0, 200)")
it 'edits draft note' do
write_diff_comment
write_comment(text: 'Testing update', button_text: 'Save comment')
find('.js-note-edit').click
expect(page).to have_selector('.draft-note-component', text: 'Testing update')
end
# make sure comment form is in view
execute_script("window.scrollBy(0, 200)")
context 'with image and file draft note' do
let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project) }
let!(:draft_on_text) { create(:draft_note_on_text_diff, merge_request: merge_request, author: user, path: 'README.md', note: 'Lorem ipsum on text...') }
let!(:draft_on_image) { create(:draft_note_on_image_diff, merge_request: merge_request, author: user, path: 'files/images/ee_repo_logo.png', note: 'Lorem ipsum on an image...') }
write_comment(text: 'Testing update', button_text: 'Save comment')
it 'does not show in overview' do
visit_overview
expect(page).to have_selector('.draft-note-component', text: 'Testing update')
expect(page).to have_no_text(draft_on_text.note)
expect(page).to have_no_text(draft_on_image.note)
end
end
context 'with image and file draft note' do
let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project) }
let!(:draft_on_text) { create(:draft_note_on_text_diff, merge_request: merge_request, author: user, path: 'README.md', note: 'Lorem ipsum on text...') }
let!(:draft_on_image) { create(:draft_note_on_image_diff, merge_request: merge_request, author: user, path: 'files/images/ee_repo_logo.png', note: 'Lorem ipsum on an image...') }
it 'does not show in overview' do
visit_overview
context 'adding single comment to review' do
before do
visit_overview
end
expect(page).to have_no_text(draft_on_text.note)
expect(page).to have_no_text(draft_on_image.note)
end
it 'at first does not show `Add to review` and `Add comment now` buttons' do
expect(page).to have_no_button('Add to review')
expect(page).to have_no_button('Add comment now')
end
context 'adding single comment to review' do
context 'when review has started' do
before do
visit_overview
end
it 'at first does not show `Add to review` and `Add comment now` buttons' do
expect(page).to have_no_button('Add to review')
expect(page).to have_no_button('Add comment now')
end
context 'when review has started' do
before do
visit_diffs
visit_diffs
write_diff_comment
write_diff_comment
visit_overview
end
visit_overview
end
it 'can add comment to review' do
write_comment(selector: '.js-main-target-form', field: 'note-body', text: 'Its a draft comment', button_text: 'Add to review')
it 'can add comment to review' do
write_comment(selector: '.js-main-target-form', field: 'note-body', text: 'Its a draft comment', button_text: 'Add to review')
expect(page).to have_selector('.draft-note-component', text: 'Its a draft comment')
expect(page).to have_selector('.draft-note-component', text: 'Its a draft comment')
click_button('Pending comments')
click_button('Pending comments')
expect(page).to have_text('2 pending comments')
end
expect(page).to have_text('2 pending comments')
end
it 'can add comment right away' do
write_comment(selector: '.js-main-target-form', field: 'note-body', text: 'Its a regular comment', button_text: 'Add comment now')
it 'can add comment right away' do
write_comment(selector: '.js-main-target-form', field: 'note-body', text: 'Its a regular comment', button_text: 'Add comment now')
expect(page).to have_selector('.note:not(.draft-note)', text: 'Its a regular comment')
expect(page).to have_selector('.note:not(.draft-note)', text: 'Its a regular comment')
click_button('Pending comments')
click_button('Pending comments')
expect(page).to have_text('1 pending comment')
end
expect(page).to have_text('1 pending comment')
end
end
end
context 'in parallel diff' do
before do
find('.js-show-diff-settings').click
click_button 'Side-by-side'
find('.js-show-diff-settings').click
end
context 'in parallel diff' do
before do
find('.js-show-diff-settings').click
click_button 'Side-by-side'
find('.js-show-diff-settings').click
end
it 'adds draft comments to both sides' do
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9')
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9', button_text: 'Add to review', text: 'Another wrong line')
it 'adds draft comments to both sides' do
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9')
write_parallel_comment('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9', button_text: 'Add to review', text: 'Another wrong line')
expect(find('.new .draft-note-component')).to have_content('Line is wrong')
expect(find('.old .draft-note-component')).to have_content('Another wrong line')
expect(find('.new .draft-note-component')).to have_content('Line is wrong')
expect(find('.old .draft-note-component')).to have_content('Another wrong line')
expect(find('.review-bar-content .btn-confirm')).to have_content('2')
end
expect(find('.review-bar-content .gl-badge')).to have_content('2')
end
end
context 'thread is unresolved' do
let!(:active_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
context 'thread is unresolved' do
let!(:active_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
before do
visit_diffs
end
before do
visit_diffs
end
it 'publishes comment right away and resolves the thread' do
expect(active_discussion.resolved?).to eq(false)
it 'publishes comment right away and resolves the thread' do
expect(active_discussion.resolved?).to eq(false)
write_reply_to_discussion(button_text: 'Add comment now', resolve: true)
write_reply_to_discussion(button_text: 'Add comment now', resolve: true)
page.within '.discussions-counter' do
expect(page).to have_content('All threads resolved')
end
page.within '.discussions-counter' do
expect(page).to have_content('All threads resolved')
end
end
it 'publishes review and resolves the thread' do
expect(active_discussion.resolved?).to eq(false)
it 'publishes review and resolves the thread' do
expect(active_discussion.resolved?).to eq(false)
write_reply_to_discussion(resolve: true)
write_reply_to_discussion(resolve: true)
page.within('.review-bar-content') do
click_button 'Submit review'
end
page.within('.review-bar-content') do
click_button 'Finish review'
click_button 'Submit review'
end
wait_for_requests
wait_for_requests
page.within '.discussions-counter' do
expect(page).to have_content('All threads resolved')
end
page.within '.discussions-counter' do
expect(page).to have_content('All threads resolved')
end
end
end
context 'thread is resolved' do
let!(:active_discussion) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project).to_discussion }
context 'thread is resolved' do
let!(:active_discussion) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project).to_discussion }
before do
active_discussion.resolve!(@current_user)
before do
active_discussion.resolve!(@current_user)
visit_diffs
visit_diffs
page.find('.js-diff-comment-avatar').click
end
page.find('.js-diff-comment-avatar').click
end
it 'publishes comment right away and unresolves the thread' do
expect(active_discussion.resolved?).to eq(true)
it 'publishes comment right away and unresolves the thread' do
expect(active_discussion.resolved?).to eq(true)
write_reply_to_discussion(button_text: 'Add comment now', unresolve: true)
write_reply_to_discussion(button_text: 'Add comment now', unresolve: true)
page.within '.discussions-counter' do
expect(page).to have_content('1 unresolved thread')
end
page.within '.discussions-counter' do
expect(page).to have_content('1 unresolved thread')
end
end
it 'publishes review and unresolves the thread' do
expect(active_discussion.resolved?).to eq(true)
it 'publishes review and unresolves the thread' do
expect(active_discussion.resolved?).to eq(true)
wait_for_requests
wait_for_requests
write_reply_to_discussion(button_text: 'Start a review', unresolve: true)
write_reply_to_discussion(button_text: 'Start a review', unresolve: true)
page.within('.review-bar-content') do
click_button 'Submit review'
end
page.within('.review-bar-content') do
click_button 'Finish review'
click_button 'Submit review'
end
wait_for_requests
wait_for_requests
page.within '.discussions-counter' do
expect(page).to have_content('1 unresolved thread')
end
page.within '.discussions-counter' do
expect(page).to have_content('1 unresolved thread')
end
end
end
......
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