Commit 90b5a6c8 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'persist-collapsed-swimlanes' into 'master'

Persist collapsed state to DB

See merge request gitlab-org/gitlab!43681
parents 47a3b9ab 969eb2dc
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlPopover, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { __, n__, sprintf } from '~/locale'; import { __, n__, sprintf } from '~/locale';
import createFlash from '~/flash';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { statusType } from '../../epic/constants'; import { statusType } from '../../epic/constants';
...@@ -40,8 +41,12 @@ export default { ...@@ -40,8 +41,12 @@ export default {
}, },
}, },
data() { data() {
const { userPreferences } = this.epic;
const { collapsed = false } = userPreferences || {};
return { return {
isExpanded: true, isCollapsed: collapsed,
}; };
}, },
computed: { computed: {
...@@ -51,10 +56,10 @@ export default { ...@@ -51,10 +56,10 @@ export default {
return this.epic.state === statusType.open; return this.epic.state === statusType.open;
}, },
chevronTooltip() { chevronTooltip() {
return this.isExpanded ? __('Collapse') : __('Expand'); return this.isCollapsed ? __('Expand') : __('Collapse');
}, },
chevronIcon() { chevronIcon() {
return this.isExpanded ? 'chevron-down' : 'chevron-right'; return this.isCollapsed ? 'chevron-right' : 'chevron-down';
}, },
issuesCount() { issuesCount() {
return this.lists.reduce( return this.lists.reduce(
...@@ -95,9 +100,16 @@ export default { ...@@ -95,9 +100,16 @@ export default {
this.fetchIssuesForEpic(this.epic.id); this.fetchIssuesForEpic(this.epic.id);
}, },
methods: { methods: {
...mapActions(['fetchIssuesForEpic']), ...mapActions(['fetchIssuesForEpic', 'updateBoardEpicUserPreferences']),
toggleExpanded() { toggleCollapsed() {
this.isExpanded = !this.isExpanded; this.isCollapsed = !this.isCollapsed;
this.updateBoardEpicUserPreferences({
collapsed: this.isCollapsed,
epicId: this.epic.id,
}).catch(() => {
createFlash({ message: __('Unable to save your preference'), captureError: true });
});
}, },
}, },
}; };
...@@ -115,7 +127,7 @@ export default { ...@@ -115,7 +127,7 @@ export default {
class="gl-mr-2 gl-cursor-pointer" class="gl-mr-2 gl-cursor-pointer"
variant="link" variant="link"
data-testid="epic-lane-chevron" data-testid="epic-lane-chevron"
@click="toggleExpanded" @click="toggleCollapsed"
/> />
<h4 <h4
ref="epicTitle" ref="epicTitle"
...@@ -146,7 +158,7 @@ export default { ...@@ -146,7 +158,7 @@ export default {
<gl-loading-icon v-if="isLoading" class="gl-p-2" /> <gl-loading-icon v-if="isLoading" class="gl-p-2" />
</div> </div>
</div> </div>
<div v-if="isExpanded" class="gl-display-flex" data-testid="board-epic-lane-issues"> <div v-if="!isCollapsed" class="gl-display-flex" data-testid="board-epic-lane-issues">
<issues-lane-list <issues-lane-list
v-for="list in lists" v-for="list in lists"
:key="`${list.id}-issues`" :key="`${list.id}-issues`"
......
...@@ -21,6 +21,9 @@ query BoardEE( ...@@ -21,6 +21,9 @@ query BoardEE(
edges { edges {
node { node {
...BoardEpicNode ...BoardEpicNode
userPreferences {
collapsed
}
} }
} }
pageInfo { pageInfo {
...@@ -41,6 +44,9 @@ query BoardEE( ...@@ -41,6 +44,9 @@ query BoardEE(
edges { edges {
node { node {
...BoardEpicNode ...BoardEpicNode
userPreferences {
collapsed
}
} }
} }
pageInfo { pageInfo {
......
mutation updateBoardEpicUserPreferences(
$boardId: BoardID!
$epicId: EpicID!
$collapsed: Boolean!
) {
updateBoardEpicUserPreferences(
input: { boardId: $boardId, epicId: $epicId, collapsed: $collapsed }
) {
errors
epicUserPreferences {
collapsed
}
}
}
...@@ -26,6 +26,7 @@ import issueSetEpic from '../queries/issue_set_epic.mutation.graphql'; ...@@ -26,6 +26,7 @@ import issueSetEpic from '../queries/issue_set_epic.mutation.graphql';
import issueSetWeight from '../queries/issue_set_weight.mutation.graphql'; import issueSetWeight from '../queries/issue_set_weight.mutation.graphql';
import listsIssuesQuery from '~/boards/queries/lists_issues.query.graphql'; import listsIssuesQuery from '~/boards/queries/lists_issues.query.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql'; import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
import updateBoardEpicUserPreferencesMutation from '../queries/updateBoardEpicUserPreferences.mutation.graphql';
const notImplemented = () => { const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */ /* eslint-disable-next-line @gitlab/require-i18n-strings */
...@@ -140,6 +141,40 @@ export default { ...@@ -140,6 +141,40 @@ export default {
.catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE)); .catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE));
}, },
updateBoardEpicUserPreferences({ commit, state }, { epicId, collapsed }) {
const {
endpoints: { boardId },
} = state;
const variables = {
boardId: fullBoardId(boardId),
epicId,
collapsed,
};
return gqlClient
.mutate({
mutation: updateBoardEpicUserPreferencesMutation,
variables,
})
.then(({ data }) => {
if (data?.updateBoardEpicUserPreferences?.errors.length) {
throw new Error();
}
const { epicUserPreferences: userPreferences } = data?.updateBoardEpicUserPreferences;
commit(types.SET_BOARD_EPIC_USER_PREFERENCES, { epicId, userPreferences });
})
.catch(() => {
commit(types.SET_BOARD_EPIC_USER_PREFERENCES, {
epicId,
userPreferences: {
collapsed: !collapsed,
},
});
});
},
setShowLabels({ commit }, val) { setShowLabels({ commit }, val) {
commit(types.SET_SHOW_LABELS, val); commit(types.SET_SHOW_LABELS, val);
}, },
......
...@@ -28,3 +28,4 @@ export const SET_FILTERS = 'SET_FILTERS'; ...@@ -28,3 +28,4 @@ export const SET_FILTERS = 'SET_FILTERS';
export const MOVE_ISSUE = 'MOVE_ISSUE'; export const MOVE_ISSUE = 'MOVE_ISSUE';
export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS'; export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS';
export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE'; export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE';
export const SET_BOARD_EPIC_USER_PREFERENCES = 'SET_BOARD_EPIC_USER_PREFERENCES';
...@@ -155,4 +155,14 @@ export default { ...@@ -155,4 +155,14 @@ export default {
removeIssueFromList({ state, listId: fromListId, issueId: issue.id }); removeIssueFromList({ state, listId: fromListId, issueId: issue.id });
addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId }); addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId });
}, },
[mutationTypes.SET_BOARD_EPIC_USER_PREFERENCES]: (state, val) => {
const { userPreferences, epicId } = val;
const epic = state.epics.filter(currentEpic => currentEpic.id === epicId)[0];
if (epic) {
Vue.set(epic, 'userPreferences', userPreferences);
}
},
}; };
---
title: Persist collapsed state to DB
merge_request: 43681
author:
type: added
...@@ -14,10 +14,13 @@ describe('EpicLane', () => { ...@@ -14,10 +14,13 @@ describe('EpicLane', () => {
const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`); const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
const updateBoardEpicUserPreferencesSpy = jest.fn();
const createStore = isLoading => { const createStore = isLoading => {
return new Vuex.Store({ return new Vuex.Store({
actions: { actions: {
fetchIssuesForEpic: jest.fn(), fetchIssuesForEpic: jest.fn(),
updateBoardEpicUserPreferences: updateBoardEpicUserPreferencesSpy,
}, },
state: { state: {
issuesByListId: mockIssuesByListId, issuesByListId: mockIssuesByListId,
...@@ -76,13 +79,13 @@ describe('EpicLane', () => { ...@@ -76,13 +79,13 @@ describe('EpicLane', () => {
it('hides issues when collapsing', () => { it('hides issues when collapsing', () => {
expect(wrapper.findAll(IssuesLaneList)).toHaveLength(wrapper.props('lists').length); expect(wrapper.findAll(IssuesLaneList)).toHaveLength(wrapper.props('lists').length);
expect(wrapper.vm.isExpanded).toBe(true); expect(wrapper.vm.isCollapsed).toBe(false);
findByTestId('epic-lane-chevron').vm.$emit('click'); findByTestId('epic-lane-chevron').vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.findAll(IssuesLaneList)).toHaveLength(0); expect(wrapper.findAll(IssuesLaneList)).toHaveLength(0);
expect(wrapper.vm.isExpanded).toBe(false); expect(wrapper.vm.isCollapsed).toBe(true);
}); });
}); });
...@@ -95,5 +98,26 @@ describe('EpicLane', () => { ...@@ -95,5 +98,26 @@ describe('EpicLane', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(findByTestId('epic-lane-issue-count').exists()).toBe(false); expect(findByTestId('epic-lane-issue-count').exists()).toBe(false);
}); });
it('invokes `updateBoardEpicUserPreferences` method on collapse', () => {
const collapsedValue = false;
expect(wrapper.vm.isCollapsed).toBe(collapsedValue);
findByTestId('epic-lane-chevron').vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(updateBoardEpicUserPreferencesSpy).toHaveBeenCalled();
const payload = updateBoardEpicUserPreferencesSpy.mock.calls[0][1];
expect(payload).toEqual({
collapsed: !collapsedValue,
epicId: mockEpic.id,
});
expect(wrapper.vm.isCollapsed).toBe(true);
});
});
}); });
}); });
...@@ -165,6 +165,47 @@ describe('fetchEpicsSwimlanes', () => { ...@@ -165,6 +165,47 @@ describe('fetchEpicsSwimlanes', () => {
}); });
}); });
describe('updateBoardEpicUserPreferences', () => {
const state = {
endpoints: {
boardId: 1,
},
};
const queryResponse = (collapsed = false) => ({
data: {
updateBoardEpicUserPreferences: {
errors: [],
epicUserPreferences: { collapsed },
},
},
});
it('should send mutation', done => {
const collapsed = true;
jest.spyOn(gqlClient, 'mutate').mockResolvedValue(queryResponse(collapsed));
testAction(
actions.updateBoardEpicUserPreferences,
{ epicId: mockEpic.id, collapsed },
state,
[
{
payload: {
epicId: mockEpic.id,
userPreferences: {
collapsed,
},
},
type: types.SET_BOARD_EPIC_USER_PREFERENCES,
},
],
[],
done,
);
});
});
describe('setShowLabels', () => { describe('setShowLabels', () => {
it('should commit mutation SET_SHOW_LABELS', done => { it('should commit mutation SET_SHOW_LABELS', done => {
const state = { const state = {
......
...@@ -319,3 +319,19 @@ describe('MOVE_ISSUE', () => { ...@@ -319,3 +319,19 @@ describe('MOVE_ISSUE', () => {
expect(state.issues['437'].epic).toEqual(null); expect(state.issues['437'].epic).toEqual(null);
}); });
}); });
describe('SET_BOARD_EPIC_USER_PREFERENCES', () => {
it('should replace userPreferences on the given epic', () => {
state = {
...state,
epics: mockEpics,
};
const epic = mockEpics[0];
const userPreferences = { collapsed: false };
mutations.SET_BOARD_EPIC_USER_PREFERENCES(state, { epicId: epic.id, userPreferences });
expect(state.epics[0].userPreferences).toEqual(userPreferences);
});
});
...@@ -27967,6 +27967,9 @@ msgstr "" ...@@ -27967,6 +27967,9 @@ msgstr ""
msgid "Unable to save your changes. Please try again." msgid "Unable to save your changes. Please try again."
msgstr "" msgstr ""
msgid "Unable to save your preference"
msgstr ""
msgid "Unable to schedule a pipeline to run immediately" msgid "Unable to schedule a pipeline to run immediately"
msgstr "" msgstr ""
......
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