diff options
21 files changed, 158 insertions, 21 deletions
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 6e8f43048d1..affa2d1b574 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,7 +1,8 @@ <script> import $ from 'jquery'; -import Icon from '~/vue_shared/components/icon.vue'; import { mapGetters, mapActions } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE } from '../constants'; export default { components: { @@ -12,14 +13,17 @@ export default { type: Array, required: true, }, - defaultValue: { + selectedValue: { type: Number, default: null, required: false, }, }, data() { - return { currentValue: this.defaultValue }; + return { + currentValue: this.selectedValue, + defaultValue: DISCUSSION_FILTERS_DEFAULT_VALUE, + }; }, computed: { ...mapGetters(['getNotesDataByProp']), @@ -28,8 +32,11 @@ export default { return this.filters.find(filter => filter.value === this.currentValue); }, }, + mounted() { + this.toggleCommentsForm(); + }, methods: { - ...mapActions(['filterDiscussion']), + ...mapActions(['filterDiscussion', 'setCommentsDisabled']), selectFilter(value) { const filter = parseInt(value, 10); @@ -39,6 +46,10 @@ export default { if (filter === this.currentValue) return; this.currentValue = filter; this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter }); + this.toggleCommentsForm(); + }, + toggleCommentsForm() { + this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); }, }, }; @@ -73,6 +84,10 @@ export default { > {{ filter.title }} </button> + <div + v-if="filter.value === defaultValue" + class="dropdown-divider" + ></div> </li> </ul> </div> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 7514ce8a1eb..ed5ac112dc0 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -60,6 +60,7 @@ export default { 'getNotesDataByProp', 'discussionCount', 'isLoading', + 'commentsDisabled', ]), noteableType() { return this.noteableData.noteableType; @@ -206,6 +207,7 @@ export default { </ul> <comment-form + v-if="!commentsDisabled" :noteable-type="noteableType" :markdown-version="markdownVersion" /> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 2c3e07c0506..3147dc64c27 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -15,6 +15,8 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const DESCRIPTION_TYPE = 'changed the description'; +export const HISTORY_ONLY_FILTER_VALUE = 2; +export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 06eadaeea0e..5c5f38a3fb0 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -6,7 +6,7 @@ export default store => { if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; - const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null; + const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filters = Object.keys(filterValues).map(entry => ({ title: entry, @@ -24,7 +24,7 @@ export default store => { return createElement('discussion-filter', { props: { filters, - defaultValue, + selectedValue, }, }); }, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b5dd49bc6c9..88739ffb083 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -364,5 +364,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter }) => { }); }; +export const setCommentsDisabled = ({ commit }, data) => { + commit(types.DISABLE_COMMENTS, data); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index e4f36154fcd..8df95c279eb 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -192,5 +192,7 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => { return getters.unresolvedDiscussionsIdsByDate[0]; }; +export const commentsDisabled = state => state.commentsDisabled; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 400142668ea..8aea269ea7d 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -21,6 +21,7 @@ export default () => ({ noteableData: { current_user: {}, }, + commentsDisabled: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 2fa53aef1d4..dfbf3b7b34b 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -15,6 +15,7 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; +export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 65085452139..c8d9e196103 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -225,4 +225,8 @@ export default { discussion.truncated_diff_lines = diffLines; }, + + [types.DISABLE_COMMENTS](state, value) { + state.commentsDisabled = value; + }, }; diff --git a/app/models/note.rb b/app/models/note.rb index 990689a95f5..592efb714f3 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -117,6 +117,8 @@ class Note < ActiveRecord::Base case notes_filter when UserPreference::NOTES_FILTERS[:only_comments] user + when UserPreference::NOTES_FILTERS[:only_activity] + system else all end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 6cd91abc261..32d0407800f 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -4,7 +4,7 @@ class UserPreference < ActiveRecord::Base # We could use enums, but Rails 4 doesn't support multiple # enum options with same name for multiple fields, also it creates # extra methods that aren't really needed here. - NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze + NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze belongs_to :user @@ -14,7 +14,8 @@ class UserPreference < ActiveRecord::Base def notes_filters { s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes], - s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments] + s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments], + s_('Notes|Show history only') => NOTES_FILTERS[:only_activity] } end end diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb index fbdaab459b3..b99f80424db 100644 --- a/app/serializers/user_preference_entity.rb +++ b/app/serializers/user_preference_entity.rb @@ -7,4 +7,8 @@ class UserPreferenceEntity < Grape::Entity expose :notes_filters do |user_preference| UserPreference.notes_filters end + + expose :default_notes_filter do |user_preference| + UserPreference::NOTES_FILTERS[:all_notes] + end end diff --git a/changelogs/unreleased/issue_51323.yml b/changelogs/unreleased/issue_51323.yml new file mode 100644 index 00000000000..b0e83e303d1 --- /dev/null +++ b/changelogs/unreleased/issue_51323.yml @@ -0,0 +1,5 @@ +--- +title: Add 'only history' option to notes filter +merge_request: +author: +type: changed diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 708e74941cd..2f4b0e900c3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4165,6 +4165,9 @@ msgstr "" msgid "Notes|Show comments only" msgstr "" +msgid "Notes|Show history only" +msgstr "" + msgid "Notification events" msgstr "" diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index de9974c45e1..b51f1955ac4 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -13,7 +13,7 @@ describe NotesFinder do let!(:comment) { create(:note_on_issue, project: project) } let!(:system_note) { create(:note_on_issue, project: project, system: true) } - it 'filters system notes' do + it 'returns only user notes when using only_comments filter' do finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments]) notes = finder.execute @@ -21,6 +21,14 @@ describe NotesFinder do expect(notes).to match_array(comment) end + it 'returns only system notes when using only_activity filters' do + finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_activity]) + + notes = finder.execute + + expect(notes).to match_array(system_note) + end + it 'gets all notes' do finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity]) diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index a81bdf618a3..9070d968cfd 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -19,7 +19,7 @@ describe('DiscussionFilter component', () => { }, ]; const Component = Vue.extend(DiscussionFilter); - const defaultValue = discussionFiltersMock[0].value; + const selectedValue = discussionFiltersMock[0].value; store.state.discussions = discussions; vm = mountComponentWithStore(Component, { @@ -27,7 +27,7 @@ describe('DiscussionFilter component', () => { store, props: { filters: discussionFiltersMock, - defaultValue, + selectedValue, }, }); }); @@ -63,4 +63,24 @@ describe('DiscussionFilter component', () => { expect(vm.filterDiscussion).not.toHaveBeenCalled(); }); + + it('disables commenting when "Show history only" filter is applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(true); + }); + + it('enables commenting when "Show history only" filter is not applied', () => { + const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + filterItem.click(); + + expect(vm.$store.state.commentsDisabled).toBe(false); + }); + + it('renders a dropdown divider for the default filter', () => { + const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child'); + + expect(defaultFilter.lastChild.classList).toContain('dropdown-divider'); + }); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 3e289a6b8e6..0081f42c330 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -121,6 +121,13 @@ describe('note_app', () => { ).toEqual('Write a comment or drag your files hereā¦'); }); + it('should not render form when commenting is disabled', () => { + store.state.commentsDisabled = true; + vm = mountComponent(); + + expect(vm.$el.querySelector('.js-main-target-form')).toEqual(null); + }); + it('should render form comment button as disabled', () => { expect(vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled')).toEqual( 'disabled', diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index f4643fd55ed..0c0bc45b201 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -509,4 +509,17 @@ describe('Actions Notes Store', () => { expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated'); }); }); + + describe('setCommentsDisabled', () => { + it('should set comments disabled state', done => { + testAction( + actions.setCommentsDisabled, + true, + null, + [{ type: 'DISABLE_COMMENTS', payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 380ab59099d..461de5a3106 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -427,4 +427,14 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].expanded).toBe(true); }); }); + + describe('DISABLE_COMMENTS', () => { + it('should set comments disabled state', () => { + const state = {}; + + mutations.DISABLE_COMMENTS(state, true); + + expect(state.commentsDisabled).toEqual(true); + }); + }); }); diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index 64d9d9a78b4..2898613545c 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -6,22 +6,43 @@ describe UserPreference do describe '#set_notes_filter' do let(:issuable) { build_stubbed(:issue) } let(:user_preference) { create(:user_preference) } - let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] } - it 'returns updated discussion filter' do - filter_name = - user_preference.set_notes_filter(only_comments, issuable) + shared_examples 'setting system notes' do + it 'returns updated discussion filter' do + filter_name = + user_preference.set_notes_filter(filter, issuable) + + expect(filter_name).to eq(filter) + end + + it 'updates discussion filter for issuable class' do + user_preference.set_notes_filter(filter, issuable) + + expect(user_preference.reload.issue_notes_filter).to eq(filter) + end + end + + context 'when filter is set to all notes' do + let(:filter) { described_class::NOTES_FILTERS[:all_notes] } + + it_behaves_like 'setting system notes' + end + + context 'when filter is set to only comments' do + let(:filter) { described_class::NOTES_FILTERS[:only_comments] } - expect(filter_name).to eq(only_comments) + it_behaves_like 'setting system notes' end - it 'updates discussion filter for issuable class' do - user_preference.set_notes_filter(only_comments, issuable) + context 'when filter is set to only activity' do + let(:filter) { described_class::NOTES_FILTERS[:only_activity] } - expect(user_preference.reload.issue_notes_filter).to eq(only_comments) + it_behaves_like 'setting system notes' end context 'when notes_filter parameter is invalid' do + let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] } + it 'returns the current notes filter' do user_preference.set_notes_filter(only_comments, issuable) diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index 9c9d7ad781e..95e69328080 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -34,12 +34,24 @@ shared_examples 'issuable notes filter' do expect(user.reload.notes_filter_for(issuable)).to eq(0) end - it 'returns no system note' do + it 'returns only user comments' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable) get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid + discussions = JSON.parse(response.body) - expect(JSON.parse(response.body).count).to eq(1) + expect(discussions.count).to eq(1) + expect(discussions.first["notes"].first["system"]).to be(false) + end + + it 'returns only activity notes' do + user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable) + + get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid + discussions = JSON.parse(response.body) + + expect(discussions.count).to eq(1) + expect(discussions.first["notes"].first["system"]).to be(true) end context 'when filter is set to "only_comments"' do |