diff options
10 files changed, 253 insertions, 39 deletions
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index e03d6e9cd02..31164f74201 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -7,7 +7,9 @@ import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE, DISCUSSION_TAB_LABEL, + DISCUSSION_FILTER_TYPES, } from '../constants'; +import notesEventHub from '../event_hub'; export default { components: { @@ -46,6 +48,7 @@ export default { this.toggleFilters(currentTab); } + notesEventHub.$on('dropdownSelect', this.selectFilter); window.addEventListener('hashchange', this.handleLocationHash); this.handleLocationHash(); }, @@ -53,6 +56,7 @@ export default { this.toggleCommentsForm(); }, destroyed() { + notesEventHub.$off('dropdownSelect', this.selectFilter); window.removeEventListener('hashchange', this.handleLocationHash); }, methods: { @@ -86,12 +90,23 @@ export default { this.setTargetNoteHash(hash); } }, + filterType(value) { + if (value === 0) { + return DISCUSSION_FILTER_TYPES.ALL; + } else if (value === 1) { + return DISCUSSION_FILTER_TYPES.COMMENTS; + } + return DISCUSSION_FILTER_TYPES.HISTORY; + }, }, }; </script> <template> - <div v-if="displayFilters" class="discussion-filter-container d-inline-block align-bottom"> + <div + v-if="displayFilters" + class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom" + > <button id="discussion-filter-dropdown" ref="dropdownToggle" @@ -102,12 +117,17 @@ export default { {{ currentFilter.title }} <icon name="chevron-down" /> </button> <div + ref="dropdownMenu" class="dropdown-menu dropdown-menu-selectable dropdown-menu-right" aria-labelledby="discussion-filter-dropdown" > <div class="dropdown-content"> <ul> - <li v-for="filter in filters" :key="filter.value"> + <li + v-for="filter in filters" + :key="filter.value" + :data-filter-type="filterType(filter.value)" + > <button :class="{ 'is-active': filter.value === currentValue }" class="qa-filter-options" diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue new file mode 100644 index 00000000000..46661e06f6d --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue @@ -0,0 +1,52 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import { __, sprintf } from '~/locale'; + +import notesEventHub from '../event_hub'; + +export default { + components: { + GlButton, + Icon, + }, + computed: { + timelineContent() { + return sprintf( + __( + "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options.", + ), + { + startTag: `<b>`, + endTag: `</b>`, + }, + false, + ); + }, + }, + methods: { + selectFilter(value) { + notesEventHub.$emit('dropdownSelect', value); + }, + }, +}; +</script> + +<template> + <li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"> + <div class="timeline-icon"> + <icon name="comment" /> + </div> + <div class="timeline-content"> + <div v-html="timelineContent"></div> + <div class="discussion-filter-actions mt-2"> + <gl-button variant="default" @click="selectFilter(0)"> + {{ __('Show all activity') }} + </gl-button> + <gl-button variant="default" @click="selectFilter(1)"> + {{ __('Show comments only') }} + </gl-button> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 8d3f6d902f8..e2bd59f7631 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -6,6 +6,7 @@ import * as constants from '../constants'; import eventHub from '../event_hub'; import noteableNote from './noteable_note.vue'; import noteableDiscussion from './noteable_discussion.vue'; +import discussionFilterNote from './discussion_filter_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue'; import commentForm from './comment_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; @@ -24,6 +25,7 @@ export default { placeholderNote, placeholderSystemNote, skeletonLoadingContainer, + discussionFilterNote, }, props: { noteableData: { @@ -235,6 +237,7 @@ export default { :help-page-path="helpPagePath" /> </template> + <discussion-filter-note v-show="commentsDisabled" /> </ul> <comment-form v-if="!commentsDisabled" :noteable-type="noteableType" /> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 78d365fe94b..fba3db8542c 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -24,3 +24,9 @@ export const NOTEABLE_TYPE_MAPPING = { MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE, Epic: EPIC_NOTEABLE_TYPE, }; + +export const DISCUSSION_FILTER_TYPES = { + ALL: 'all', + COMMENTS: 'comments', + HISTORY: 'history', +}; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7e7eff1346a..1198b9ea143 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -4,7 +4,7 @@ $note-form-margin-left: 72px; @mixin vertical-line($left) { &::before { - content: ''; + content: ""; border-left: 2px solid $gray-100; position: absolute; top: 0; @@ -53,12 +53,12 @@ $note-form-margin-left: 72px; &.note-form { margin-left: 0; - @include notes-media('min', map-get($grid-breakpoints, md)) { + @include notes-media("min", map-get($grid-breakpoints, md)) { margin-left: $note-form-margin-left; } .timeline-icon { - @include notes-media('min', map-get($grid-breakpoints, sm)) { + @include notes-media("min", map-get($grid-breakpoints, sm)) { margin-left: -$note-icon-gutter-width; } } @@ -242,7 +242,7 @@ $note-form-margin-left: 72px; } .note-header { - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media("max", map-get($grid-breakpoints, xs)) { .inline { display: block; } @@ -303,28 +303,8 @@ $note-form-margin-left: 72px; } } - .timeline-icon { - float: left; - display: flex; - align-items: center; - background-color: $white-light; - width: $system-note-icon-size; - height: $system-note-icon-size; - border: 1px solid $border-color; - border-radius: $system-note-icon-size; - margin: -6px $gl-padding 0 0; - - svg { - width: $system-note-svg-size; - height: $system-note-svg-size; - fill: $gray-darkest; - display: block; - margin: 0 auto; - } - } - .timeline-content { - @include notes-media('min', map-get($grid-breakpoints, sm)) { + @include notes-media("min", map-get($grid-breakpoints, sm)) { margin-left: 30px; } } @@ -368,7 +348,7 @@ $note-form-margin-left: 72px; } &::after { - content: ''; + content: ""; height: 70px; position: absolute; left: $gl-padding-24; @@ -380,6 +360,37 @@ $note-form-margin-left: 72px; } } } + + .system-note, + .discussion-filter-note { + .timeline-icon { + float: left; + display: flex; + align-items: center; + background-color: $white-light; + width: $system-note-icon-size; + height: $system-note-icon-size; + border: 1px solid $border-color; + border-radius: $system-note-icon-size; + margin: -6px $gl-padding 0 0; + + svg { + width: $system-note-svg-size; + height: $system-note-svg-size; + fill: $gray-darkest; + display: block; + margin: 0 auto; + } + } + } + + .discussion-filter-note { + .timeline-icon { + width: $system-note-icon-size + 6; + height: $system-note-icon-size + 6; + margin-top: -8px; + } + } } // Diff code in discussion view @@ -579,7 +590,7 @@ $note-form-margin-left: 72px; .note-headline-light { display: inline; - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media("max", map-get($grid-breakpoints, xs)) { display: block; } } @@ -645,7 +656,7 @@ $note-form-margin-left: 72px; margin-left: 10px; color: $gray-darkest; - @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { + @include notes-media("max", map-get($grid-breakpoints, sm) - 1) { float: none; margin-left: 0; } @@ -764,7 +775,7 @@ $note-form-margin-left: 72px; } .line-resolve-all-container { - @include notes-media('min', map-get($grid-breakpoints, sm)) { + @include notes-media("min", map-get($grid-breakpoints, sm)) { margin-right: 0; } @@ -905,7 +916,6 @@ $note-form-margin-left: 72px; } .discussion-filter-container { - .btn > svg { width: $gl-col-padding; height: $gl-col-padding; @@ -927,7 +937,6 @@ $note-form-margin-left: 72px; //This needs to be deleted when Snippet/Commit comments are convered to Vue // See https://gitlab.com/gitlab-org/gitlab-ce/issues/53918#note_117038785 .unstyled-comments { - .discussion-header { padding: $gl-padding; border-bottom: 1px solid $border-color; diff --git a/changelogs/unreleased/51819-show-feed-toggle-under-system-notes.yml b/changelogs/unreleased/51819-show-feed-toggle-under-system-notes.yml new file mode 100644 index 00000000000..76ea4149c56 --- /dev/null +++ b/changelogs/unreleased/51819-show-feed-toggle-under-system-notes.yml @@ -0,0 +1,5 @@ +--- +title: Add support for toggling discussion filter from notes section +merge_request: 25426 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bd45b01d463..150e16ed67f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6743,9 +6743,15 @@ msgstr "" msgid "Sherlock Transactions" msgstr "" +msgid "Show all activity" +msgstr "" + msgid "Show command" msgstr "" +msgid "Show comments only" +msgstr "" + msgid "Show complete raw log" msgstr "" @@ -8654,6 +8660,9 @@ msgstr "" msgid "You'll need to use different branch names to get a valid comparison." msgstr "" +msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options." +msgstr "" + msgid "You're receiving this email because %{reason}." msgstr "" diff --git a/spec/javascripts/notes/components/discussion_filter_note_spec.js b/spec/javascripts/notes/components/discussion_filter_note_spec.js new file mode 100644 index 00000000000..52d2e7ce947 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_filter_note_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; +import eventHub from '~/notes/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('DiscussionFilterNote component', () => { + let vm; + + const createComponent = () => { + const Component = Vue.extend(DiscussionFilterNote); + + return mountComponent(Component); + }; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('timelineContent', () => { + it('returns string containing instruction for switching feed type', () => { + expect(vm.timelineContent).toBe( + "You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.", + ); + }); + }); + }); + + describe('methods', () => { + describe('selectFilter', () => { + it('emits `dropdownSelect` event on `eventHub` with provided param', () => { + spyOn(eventHub, '$emit'); + + vm.selectFilter(1); + + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); + }); + }); + }); + + describe('template', () => { + it('renders component container element', () => { + expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); + }); + + it('renders comment icon element', () => { + expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( + 'comment', + ); + }); + + it('renders filter information note', () => { + expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('renders filter buttons', () => { + const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); + + expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( + 'Show all activity', + ); + + expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( + 'Show comments only', + ); + }); + + it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); + spyOn(vm, 'selectFilter'); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(0); + }); + + it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); + spyOn(vm, 'selectFilter'); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 91dab58ba7f..1c366aee8e2 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import createStore from '~/notes/stores'; import DiscussionFilter from '~/notes/components/discussion_filter.vue'; -import { DISCUSSION_FILTERS_DEFAULT_VALUE } from '~/notes/constants'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { discussionFiltersMock, discussionMock } from '../mock_data'; @@ -54,14 +54,18 @@ describe('DiscussionFilter component', () => { }); it('updates to the selected item', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`, + ); filterItem.click(); expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim()); }); it('only updates when selected filter changes', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`, + ); spyOn(vm, 'filterDiscussion'); filterItem.click(); @@ -70,21 +74,27 @@ describe('DiscussionFilter component', () => { }); it('disables commenting when "Show history only" filter is applied', () => { - const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] 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'); + const filterItem = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] 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'); + const defaultFilter = vm.$el.querySelector( + `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`, + ); 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 82f58dafc78..d716ece3766 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -126,6 +126,13 @@ describe('note_app', () => { expect(wrapper.find('.js-main-target-form').exists()).toBe(false); }); + it('should render discussion filter note `commentsDisabled` is true', () => { + store.state.commentsDisabled = true; + wrapper = mountComponent(); + + expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true); + }); + it('should render form comment button as disabled', () => { expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled'); }); |