Commit eb2f29be authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Peter Leitzen

Add incidents comments timeline view

parent 28ab1364
...@@ -5,6 +5,7 @@ import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility ...@@ -5,6 +5,7 @@ import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility
import { import {
DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTERS_DEFAULT_VALUE,
HISTORY_ONLY_FILTER_VALUE, HISTORY_ONLY_FILTER_VALUE,
COMMENTS_ONLY_FILTER_VALUE,
DISCUSSION_TAB_LABEL, DISCUSSION_TAB_LABEL,
DISCUSSION_FILTER_TYPES, DISCUSSION_FILTER_TYPES,
NOTE_UNDERSCORE, NOTE_UNDERSCORE,
...@@ -38,7 +39,7 @@ export default { ...@@ -38,7 +39,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapGetters(['getNotesDataByProp']), ...mapGetters(['getNotesDataByProp', 'timelineEnabled']),
currentFilter() { currentFilter() {
if (!this.currentValue) return this.filters[0]; if (!this.currentValue) return this.filters[0];
return this.filters.find(filter => filter.value === this.currentValue); return this.filters.find(filter => filter.value === this.currentValue);
...@@ -63,11 +64,20 @@ export default { ...@@ -63,11 +64,20 @@ export default {
window.removeEventListener('hashchange', this.handleLocationHash); window.removeEventListener('hashchange', this.handleLocationHash);
}, },
methods: { methods: {
...mapActions(['filterDiscussion', 'setCommentsDisabled', 'setTargetNoteHash']), ...mapActions([
'filterDiscussion',
'setCommentsDisabled',
'setTargetNoteHash',
'setTimelineView',
]),
selectFilter(value, persistFilter = true) { selectFilter(value, persistFilter = true) {
const filter = parseInt(value, 10); const filter = parseInt(value, 10);
if (filter === this.currentValue) return; if (filter === this.currentValue) return;
if (this.timelineEnabled && filter !== COMMENTS_ONLY_FILTER_VALUE) {
this.setTimelineView(false);
}
this.currentValue = filter; this.currentValue = filter;
this.filterDiscussion({ this.filterDiscussion({
path: this.getNotesDataByProp('discussionsPath'), path: this.getNotesDataByProp('discussionsPath'),
......
...@@ -73,6 +73,7 @@ export default { ...@@ -73,6 +73,7 @@ export default {
'userCanReply', 'userCanReply',
'discussionTabCounter', 'discussionTabCounter',
'sortDirection', 'sortDirection',
'timelineEnabled',
]), ]),
sortDirDesc() { sortDirDesc() {
return this.sortDirection === constants.DESC; return this.sortDirection === constants.DESC;
...@@ -95,7 +96,7 @@ export default { ...@@ -95,7 +96,7 @@ export default {
return this.discussions; return this.discussions;
}, },
canReply() { canReply() {
return this.userCanReply && !this.commentsDisabled; return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled;
}, },
slotKeys() { slotKeys() {
return this.sortDirDesc ? ['form', 'comments'] : ['comments', 'form']; return this.sortDirDesc ? ['form', 'comments'] : ['comments', 'form'];
...@@ -252,7 +253,7 @@ export default { ...@@ -252,7 +253,7 @@ export default {
<ordered-layout :slot-keys="slotKeys"> <ordered-layout :slot-keys="slotKeys">
<template #form> <template #form>
<comment-form <comment-form
v-if="!commentsDisabled" v-if="!(commentsDisabled || timelineEnabled)"
class="js-comment-form" class="js-comment-form"
:noteable-type="noteableType" :noteable-type="noteableType"
/> />
......
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
computed: { computed: {
...mapGetters(['sortDirection', 'noteableType']), ...mapGetters(['sortDirection', 'persistSortOrder', 'noteableType']),
selectedOption() { selectedOption() {
return SORT_OPTIONS.find(({ key }) => this.sortDirection === key); return SORT_OPTIONS.find(({ key }) => this.sortDirection === key);
}, },
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
return; return;
} }
this.setDiscussionSortDirection(direction); this.setDiscussionSortDirection({ direction });
this.track('change_discussion_sort_direction', { property: direction }); this.track('change_discussion_sort_direction', { property: direction });
}, },
isDropdownItemActive(sortDir) { isDropdownItemActive(sortDir) {
...@@ -53,7 +53,8 @@ export default { ...@@ -53,7 +53,8 @@ export default {
<local-storage-sync <local-storage-sync
:value="sortDirection" :value="sortDirection"
:storage-key="storageKey" :storage-key="storageKey"
@input="setDiscussionSortDirection" :persist="persistSortOrder"
@input="setDiscussionSortDirection({ direction: $event })"
/> />
<gl-dropdown <gl-dropdown
:text="dropdownText" :text="dropdownText"
......
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import { COMMENTS_ONLY_FILTER_VALUE, DESC } from '../constants';
import notesEventHub from '../event_hub';
export const timelineEnabledTooltip = s__('Timeline|Turn timeline view off');
export const timelineDisabledTooltip = s__('Timeline|Turn timeline view on');
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
computed: {
...mapGetters(['timelineEnabled', 'sortDirection']),
tooltip() {
return this.timelineEnabled ? timelineEnabledTooltip : timelineDisabledTooltip;
},
},
methods: {
...mapActions(['setTimelineView', 'setDiscussionSortDirection']),
setSort() {
if (this.timelineEnabled && this.sortDirection !== DESC) {
this.setDiscussionSortDirection({ direction: DESC, persist: false });
}
},
setFilter() {
notesEventHub.$emit('dropdownSelect', COMMENTS_ONLY_FILTER_VALUE, false);
},
toggleTimeline(event) {
event.currentTarget.blur();
this.setTimelineView(!this.timelineEnabled);
this.setSort();
this.setFilter();
},
},
};
</script>
<template>
<gl-button
v-gl-tooltip
icon="comments"
size="small"
:selected="timelineEnabled"
:title="tooltip"
:aria-label="tooltip"
class="gl-mr-3"
@click="toggleTimeline"
/>
</template>
...@@ -14,8 +14,9 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest'; ...@@ -14,8 +14,9 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest';
export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const RESOLVE_NOTE_METHOD_NAME = 'post';
export const DESCRIPTION_TYPE = 'changed the description'; export const DESCRIPTION_TYPE = 'changed the description';
export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const COMMENTS_ONLY_FILTER_VALUE = 1;
export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_TAB_LABEL = 'show'; export const DISCUSSION_TAB_LABEL = 'show';
export const NOTE_UNDERSCORE = 'note_'; export const NOTE_UNDERSCORE = 'note_';
export const TIME_DIFFERENCE_VALUE = 10; export const TIME_DIFFERENCE_VALUE = 10;
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters'; import initDiscussionFilters from './discussion_filters';
import initSortDiscussions from './sort_discussions'; import initSortDiscussions from './sort_discussions';
import initTimelineToggle from './timeline';
import { store } from './stores'; import { store } from './stores';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
...@@ -59,4 +60,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -59,4 +60,5 @@ document.addEventListener('DOMContentLoaded', () => {
initDiscussionFilters(store); initDiscussionFilters(store);
initSortDiscussions(store); initSortDiscussions(store);
initTimelineToggle(store);
}); });
...@@ -99,8 +99,12 @@ export const updateDiscussion = ({ commit, state }, discussion) => { ...@@ -99,8 +99,12 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
return utils.findNoteObjectById(state.discussions, discussion.id); return utils.findNoteObjectById(state.discussions, discussion.id);
}; };
export const setDiscussionSortDirection = ({ commit }, direction) => { export const setDiscussionSortDirection = ({ commit }, { direction, persist = true }) => {
commit(types.SET_DISCUSSIONS_SORT, direction); commit(types.SET_DISCUSSIONS_SORT, { direction, persist });
};
export const setTimelineView = ({ commit }, enabled) => {
commit(types.SET_TIMELINE_VIEW, enabled);
}; };
export const setSelectedCommentPosition = ({ commit }, position) => { export const setSelectedCommentPosition = ({ commit }, position) => {
......
...@@ -5,6 +5,23 @@ import { collapseSystemNotes } from './collapse_utils'; ...@@ -5,6 +5,23 @@ import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => { export const discussions = state => {
let discussionsInState = clone(state.discussions); let discussionsInState = clone(state.discussions);
// NOTE: not testing bc will be removed when backend is finished. // NOTE: not testing bc will be removed when backend is finished.
if (state.isTimelineEnabled) {
discussionsInState = discussionsInState
.reduce((acc, discussion) => {
const transformedToIndividualNotes = discussion.notes.map(note => ({
...discussion,
id: note.id,
created_at: note.created_at,
individual_note: true,
notes: [note],
}));
return acc.concat(transformedToIndividualNotes);
}, [])
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
}
if (state.discussionSortOrder === constants.DESC) { if (state.discussionSortOrder === constants.DESC) {
discussionsInState = discussionsInState.reverse(); discussionsInState = discussionsInState.reverse();
} }
...@@ -27,6 +44,10 @@ export const isNotesFetched = state => state.isNotesFetched; ...@@ -27,6 +44,10 @@ export const isNotesFetched = state => state.isNotesFetched;
export const sortDirection = state => state.discussionSortOrder; export const sortDirection = state => state.discussionSortOrder;
export const persistSortOrder = state => state.persistSortOrder;
export const timelineEnabled = state => state.isTimelineEnabled;
export const isLoading = state => state.isLoading; export const isLoading = state => state.isLoading;
export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNotesDataByProp = state => prop => state.notesData[prop];
......
...@@ -7,6 +7,7 @@ export default () => ({ ...@@ -7,6 +7,7 @@ export default () => ({
state: { state: {
discussions: [], discussions: [],
discussionSortOrder: ASC, discussionSortOrder: ASC,
persistSortOrder: true,
convertedDisscussionIds: [], convertedDisscussionIds: [],
targetNoteHash: null, targetNoteHash: null,
lastFetchedAt: null, lastFetchedAt: null,
...@@ -45,6 +46,7 @@ export default () => ({ ...@@ -45,6 +46,7 @@ export default () => ({
resolvableDiscussionsCount: 0, resolvableDiscussionsCount: 0,
unresolvedDiscussionsCount: 0, unresolvedDiscussionsCount: 0,
descriptionVersions: {}, descriptionVersions: {},
isTimelineEnabled: false,
}, },
actions, actions,
getters, getters,
......
...@@ -34,6 +34,7 @@ export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS'; ...@@ -34,6 +34,7 @@ export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS';
export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS'; export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS';
export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID';
export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT'; export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT';
export const SET_TIMELINE_VIEW = 'SET_TIMELINE_VIEW';
export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION'; export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION';
export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER'; export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER';
export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS'; export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
......
...@@ -313,8 +313,13 @@ export default { ...@@ -313,8 +313,13 @@ export default {
discussion.truncated_diff_lines = utils.prepareDiffLines(diffLines); discussion.truncated_diff_lines = utils.prepareDiffLines(diffLines);
}, },
[types.SET_DISCUSSIONS_SORT](state, sort) { [types.SET_DISCUSSIONS_SORT](state, { direction, persist }) {
state.discussionSortOrder = sort; state.discussionSortOrder = direction;
state.persistSortOrder = persist;
},
[types.SET_TIMELINE_VIEW](state, value) {
state.isTimelineEnabled = value;
}, },
[types.SET_SELECTED_COMMENT_POSITION](state, position) { [types.SET_SELECTED_COMMENT_POSITION](state, position) {
......
import Vue from 'vue';
import TimelineToggle from './components/timeline_toggle.vue';
export default function initTimelineToggle(store) {
const el = document.getElementById('js-incidents-timeline-toggle');
if (!el) return null;
return new Vue({
el,
store,
render(createElement) {
return createElement(TimelineToggle);
},
});
}
...@@ -17,6 +17,11 @@ export default { ...@@ -17,6 +17,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
persist: {
type: Boolean,
required: false,
default: true,
},
}, },
watch: { watch: {
value(newVal) { value(newVal) {
...@@ -52,6 +57,8 @@ export default { ...@@ -52,6 +57,8 @@ export default {
} }
}, },
saveValue(val) { saveValue(val) {
if (!this.persist) return;
localStorage.setItem(this.storageKey, val); localStorage.setItem(this.storageKey, val);
}, },
serialize(val) { serialize(val) {
......
...@@ -91,6 +91,7 @@ ...@@ -91,6 +91,7 @@
.js-noteable-awards .js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true = render 'award_emoji/awards_block', awardable: @issue, inline: true
.new-branch-col .new-branch-col
= render_if_exists "projects/issues/timeline_toggle", issue: @issue
#js-vue-sort-issue-discussions #js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } } #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button? = render 'new_branch' if show_new_branch_button?
......
---
title: Add timeline toggle button for incidents comments
merge_request: 43302
author:
type: added
...@@ -50,5 +50,9 @@ module EE ...@@ -50,5 +50,9 @@ module EE
# than the filter epic id on params # than the filter epic id on params
epic_id.to_i != issue.epic_issue.epic_id epic_id.to_i != issue.epic_issue.epic_id
end end
def show_timeline_view_toggle?(issue)
issue.incident? && issue.project.feature_available?(:incident_timeline_view)
end
end end
end end
...@@ -116,6 +116,7 @@ class License < ApplicationRecord ...@@ -116,6 +116,7 @@ class License < ApplicationRecord
minimal_access_role minimal_access_role
unprotection_restrictions unprotection_restrictions
ci_project_subscriptions ci_project_subscriptions
incident_timeline_view
] ]
EEP_FEATURES.freeze EEP_FEATURES.freeze
......
- if show_timeline_view_toggle?(issue)
#js-incidents-timeline-toggle
...@@ -69,4 +69,24 @@ RSpec.describe EE::IssuesHelper do ...@@ -69,4 +69,24 @@ RSpec.describe EE::IssuesHelper do
expect(helper.issue_in_subepic?(issue, 'subepic_id')).to be_truthy expect(helper.issue_in_subepic?(issue, 'subepic_id')).to be_truthy
end end
end end
describe '#show_timeline_view_toggle?' do
subject { helper.show_timeline_view_toggle?(issue) }
it { is_expected.to be_falsy }
context 'issue is an incident' do
let(:issue) { build_stubbed(:incident) }
it { is_expected.to be_falsy }
context 'with license' do
before do
stub_licensed_features(incident_timeline_view: true)
end
it { is_expected.to be_truthy }
end
end
end
end end
...@@ -20,4 +20,16 @@ RSpec.describe 'projects/issues/show' do ...@@ -20,4 +20,16 @@ RSpec.describe 'projects/issues/show' do
expect(rendered).to have_selector('[aria-label="GitLab Team Member"]') expect(rendered).to have_selector('[aria-label="GitLab Team Member"]')
end end
end end
context 'for applicable incidents' do
before do
allow(view).to receive(:show_timeline_view_toggle?).and_return(true)
end
it 'renders a timeline toggle' do
render
expect(rendered).to have_selector('#js-incidents-timeline-toggle')
end
end
end end
...@@ -26854,6 +26854,12 @@ msgstr "" ...@@ -26854,6 +26854,12 @@ msgstr ""
msgid "Timeago|right now" msgid "Timeago|right now"
msgstr "" msgstr ""
msgid "Timeline|Turn timeline view off"
msgstr ""
msgid "Timeline|Turn timeline view on"
msgstr ""
msgid "Timeout" msgid "Timeout"
msgstr "" msgstr ""
......
...@@ -25,6 +25,8 @@ describe('DiscussionFilter component', () => { ...@@ -25,6 +25,8 @@ describe('DiscussionFilter component', () => {
const filterDiscussion = jest.fn(); const filterDiscussion = jest.fn();
const findFilter = filterType => wrapper.find(`.dropdown-item[data-filter-type="${filterType}"]`);
const mountComponent = () => { const mountComponent = () => {
const discussions = [ const discussions = [
{ {
...@@ -89,9 +91,7 @@ describe('DiscussionFilter component', () => { ...@@ -89,9 +91,7 @@ describe('DiscussionFilter component', () => {
}); });
it('updates to the selected item', () => { it('updates to the selected item', () => {
const filterItem = wrapper.find( const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL);
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"]`,
);
filterItem.trigger('click'); filterItem.trigger('click');
...@@ -99,29 +99,27 @@ describe('DiscussionFilter component', () => { ...@@ -99,29 +99,27 @@ describe('DiscussionFilter component', () => {
}); });
it('only updates when selected filter changes', () => { it('only updates when selected filter changes', () => {
wrapper findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
.find(
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
)
.trigger('click');
expect(filterDiscussion).not.toHaveBeenCalled(); expect(filterDiscussion).not.toHaveBeenCalled();
}); });
it('disables timeline view if it was enabled', () => {
store.state.isTimelineEnabled = true;
findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
expect(wrapper.vm.$store.state.isTimelineEnabled).toBe(false);
});
it('disables commenting when "Show history only" filter is applied', () => { it('disables commenting when "Show history only" filter is applied', () => {
const filterItem = wrapper.find( findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"]`,
);
filterItem.trigger('click');
expect(wrapper.vm.$store.state.commentsDisabled).toBe(true); expect(wrapper.vm.$store.state.commentsDisabled).toBe(true);
}); });
it('enables commenting when "Show history only" filter is not applied', () => { it('enables commenting when "Show history only" filter is not applied', () => {
const filterItem = wrapper.find( findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
`.discussion-filter-container .dropdown-item[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
);
filterItem.trigger('click');
expect(wrapper.vm.$store.state.commentsDisabled).toBe(false); expect(wrapper.vm.$store.state.commentsDisabled).toBe(false);
}); });
......
...@@ -174,6 +174,23 @@ describe('note_app', () => { ...@@ -174,6 +174,23 @@ describe('note_app', () => {
}); });
}); });
describe('timeline view', () => {
beforeEach(() => {
setFixtures('<div class="js-discussions-count"></div>');
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
store.state.commentsDisabled = false;
store.state.isTimelineEnabled = true;
wrapper = mountComponent();
return waitForDiscussionsRequest();
});
it('should not render comments form', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
});
describe('while fetching data', () => { describe('while fetching data', () => {
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="js-discussions-count"></div>'); setFixtures('<div class="js-discussions-count"></div>');
......
...@@ -46,7 +46,7 @@ describe('Sort Discussion component', () => { ...@@ -46,7 +46,7 @@ describe('Sort Discussion component', () => {
it('calls setDiscussionSortDirection when update is emitted', () => { it('calls setDiscussionSortDirection when update is emitted', () => {
findLocalStorageSync().vm.$emit('input', ASC); findLocalStorageSync().vm.$emit('input', ASC);
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', ASC); expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { direction: ASC });
}); });
}); });
...@@ -57,7 +57,9 @@ describe('Sort Discussion component', () => { ...@@ -57,7 +57,9 @@ describe('Sort Discussion component', () => {
wrapper.find('.js-newest-first').vm.$emit('click'); wrapper.find('.js-newest-first').vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', DESC); expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
direction: DESC,
});
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
property: DESC, property: DESC,
}); });
...@@ -81,7 +83,9 @@ describe('Sort Discussion component', () => { ...@@ -81,7 +83,9 @@ describe('Sort Discussion component', () => {
it('calls the right actions', () => { it('calls the right actions', () => {
wrapper.find('.js-oldest-first').vm.$emit('click'); wrapper.find('.js-oldest-first').vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', ASC); expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
direction: ASC,
});
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', {
property: ASC, property: ASC,
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import Vuex from 'vuex';
import TimelineToggle, {
timelineEnabledTooltip,
timelineDisabledTooltip,
} from '~/notes/components/timeline_toggle.vue';
import createStore from '~/notes/stores';
import { ASC, DESC } from '~/notes/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Timeline toggle', () => {
let wrapper;
let store;
const mockEvent = { currentTarget: { blur: jest.fn() } };
const createComponent = () => {
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(TimelineToggle, {
localVue,
store,
});
};
const findGlButton = () => wrapper.find(GlButton);
beforeEach(() => {
store = createStore();
createComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
store.dispatch.mockReset();
mockEvent.currentTarget.blur.mockReset();
});
describe('ON state', () => {
it('should update timeline flag in the store', () => {
store.state.isTimelineEnabled = false;
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).toHaveBeenCalledWith('setTimelineView', true);
});
it('should set sort direction to DESC if not set', () => {
store.state.isTimelineEnabled = true;
store.state.sortDirection = ASC;
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
direction: DESC,
persist: false,
});
});
it('should set correct UI state', async () => {
store.state.isTimelineEnabled = true;
findGlButton().vm.$emit('click', mockEvent);
await wrapper.vm.$nextTick();
expect(findGlButton().attributes('title')).toBe(timelineEnabledTooltip);
expect(findGlButton().attributes('selected')).toBe('true');
expect(mockEvent.currentTarget.blur).toHaveBeenCalled();
});
});
describe('OFF state', () => {
it('should update timeline flag in the store', () => {
store.state.isTimelineEnabled = true;
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).toHaveBeenCalledWith('setTimelineView', false);
});
it('should NOT update sort direction', () => {
store.state.isTimelineEnabled = false;
findGlButton().vm.$emit('click', mockEvent);
expect(store.dispatch).not.toHaveBeenCalledWith();
});
it('should set correct UI state', async () => {
store.state.isTimelineEnabled = false;
findGlButton().vm.$emit('click', mockEvent);
await wrapper.vm.$nextTick();
expect(findGlButton().attributes('title')).toBe(timelineDisabledTooltip);
expect(findGlButton().attributes('selected')).toBe(undefined);
expect(mockEvent.currentTarget.blur).toHaveBeenCalled();
});
});
});
...@@ -1144,9 +1144,14 @@ describe('Actions Notes Store', () => { ...@@ -1144,9 +1144,14 @@ describe('Actions Notes Store', () => {
it('calls the correct mutation with the correct args', done => { it('calls the correct mutation with the correct args', done => {
testAction( testAction(
actions.setDiscussionSortDirection, actions.setDiscussionSortDirection,
notesConstants.DESC, { direction: notesConstants.DESC, persist: false },
{}, {},
[{ type: mutationTypes.SET_DISCUSSIONS_SORT, payload: notesConstants.DESC }], [
{
type: mutationTypes.SET_DISCUSSIONS_SORT,
payload: { direction: notesConstants.DESC, persist: false },
},
],
[], [],
done, done,
); );
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
noteableDataMock, noteableDataMock,
individualNote, individualNote,
collapseNotesMock, collapseNotesMock,
discussionMock,
discussion1, discussion1,
discussion2, discussion2,
discussion3, discussion3,
...@@ -65,6 +66,18 @@ describe('Getters Notes Store', () => { ...@@ -65,6 +66,18 @@ describe('Getters Notes Store', () => {
it('should return all discussions in the store', () => { it('should return all discussions in the store', () => {
expect(getters.discussions(state)).toEqual([individualNote]); expect(getters.discussions(state)).toEqual([individualNote]);
}); });
it('should transform discussion to individual notes in timeline view', () => {
state.discussions = [discussionMock];
state.isTimelineEnabled = true;
expect(getters.discussions(state).length).toEqual(discussionMock.notes.length);
getters.discussions(state).forEach(discussion => {
expect(discussion.individual_note).toBe(true);
expect(discussion.id).toBe(discussion.notes[0].id);
expect(discussion.created_at).toBe(discussion.notes[0].created_at);
});
});
}); });
describe('resolvedDiscussionsById', () => { describe('resolvedDiscussionsById', () => {
......
...@@ -680,9 +680,10 @@ describe('Notes Store mutations', () => { ...@@ -680,9 +680,10 @@ describe('Notes Store mutations', () => {
}); });
it('sets sort order', () => { it('sets sort order', () => {
mutations.SET_DISCUSSIONS_SORT(state, DESC); mutations.SET_DISCUSSIONS_SORT(state, { direction: DESC, persist: false });
expect(state.discussionSortOrder).toBe(DESC); expect(state.discussionSortOrder).toBe(DESC);
expect(state.persistSortOrder).toBe(false);
}); });
}); });
......
...@@ -126,6 +126,34 @@ describe('Local Storage Sync', () => { ...@@ -126,6 +126,34 @@ describe('Local Storage Sync', () => {
expect(localStorage.getItem(storageKey)).toBe(newValue); expect(localStorage.getItem(storageKey)).toBe(newValue);
}); });
}); });
it('persists the value by default', async () => {
const persistedValue = 'persisted';
createComponent({
props: {
storageKey,
},
});
wrapper.setProps({ value: persistedValue });
await wrapper.vm.$nextTick();
expect(localStorage.getItem(storageKey)).toBe(persistedValue);
});
it('does not save a value if persist is set to false', async () => {
const notPersistedValue = 'notPersisted';
createComponent({
props: {
storageKey,
},
});
wrapper.setProps({ persist: false, value: notPersistedValue });
await wrapper.vm.$nextTick();
expect(localStorage.getItem(storageKey)).not.toBe(notPersistedValue);
});
}); });
describe('with "asJson" prop set to "true"', () => { describe('with "asJson" prop set to "true"', () => {
......
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