diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-18 03:09:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-18 03:09:24 +0000 |
commit | 95f5aad5aa99577555f01476f771e581957934f1 (patch) | |
tree | 3ce10768748ac63c5b42dfbfde989983afa54973 | |
parent | ccefff8087799bc076737ad080f18cf98e6fe114 (diff) | |
download | gitlab-ce-95f5aad5aa99577555f01476f771e581957934f1.tar.gz |
Add latest changes from gitlab-org/gitlab@master
9 files changed, 169 insertions, 29 deletions
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue index 4c100ec7335..92203381154 100644 --- a/app/assets/javascripts/batch_comments/components/draft_note.vue +++ b/app/assets/javascripts/batch_comments/components/draft_note.vue @@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import NoteableNote from '~/notes/components/noteable_note.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import PublishButton from './publish_button.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -10,6 +11,7 @@ export default { PublishButton, LoadingButton, }, + mixins: [glFeatureFlagsMixin()], props: { draft: { type: Object, @@ -64,14 +66,27 @@ export default { handleNotEditing() { this.isEditingDraft = false; }, + handleMouseEnter(draft) { + if (this.glFeatures.multilineComments && draft.position) { + this.setSelectedCommentPositionHover(draft.position.line_range); + } + }, + handleMouseLeave(draft) { + // Even though position isn't used here we still don't want to unecessarily call a mutation + // The lack of position tells us that highlighting is irrelevant in this context + if (this.glFeatures.multilineComments && draft.position) { + this.setSelectedCommentPositionHover(); + } + }, }, }; </script> <template> <article + role="article" class="draft-note-component note-wrapper" - @mouseenter="setSelectedCommentPositionHover(draft.position.line_range)" - @mouseleave="setSelectedCommentPositionHover()" + @mouseenter="handleMouseEnter(draft)" + @mouseleave="handleMouseLeave(draft)" > <ul class="notes draft-notes"> <noteable-note diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 458da5cf67f..a1e887c47d0 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -9,6 +9,7 @@ import NoteableNote from './noteable_note.vue'; import ToggleRepliesWidget from './toggle_replies_widget.vue'; import NoteEditedText from './note_edited_text.vue'; import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'DiscussionNotes', @@ -17,6 +18,7 @@ export default { NoteEditedText, DiscussionNotesRepliesWrapper, }, + mixins: [glFeatureFlagsMixin()], props: { discussion: { type: Object, @@ -93,6 +95,18 @@ export default { componentData(note) { return note.isPlaceholderNote ? note.notes[0] : note; }, + handleMouseEnter(discussion) { + if (this.glFeatures.multilineComments && discussion.position) { + this.setSelectedCommentPositionHover(discussion.position.line_range); + } + }, + handleMouseLeave(discussion) { + // Even though position isn't used here we still don't want to unecessarily call a mutation + // The lack of position tells us that highlighting is irrelevant in this context + if (this.glFeatures.multilineComments && discussion.position) { + this.setSelectedCommentPositionHover(); + } + }, }, }; </script> @@ -101,8 +115,8 @@ export default { <div class="discussion-notes"> <ul class="notes" - @mouseenter="setSelectedCommentPositionHover(discussion.position.line_range)" - @mouseleave="setSelectedCommentPositionHover()" + @mouseenter="handleMouseEnter(discussion)" + @mouseleave="handleMouseLeave(discussion)" > <template v-if="shouldGroupReplies"> <component diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 9bf8cffe940..5999ded8721 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -152,9 +152,10 @@ export default { return this.line && this.startLineNumber !== this.endLineNumber; }, + showMultilineCommentForm() { + return Boolean(this.isEditing && this.note.position && this.diffFile && this.line); + }, commentLineOptions() { - if (!this.diffFile || !this.line) return []; - const sideA = this.line.type === 'new' ? 'right' : 'left'; const sideB = sideA === 'left' ? 'right' : 'left'; const lines = this.diffFile.highlighted_diff_lines.length @@ -339,7 +340,7 @@ export default { > <div v-if="showMultiLineComment" data-testid="multiline-comment"> <multiline-comment-form - v-if="isEditing && note.position" + v-if="showMultilineCommentForm" v-model="commentLineStart" :line="line" :comment-line-options="commentLineOptions" diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b7885771781..cf7c40b97d8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -251,17 +251,12 @@ class MergeRequest < ApplicationRecord end scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } - - PROJECT_ROUTE_AND_NAMESPACE_ROUTE = [ - target_project: [:route, { namespace: :route }], - source_project: [:route, { namespace: :route }] - ].freeze - scope :with_api_entity_associations, -> { - preload(:assignees, :author, :unresolved_notes, :labels, :milestone, - :timelogs, :latest_merge_request_diff, - *PROJECT_ROUTE_AND_NAMESPACE_ROUTE, - metrics: [:latest_closed_by, :merged_by]) + preload_routables + .preload(:assignees, :author, :unresolved_notes, :labels, :milestone, + :timelogs, :latest_merge_request_diff, + target_project: :project_feature, + metrics: [:latest_closed_by, :merged_by]) } scope :by_target_branch_wildcard, ->(wildcard_branch_name) do @@ -269,6 +264,10 @@ class MergeRequest < ApplicationRecord end scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) } scope :preload_source_project, -> { preload(:source_project) } + scope :preload_routables, -> do + preload(target_project: [:route, { namespace: :route }], + source_project: [:route, { namespace: :route }]) + end scope :with_auto_merge_enabled, -> do with_state(:opened).where(auto_merge_enabled: true) diff --git a/changelogs/unreleased/jdb-fix-js-error-multilinecomment-overview.yml b/changelogs/unreleased/jdb-fix-js-error-multilinecomment-overview.yml new file mode 100644 index 00000000000..4909867e25a --- /dev/null +++ b/changelogs/unreleased/jdb-fix-js-error-multilinecomment-overview.yml @@ -0,0 +1,5 @@ +--- +title: Fix editing note throws js error +merge_request: 37216 +author: +type: fixed diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index eea7f25dbc1..99980c98f8b 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -1,4 +1,5 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { getByRole } from '@testing-library/dom'; import DraftNote from '~/batch_comments/components/draft_note.vue'; import { createStore } from '~/batch_comments/stores'; import NoteableNote from '~/notes/components/noteable_note.vue'; @@ -8,21 +9,34 @@ import { createDraft } from '../mock_data'; const localVue = createLocalVue(); describe('Batch comments draft note component', () => { + let store; let wrapper; let draft; + const LINE_RANGE = {}; + const draftWithLineRange = { + position: { + line_range: LINE_RANGE, + }, + }; - beforeEach(() => { - const store = createStore(); - - draft = createDraft(); + const getList = () => getByRole(wrapper.element, 'list'); + const createComponent = (propsData = { draft }, features = {}) => { wrapper = shallowMount(localVue.extend(DraftNote), { store, - propsData: { draft }, + propsData, localVue, + provide: { + glFeatures: { multilineComments: true, ...features }, + }, }); jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(); + }; + + beforeEach(() => { + store = createStore(); + draft = createDraft(); }); afterEach(() => { @@ -30,6 +44,7 @@ describe('Batch comments draft note component', () => { }); it('renders template', () => { + createComponent(); expect(wrapper.find('.draft-pending-label').exists()).toBe(true); const note = wrapper.find(NoteableNote); @@ -40,6 +55,7 @@ describe('Batch comments draft note component', () => { describe('add comment now', () => { it('dispatches publishSingleDraft when clicking', () => { + createComponent(); const publishNowButton = wrapper.find({ ref: 'publishNowButton' }); publishNowButton.vm.$emit('click'); @@ -50,6 +66,7 @@ describe('Batch comments draft note component', () => { }); it('sets as loading when draft is publishing', done => { + createComponent(); wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1); wrapper.vm.$nextTick(() => { @@ -64,6 +81,7 @@ describe('Batch comments draft note component', () => { describe('update', () => { it('dispatches updateDraft', done => { + createComponent(); const note = wrapper.find(NoteableNote); note.vm.$emit('handleEdit'); @@ -91,6 +109,7 @@ describe('Batch comments draft note component', () => { describe('deleteDraft', () => { it('dispatches deleteDraft', () => { + createComponent(); jest.spyOn(window, 'confirm').mockImplementation(() => true); const note = wrapper.find(NoteableNote); @@ -103,6 +122,7 @@ describe('Batch comments draft note component', () => { describe('quick actions', () => { it('renders referenced commands', done => { + createComponent(); wrapper.setProps({ draft: { ...draft, @@ -122,4 +142,26 @@ describe('Batch comments draft note component', () => { }); }); }); + + describe('multiline comments', () => { + describe.each` + desc | props | features | event | expectedCalls + ${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]} + ${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]} + ${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]} + ${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]} + ${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]} + ${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]} + `('$desc and features $features', ({ props, event, features, expectedCalls }) => { + beforeEach(() => { + createComponent({ draft: { ...draft, ...props } }, features); + jest.spyOn(store, 'dispatch'); + }); + + it(`calls store ${expectedCalls.length} times on ${event}`, () => { + getList().dispatchEvent(new MouseEvent(event, { bubbles: true })); + expect(store.dispatch.mock.calls).toEqual(expectedCalls); + }); + }); + }); }); diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 5a10deefd09..8cc98f978c2 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { getByRole } from '@testing-library/dom'; import '~/behaviors/markdown/render_gfm'; import { SYSTEM_NOTE } from '~/notes/constants'; import DiscussionNotes from '~/notes/components/discussion_notes.vue'; @@ -9,14 +10,20 @@ import SystemNote from '~/vue_shared/components/notes/system_note.vue'; import createStore from '~/notes/stores'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; +const LINE_RANGE = {}; +const DISCUSSION_WITH_LINE_RANGE = { + ...discussionMock, + position: { + line_range: LINE_RANGE, + }, +}; + describe('DiscussionNotes', () => { + let store; let wrapper; - const createComponent = props => { - const store = createStore(); - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - + const getList = () => getByRole(wrapper.element, 'list'); + const createComponent = (props, features = {}) => { wrapper = shallowMount(DiscussionNotes, { store, propsData: { @@ -31,11 +38,21 @@ describe('DiscussionNotes', () => { slots: { 'avatar-badge': '<span class="avatar-badge-slot-content" />', }, + provide: { + glFeatures: { multilineComments: true, ...features }, + }, }); }; + beforeEach(() => { + store = createStore(); + store.dispatch('setNoteableData', noteableDataMock); + store.dispatch('setNotesData', notesDataMock); + }); + afterEach(() => { wrapper.destroy(); + wrapper = null; }); describe('rendering', () => { @@ -160,6 +177,26 @@ describe('DiscussionNotes', () => { }); }); + describe.each` + desc | props | features | event | expectedCalls + ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]} + ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]} + ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]} + ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]} + ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]} + ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]} + `('$desc and features $features', ({ props, event, features, expectedCalls }) => { + beforeEach(() => { + createComponent(props, features); + jest.spyOn(store, 'dispatch'); + }); + + it(`calls store ${expectedCalls.length} times on ${event}`, () => { + getList().dispatchEvent(new MouseEvent(event)); + expect(store.dispatch.mock.calls).toEqual(expectedCalls); + }); + }); + describe('componentData', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index fc238feb974..b0b43075c3d 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -92,8 +92,8 @@ describe('issue_note', () => { }); }); - it('should not render multiline comment form unless it is the discussion root', () => { - wrapper.setProps({ discussionRoot: false }); + it('should only render multiline comment form if it has everything it needs', () => { + wrapper.setProps({ line: { line_code: '' } }); wrapper.vm.isEditing = true; return wrapper.vm.$nextTick().then(() => { diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 52aef73ac77..f6bb7acee57 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -374,6 +374,19 @@ RSpec.describe SearchService do subject(:result) { search_service.search_objects } + shared_examples "redaction limits N+1 queries" do |limit:| + it 'does not exceed the query limit' do + # issuing the query to remove the data loading call + unredacted_results.to_a + + # only the calls from the redaction are left + query = ActiveRecord::QueryRecorder.new { result } + + # these are the project authorization calls, which are not preloaded + expect(query.count).to be <= limit + end + end + def found_blob(project) Gitlab::Search::FoundBlob.new(project: project) end @@ -427,6 +440,12 @@ RSpec.describe SearchService do it 'redacts the inaccessible merge request' do expect(result).to contain_exactly(readable) end + + context 'with :with_api_entity_associations' do + let(:unredacted_results) { ar_relation(MergeRequest.with_api_entity_associations, readable, unreadable) } + + it_behaves_like "redaction limits N+1 queries", limit: 7 + end end context 'project repository blobs' do @@ -460,6 +479,10 @@ RSpec.describe SearchService do it 'redacts the inaccessible snippet' do expect(result).to contain_exactly(readable) end + + context 'with :with_api_entity_associations' do + it_behaves_like "redaction limits N+1 queries", limit: 12 + end end context 'personal snippets' do @@ -471,6 +494,10 @@ RSpec.describe SearchService do it 'redacts the inaccessible snippet' do expect(result).to contain_exactly(readable) end + + context 'with :with_api_entity_associations' do + it_behaves_like "redaction limits N+1 queries", limit: 3 + end end context 'commits' do |