Commit 754c99fb authored by Florie Guibert's avatar Florie Guibert

Swimlanes - Add group_by URL param

Add group_by URL param for Swimlanes
parent 1fb57db4
...@@ -25,7 +25,8 @@ export default class FilteredSearchBoards extends FilteredSearchManager { ...@@ -25,7 +25,8 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
} }
updateObject(path) { updateObject(path) {
this.store.path = path.substr(1); const groupByParam = new URLSearchParams(window.location.search).get('group_by');
this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`;
if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) { if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) {
boardsStore.updateFiltersUrl(); boardsStore.updateFiltersUrl();
......
...@@ -67,7 +67,10 @@ export default { ...@@ -67,7 +67,10 @@ export default {
return this.canAdminList ? options : {}; return this.canAdminList ? options : {};
}, },
hasMoreUnassignedIssues() { hasMoreUnassignedIssues() {
return this.lists.some(list => this.pageInfoByListId[list.id]?.hasNextPage); return (
this.unassignedIssuesCount > 0 &&
this.lists.some(list => this.pageInfoByListId[list.id]?.hasNextPage)
);
}, },
}, },
methods: { methods: {
...@@ -98,6 +101,7 @@ export default { ...@@ -98,6 +101,7 @@ export default {
<template> <template>
<div <div
class="board-swimlanes gl-white-space-nowrap gl-pb-5 gl-px-3" class="board-swimlanes gl-white-space-nowrap gl-pb-5 gl-px-3"
data-testid="board-swimlanes"
data_qa_selector="board_epics_swimlanes" data_qa_selector="board_epics_swimlanes"
> >
<component <component
......
...@@ -6,6 +6,10 @@ export const EpicFilterType = { ...@@ -6,6 +6,10 @@ export const EpicFilterType = {
none: 'None', none: 'None',
}; };
export const GroupByParamType = {
epic: 'epic',
};
export default { export default {
DRAGGABLE_TAG, DRAGGABLE_TAG,
EpicFilterType, EpicFilterType,
......
...@@ -3,10 +3,11 @@ import Cookies from 'js-cookie'; ...@@ -3,10 +3,11 @@ import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils'; import { historyPushState, parseBoolean } from '~/lib/utils/common_utils';
import { setUrlParams, removeParams } from '~/lib/utils/url_utility';
import actionsCE from '~/boards/stores/actions'; import actionsCE from '~/boards/stores/actions';
import { BoardType, ListType } from '~/boards/constants'; import { BoardType, ListType } from '~/boards/constants';
import { EpicFilterType } from '../constants'; import { EpicFilterType, GroupByParamType } from '../constants';
import boardsStoreEE from './boards_store_ee'; import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as typesCE from '~/boards/stores/mutation_types'; import * as typesCE from '~/boards/stores/mutation_types';
...@@ -70,7 +71,7 @@ const fetchAndFormatListIssues = (state, extraVariables) => { ...@@ -70,7 +71,7 @@ const fetchAndFormatListIssues = (state, extraVariables) => {
export default { export default {
...actionsCE, ...actionsCE,
setFilters: ({ commit }, filters) => { setFilters: ({ commit, dispatch }, filters) => {
const filterParams = pick(filters, [ const filterParams = pick(filters, [
'assigneeUsername', 'assigneeUsername',
'authorUsername', 'authorUsername',
...@@ -82,6 +83,10 @@ export default { ...@@ -82,6 +83,10 @@ export default {
'weight', 'weight',
]); ]);
if (filters.groupBy === GroupByParamType.epic) {
dispatch('setEpicSwimlanes');
}
if (filterParams.epicId === EpicFilterType.any || filterParams.epicId === EpicFilterType.none) { if (filterParams.epicId === EpicFilterType.any || filterParams.epicId === EpicFilterType.none) {
filterParams.epicWildcardId = filterParams.epicId.toUpperCase(); filterParams.epicWildcardId = filterParams.epicId.toUpperCase();
filterParams.epicId = undefined; filterParams.epicId = undefined;
...@@ -232,13 +237,16 @@ export default { ...@@ -232,13 +237,16 @@ export default {
fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false, noEpicIssues = false }) => { fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false, noEpicIssues = false }) => {
commit(types.REQUEST_ISSUES_FOR_LIST, { listId, fetchNext }); commit(types.REQUEST_ISSUES_FOR_LIST, { listId, fetchNext });
const { filterParams } = state; const { epicId, ...filterParams } = state.filterParams;
if (noEpicIssues && epicId !== undefined) {
return null;
}
const variables = { const variables = {
id: listId, id: listId,
filters: noEpicIssues filters: noEpicIssues
? { ...filterParams, epicWildcardId: EpicFilterType.none.toUpperCase() } ? { ...filterParams, epicWildcardId: EpicFilterType.none.toUpperCase() }
: filterParams, : { ...filterParams, epicId },
after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined, after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined,
first: 20, first: 20,
}; };
...@@ -275,13 +283,23 @@ export default { ...@@ -275,13 +283,23 @@ export default {
commit(types.TOGGLE_EPICS_SWIMLANES); commit(types.TOGGLE_EPICS_SWIMLANES);
if (state.isShowingEpicsSwimlanes) { if (state.isShowingEpicsSwimlanes) {
dispatch('fetchEpicsSwimlanes', {}).catch(() => commit(types.RECEIVE_SWIMLANES_FAILURE)); historyPushState(setUrlParams({ group_by: GroupByParamType.epic }, window.location.href));
dispatch('fetchEpicsSwimlanes', {});
} else if (!gon.features.graphqlBoardLists) { } else if (!gon.features.graphqlBoardLists) {
historyPushState(removeParams(['group_by']));
boardsStore.create(); boardsStore.create();
eventHub.$emit('initialBoardLoad'); eventHub.$emit('initialBoardLoad');
} else {
historyPushState(removeParams(['group_by']));
} }
}, },
setEpicSwimlanes: ({ commit, dispatch }) => {
commit(types.SET_EPICS_SWIMLANES);
dispatch('fetchEpicsSwimlanes', {});
},
resetEpics: ({ commit }) => { resetEpics: ({ commit }) => {
commit(types.RESET_EPICS); commit(types.RESET_EPICS);
}, },
......
...@@ -18,6 +18,7 @@ export const REQUEST_ISSUES_FOR_EPIC = 'REQUEST_ISSUES_FOR_EPIC'; ...@@ -18,6 +18,7 @@ export const REQUEST_ISSUES_FOR_EPIC = 'REQUEST_ISSUES_FOR_EPIC';
export const RECEIVE_ISSUES_FOR_EPIC_SUCCESS = 'RECEIVE_ISSUES_FOR_EPIC_SUCCESS'; export const RECEIVE_ISSUES_FOR_EPIC_SUCCESS = 'RECEIVE_ISSUES_FOR_EPIC_SUCCESS';
export const RECEIVE_ISSUES_FOR_EPIC_FAILURE = 'RECEIVE_ISSUES_FOR_EPIC_FAILURE'; export const RECEIVE_ISSUES_FOR_EPIC_FAILURE = 'RECEIVE_ISSUES_FOR_EPIC_FAILURE';
export const TOGGLE_EPICS_SWIMLANES = 'TOGGLE_EPICS_SWIMLANES'; export const TOGGLE_EPICS_SWIMLANES = 'TOGGLE_EPICS_SWIMLANES';
export const SET_EPICS_SWIMLANES = 'SET_EPICS_SWIMLANES';
export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
export const RECEIVE_SWIMLANES_FAILURE = 'RECEIVE_SWIMLANES_FAILURE'; export const RECEIVE_SWIMLANES_FAILURE = 'RECEIVE_SWIMLANES_FAILURE';
export const RECEIVE_FIRST_EPICS_SUCCESS = 'RECEIVE_FIRST_EPICS_SUCCESS'; export const RECEIVE_FIRST_EPICS_SUCCESS = 'RECEIVE_FIRST_EPICS_SUCCESS';
......
...@@ -110,6 +110,11 @@ export default { ...@@ -110,6 +110,11 @@ export default {
state.epicsSwimlanesFetchInProgress = true; state.epicsSwimlanesFetchInProgress = true;
}, },
[mutationTypes.SET_EPICS_SWIMLANES]: state => {
state.isShowingEpicsSwimlanes = true;
state.epicsSwimlanesFetchInProgress = true;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, boardLists) => { [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, boardLists) => {
state.boardLists = boardLists; state.boardLists = boardLists;
state.epicsSwimlanesFetchInProgress = false; state.epicsSwimlanesFetchInProgress = false;
......
...@@ -21,6 +21,27 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -21,6 +21,27 @@ RSpec.describe 'epics swimlanes', :js do
let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1) } let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1) }
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) } let_it_be(:epic_issue2) { create(:epic_issue, epic: epic2, issue: issue2) }
context 'link to swimlanes view' do
before do
stub_licensed_features(epics: true)
sign_in(user)
visit_epics_swimlanes_page
end
it 'displays epics swimlanes when link to boards with group_by epic in URL' do
expect(page).to have_selector('[data-testid="board-swimlanes"]')
epic_lanes = page.all(:css, '.board-epic-lane')
expect(epic_lanes.length).to eq(2)
end
it 'displays issue not assigned to epic in unassigned issues lane' do
page.within('.board-lane-unassigned-issues-title') do
expect(page.find('span[data-testid="issues-lane-issue-count"]')).to have_content('1')
end
end
end
before do before do
stub_licensed_features(epics: true, swimlanes: true) stub_licensed_features(epics: true, swimlanes: true)
sign_in(user) sign_in(user)
...@@ -30,7 +51,7 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -30,7 +51,7 @@ RSpec.describe 'epics swimlanes', :js do
context 'switch to swimlanes view' do context 'switch to swimlanes view' do
it 'displays epics swimlanes when selecting Epic in Group by dropdown' do it 'displays epics swimlanes when selecting Epic in Group by dropdown' do
expect(page).to have_css('.board-swimlanes') expect(page).to have_selector('[data-testid="board-swimlanes"]')
epic_lanes = page.all(:css, '.board-epic-lane') epic_lanes = page.all(:css, '.board-epic-lane')
expect(epic_lanes.length).to eq(2) expect(epic_lanes.length).to eq(2)
...@@ -111,4 +132,9 @@ RSpec.describe 'epics swimlanes', :js do ...@@ -111,4 +132,9 @@ RSpec.describe 'epics swimlanes', :js do
page.find('.dropdown-item', text: 'Epic').click page.find('.dropdown-item', text: 'Epic').click
end end
end end
def visit_epics_swimlanes_page
visit "#{project_boards_path(project)}?group_by=epic"
wait_for_requests
end
end end
...@@ -3,8 +3,11 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -3,8 +3,11 @@ import MockAdapter from 'axios-mock-adapter';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee'; import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import actions, { gqlClient } from 'ee/boards/stores/actions'; import actions, { gqlClient } from 'ee/boards/stores/actions';
import * as types from 'ee/boards/stores/mutation_types'; import * as types from 'ee/boards/stores/mutation_types';
import { GroupByParamType } from 'ee/boards/constants';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import * as typesCE from '~/boards/stores/mutation_types'; import * as typesCE from '~/boards/stores/mutation_types';
import * as commonUtils from '~/lib/utils/common_utils';
import { setUrlParams, removeParams } from '~/lib/utils/url_utility';
import { ListType } from '~/boards/constants'; import { ListType } from '~/boards/constants';
import { formatListIssues } from '~/boards/boards_util'; import { formatListIssues } from '~/boards/boards_util';
import { import {
...@@ -28,6 +31,7 @@ let mock; ...@@ -28,6 +31,7 @@ let mock;
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
window.gon = { features: {} }; window.gon = { features: {} };
jest.spyOn(commonUtils, 'historyPushState');
}); });
afterEach(() => { afterEach(() => {
...@@ -35,7 +39,7 @@ afterEach(() => { ...@@ -35,7 +39,7 @@ afterEach(() => {
}); });
describe('setFilters', () => { describe('setFilters', () => {
it('should commit mutation SET_FILTERS, updates epicId with global id', done => { it('should commit mutation SET_FILTERS, updates epicId with global id', () => {
const state = { const state = {
filters: {}, filters: {},
}; };
...@@ -43,17 +47,16 @@ describe('setFilters', () => { ...@@ -43,17 +47,16 @@ describe('setFilters', () => {
const filters = { labelName: 'label', epicId: 1 }; const filters = { labelName: 'label', epicId: 1 };
const updatedFilters = { labelName: 'label', epicId: 'gid://gitlab/Epic/1' }; const updatedFilters = { labelName: 'label', epicId: 'gid://gitlab/Epic/1' };
testAction( return testAction(
actions.setFilters, actions.setFilters,
filters, filters,
state, state,
[{ type: types.SET_FILTERS, payload: updatedFilters }], [{ type: types.SET_FILTERS, payload: updatedFilters }],
[], [],
done,
); );
}); });
it('should commit mutation SET_FILTERS, updates epicWildcardId', done => { it('should commit mutation SET_FILTERS, updates epicWildcardId', () => {
const state = { const state = {
filters: {}, filters: {},
}; };
...@@ -61,13 +64,29 @@ describe('setFilters', () => { ...@@ -61,13 +64,29 @@ describe('setFilters', () => {
const filters = { labelName: 'label', epicId: 'None' }; const filters = { labelName: 'label', epicId: 'None' };
const updatedFilters = { labelName: 'label', epicWildcardId: 'NONE' }; const updatedFilters = { labelName: 'label', epicWildcardId: 'NONE' };
testAction( return testAction(
actions.setFilters, actions.setFilters,
filters, filters,
state, state,
[{ type: types.SET_FILTERS, payload: updatedFilters }], [{ type: types.SET_FILTERS, payload: updatedFilters }],
[], [],
done, );
});
it('should commit mutation SET_FILTERS, dispatches setEpicSwimlanes action if filters contain groupBy epic', () => {
const state = {
filters: {},
};
const filters = { labelName: 'label', epicId: 1, groupBy: 'epic' };
const updatedFilters = { labelName: 'label', epicId: 'gid://gitlab/Epic/1' };
return testAction(
actions.setFilters,
filters,
state,
[{ type: types.SET_FILTERS, payload: updatedFilters }],
[{ type: 'setEpicSwimlanes' }],
); );
}); });
}); });
...@@ -391,6 +410,48 @@ describe('toggleEpicSwimlanes', () => { ...@@ -391,6 +410,48 @@ describe('toggleEpicSwimlanes', () => {
state, state,
[{ type: types.TOGGLE_EPICS_SWIMLANES }], [{ type: types.TOGGLE_EPICS_SWIMLANES }],
[], [],
() => {
expect(commonUtils.historyPushState).toHaveBeenCalledWith(removeParams(['group_by']));
},
);
});
it('should dispatch fetchEpicsSwimlanes action when isShowingEpicsSwimlanes is true', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue({});
const state = {
isShowingEpicsSwimlanes: true,
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
};
return testAction(
actions.toggleEpicSwimlanes,
null,
state,
[{ type: types.TOGGLE_EPICS_SWIMLANES }],
[{ type: 'fetchEpicsSwimlanes', payload: {} }],
() => {
expect(commonUtils.historyPushState).toHaveBeenCalledWith(
setUrlParams({ group_by: GroupByParamType.epic }, window.location.href),
);
},
);
});
});
describe('setEpicSwimlanes', () => {
it('should commit mutation SET_EPICS_SWIMLANES and dispatch fetchEpicsSwimlanes action', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue({});
return testAction(
actions.setEpicSwimlanes,
null,
{},
[{ type: types.SET_EPICS_SWIMLANES }],
[{ type: 'fetchEpicsSwimlanes', payload: {} }],
); );
}); });
}); });
......
...@@ -178,6 +178,21 @@ describe('TOGGLE_EPICS_SWIMLANES', () => { ...@@ -178,6 +178,21 @@ describe('TOGGLE_EPICS_SWIMLANES', () => {
}); });
}); });
describe('SET_EPICS_SWIMLANES', () => {
it('set isShowingEpicsSwimlanes and epicsSwimlanesFetchInProgress to true', () => {
state = {
...state,
isShowingEpicsSwimlanes: false,
epicsSwimlanesFetchInProgress: false,
};
mutations.SET_EPICS_SWIMLANES(state);
expect(state.isShowingEpicsSwimlanes).toBe(true);
expect(state.epicsSwimlanesFetchInProgress).toBe(true);
});
});
describe('RECEIVE_BOARD_LISTS_SUCCESS', () => { describe('RECEIVE_BOARD_LISTS_SUCCESS', () => {
it('sets epicsSwimlanesFetchInProgress to false and populates boardLists with payload', () => { it('sets epicsSwimlanesFetchInProgress to false and populates boardLists with payload', () => {
state = { 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