Commit f6b20834 authored by Diego Louzán's avatar Diego Louzán Committed by Phil Hughes

feat: Add 'toggle all discussions' functionality

New button 'Toggle all discussions' next to 'Jump to next unresolved
thread', toggles expanded / not expanded state of all discussions
parent df4320ab
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import discussionNavigation from '../mixins/discussion_navigation'; import discussionNavigation from '../mixins/discussion_navigation';
...@@ -18,13 +18,11 @@ export default { ...@@ -18,13 +18,11 @@ export default {
'getNoteableData', 'getNoteableData',
'resolvableDiscussionsCount', 'resolvableDiscussionsCount',
'unresolvedDiscussionsCount', 'unresolvedDiscussionsCount',
'discussions',
]), ]),
isLoggedIn() { isLoggedIn() {
return this.getUserData.id; return this.getUserData.id;
}, },
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
allResolved() { allResolved() {
return this.unresolvedDiscussionsCount === 0; return this.unresolvedDiscussionsCount === 0;
}, },
...@@ -34,6 +32,21 @@ export default { ...@@ -34,6 +32,21 @@ export default {
resolvedDiscussionsCount() { resolvedDiscussionsCount() {
return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount; return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
}, },
toggeableDiscussions() {
return this.discussions.filter(discussion => !discussion.individual_note);
},
allExpanded() {
return this.toggeableDiscussions.every(discussion => discussion.expanded);
},
},
methods: {
...mapActions(['setExpandDiscussions']),
handleExpandDiscussions() {
this.setExpandDiscussions({
discussionIds: this.toggeableDiscussions.map(discussion => discussion.id),
expanded: !this.allExpanded,
});
},
}, },
}; };
</script> </script>
...@@ -44,8 +57,8 @@ export default { ...@@ -44,8 +57,8 @@ export default {
ref="discussionCounter" ref="discussionCounter"
class="line-resolve-all-container full-width-mobile" class="line-resolve-all-container full-width-mobile"
> >
<div class="full-width-mobile d-flex d-sm-block"> <div class="full-width-mobile d-flex d-sm-flex">
<div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all"> <div class="line-resolve-all">
<span <span
:class="{ 'is-active': allResolved }" :class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled" class="line-resolve-btn is-disabled"
...@@ -75,7 +88,7 @@ export default { ...@@ -75,7 +88,7 @@ export default {
<div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group">
<button <button
v-gl-tooltip v-gl-tooltip
title="Jump to next unresolved thread" :title="__('Jump to next unresolved thread')"
class="btn btn-default discussion-next-btn" class="btn btn-default discussion-next-btn"
data-track-event="click_button" data-track-event="click_button"
data-track-label="mr_next_unresolved_thread" data-track-label="mr_next_unresolved_thread"
...@@ -85,6 +98,16 @@ export default { ...@@ -85,6 +98,16 @@ export default {
<icon name="comment-next" /> <icon name="comment-next" />
</button> </button>
</div> </div>
<div v-if="isLoggedIn" class="btn-group btn-group-sm" role="group">
<button
v-gl-tooltip
:title="__('Toggle all threads')"
class="btn btn-default toggle-all-discussions-btn"
@click="handleExpandDiscussions"
>
<icon :name="allExpanded ? 'angle-up' : 'angle-down'" />
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -46,6 +46,10 @@ export const setNotesFetchedState = ({ commit }, state) => ...@@ -46,6 +46,10 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const setExpandDiscussions = ({ commit }, { discussionIds, expanded }) => {
commit(types.SET_EXPAND_DISCUSSIONS, { discussionIds, expanded });
};
export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => { export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => {
const config = const config =
filter !== undefined filter !== undefined
...@@ -54,6 +58,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi ...@@ -54,6 +58,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
return axios.get(path, config).then(({ data }) => { return axios.get(path, config).then(({ data }) => {
commit(types.SET_INITIAL_DISCUSSIONS, data); commit(types.SET_INITIAL_DISCUSSIONS, data);
dispatch('updateResolvableDiscussionsCounts'); dispatch('updateResolvableDiscussionsCounts');
}); });
}; };
......
...@@ -24,6 +24,7 @@ export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; ...@@ -24,6 +24,7 @@ export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION'; export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
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';
......
...@@ -190,6 +190,15 @@ export default { ...@@ -190,6 +190,15 @@ export default {
}); });
}, },
[types.SET_EXPAND_DISCUSSIONS](state, { discussionIds, expanded }) {
if (discussionIds?.length) {
discussionIds.forEach(discussionId => {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
Object.assign(discussion, { expanded });
});
}
},
[types.UPDATE_NOTE](state, note) { [types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
......
...@@ -842,11 +842,11 @@ $note-form-margin-left: 72px; ...@@ -842,11 +842,11 @@ $note-form-margin-left: 72px;
white-space: nowrap; white-space: nowrap;
} }
.btn-group { .discussion-next-btn {
margin-left: -4px; border-radius: 0;
} }
.discussion-next-btn { .toggle-all-discussions-btn {
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
...@@ -859,7 +859,6 @@ $note-form-margin-left: 72px; ...@@ -859,7 +859,6 @@ $note-form-margin-left: 72px;
} }
&.discussion-create-issue-btn { &.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0; border-radius: 0;
border-right: 0; border-right: 0;
...@@ -873,6 +872,10 @@ $note-form-margin-left: 72px; ...@@ -873,6 +872,10 @@ $note-form-margin-left: 72px;
} }
} }
} }
&.discussion-next-btn {
border-right: 0;
}
} }
} }
...@@ -884,12 +887,9 @@ $note-form-margin-left: 72px; ...@@ -884,12 +887,9 @@ $note-form-margin-left: 72px;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
font-size: $gl-btn-small-font-size; font-size: $gl-btn-small-font-size;
border-top-right-radius: 0;
&.has-next-btn { border-bottom-right-radius: 0;
border-top-right-radius: 0; border-right: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
.line-resolve-btn { .line-resolve-btn {
margin-right: 5px; margin-right: 5px;
......
---
title: Add toggle all discussions button to MRs
merge_request: 24670
author: Martin Hobert & Diego Louzán
type: added
...@@ -21000,6 +21000,9 @@ msgstr "" ...@@ -21000,6 +21000,9 @@ msgstr ""
msgid "Toggle Sidebar" msgid "Toggle Sidebar"
msgstr "" msgstr ""
msgid "Toggle all threads"
msgstr ""
msgid "Toggle backtrace" msgid "Toggle backtrace"
msgstr "" msgstr ""
......
...@@ -75,17 +75,66 @@ describe('DiscussionCounter component', () => { ...@@ -75,17 +75,66 @@ describe('DiscussionCounter component', () => {
}); });
it.each` it.each`
title | resolved | hasNextBtn | isActive | icon | groupLength title | resolved | isActive | icon | groupLength
${'hasNextButton'} | ${false} | ${true} | ${false} | ${'check-circle'} | ${2} ${'not allResolved'} | ${false} | ${false} | ${'check-circle'} | ${3}
${'allResolved'} | ${true} | ${false} | ${true} | ${'check-circle-filled'} | ${0} ${'allResolved'} | ${true} | ${true} | ${'check-circle-filled'} | ${1}
`('renders correctly if $title', ({ resolved, hasNextBtn, isActive, icon, groupLength }) => { `('renders correctly if $title', ({ resolved, isActive, icon, groupLength }) => {
updateStore({ resolvable: true, resolved }); updateStore({ resolvable: true, resolved });
wrapper = shallowMount(DiscussionCounter, { store, localVue }); wrapper = shallowMount(DiscussionCounter, { store, localVue });
expect(wrapper.find(`.has-next-btn`).exists()).toBe(hasNextBtn);
expect(wrapper.find(`.is-active`).exists()).toBe(isActive); expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
expect(wrapper.find({ name: icon }).exists()).toBe(true); expect(wrapper.find({ name: icon }).exists()).toBe(true);
expect(wrapper.findAll('[role="group"').length).toBe(groupLength); expect(wrapper.findAll('[role="group"').length).toBe(groupLength);
}); });
}); });
describe('toggle all threads button', () => {
let toggleAllButton;
const updateStoreWithExpanded = expanded => {
const discussion = { ...discussionMock, expanded };
store.commit(types.SET_INITIAL_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = shallowMount(DiscussionCounter, { store, localVue });
toggleAllButton = wrapper.find('.toggle-all-discussions-btn');
};
afterEach(() => wrapper.destroy());
it('calls button handler when clicked', () => {
updateStoreWithExpanded(true);
wrapper.setMethods({ handleExpandDiscussions: jest.fn() });
toggleAllButton.trigger('click');
expect(wrapper.vm.handleExpandDiscussions).toHaveBeenCalledTimes(1);
});
it('collapses all discussions if expanded', () => {
updateStoreWithExpanded(true);
expect(wrapper.vm.allExpanded).toBe(true);
expect(toggleAllButton.find({ name: 'angle-up' }).exists()).toBe(true);
toggleAllButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.allExpanded).toBe(false);
expect(toggleAllButton.find({ name: 'angle-down' }).exists()).toBe(true);
});
});
it('expands all discussions if collapsed', () => {
updateStoreWithExpanded(false);
expect(wrapper.vm.allExpanded).toBe(false);
expect(toggleAllButton.find({ name: 'angle-down' }).exists()).toBe(true);
toggleAllButton.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.allExpanded).toBe(true);
expect(toggleAllButton.find({ name: 'angle-up' }).exists()).toBe(true);
});
});
});
}); });
...@@ -329,6 +329,52 @@ describe('Notes Store mutations', () => { ...@@ -329,6 +329,52 @@ describe('Notes Store mutations', () => {
}); });
}); });
describe('SET_EXPAND_DISCUSSIONS', () => {
it('should succeed when discussions are null', () => {
const state = {};
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds: null, expanded: true });
expect(state).toEqual({});
});
it('should succeed when discussions are empty', () => {
const state = {};
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds: [], expanded: true });
expect(state).toEqual({});
});
it('should open all closed discussions', () => {
const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false });
const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true });
const discussionIds = [discussion1.id, discussion2.id];
const state = { discussions: [discussion1, discussion2] };
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds, expanded: true });
state.discussions.forEach(discussion => {
expect(discussion.expanded).toEqual(true);
});
});
it('should close all opened discussions', () => {
const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false });
const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true });
const discussionIds = [discussion1.id, discussion2.id];
const state = { discussions: [discussion1, discussion2] };
mutations.SET_EXPAND_DISCUSSIONS(state, { discussionIds, expanded: false });
state.discussions.forEach(discussion => {
expect(discussion.expanded).toEqual(false);
});
});
});
describe('UPDATE_NOTE', () => { describe('UPDATE_NOTE', () => {
it('should update a note', () => { it('should update a note', () => {
const state = { const state = {
......
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