Commit 51eca040 authored by Phil Hughes's avatar Phil Hughes

Merge branch '51819-show-feed-toggle-under-system-notes' into 'master'

Add support for toggling discussion filter from notes section

Closes gitlab-ee#7716 and #51819

See merge request gitlab-org/gitlab-ce!25426
parents 8baf9e5f 2dd22ecb
......@@ -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"
......
<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>
......@@ -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" />
......
......@@ -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',
};
......@@ -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;
......
---
title: Add support for toggling discussion filter from notes section
merge_request: 25426
author:
type: added
......@@ -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 ""
......
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);
});
});
});
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');
});
......
......@@ -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');
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment