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 {
}
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) {
boardsStore.updateFiltersUrl();
......
......@@ -67,7 +67,10 @@ export default {
return this.canAdminList ? options : {};
},
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: {
......@@ -98,6 +101,7 @@ export default {
<template>
<div
class="board-swimlanes gl-white-space-nowrap gl-pb-5 gl-px-3"
data-testid="board-swimlanes"
data_qa_selector="board_epics_swimlanes"
>
<component
......
......@@ -6,6 +6,10 @@ export const EpicFilterType = {
none: 'None',
};
export const GroupByParamType = {
epic: 'epic',
};
export default {
DRAGGABLE_TAG,
EpicFilterType,
......
......@@ -3,10 +3,11 @@ import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store';
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 { BoardType, ListType } from '~/boards/constants';
import { EpicFilterType } from '../constants';
import { EpicFilterType, GroupByParamType } from '../constants';
import boardsStoreEE from './boards_store_ee';
import * as types from './mutation_types';
import * as typesCE from '~/boards/stores/mutation_types';
......@@ -70,7 +71,7 @@ const fetchAndFormatListIssues = (state, extraVariables) => {
export default {
...actionsCE,
setFilters: ({ commit }, filters) => {
setFilters: ({ commit, dispatch }, filters) => {
const filterParams = pick(filters, [
'assigneeUsername',
'authorUsername',
......@@ -82,6 +83,10 @@ export default {
'weight',
]);
if (filters.groupBy === GroupByParamType.epic) {
dispatch('setEpicSwimlanes');
}
if (filterParams.epicId === EpicFilterType.any || filterParams.epicId === EpicFilterType.none) {
filterParams.epicWildcardId = filterParams.epicId.toUpperCase();
filterParams.epicId = undefined;
......@@ -232,13 +237,16 @@ export default {
fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false, noEpicIssues = false }) => {
commit(types.REQUEST_ISSUES_FOR_LIST, { listId, fetchNext });
const { filterParams } = state;
const { epicId, ...filterParams } = state.filterParams;
if (noEpicIssues && epicId !== undefined) {
return null;
}
const variables = {
id: listId,
filters: noEpicIssues
? { ...filterParams, epicWildcardId: EpicFilterType.none.toUpperCase() }
: filterParams,
: { ...filterParams, epicId },
after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined,
first: 20,
};
......@@ -275,13 +283,23 @@ export default {
commit(types.TOGGLE_EPICS_SWIMLANES);
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) {
historyPushState(removeParams(['group_by']));
boardsStore.create();
eventHub.$emit('initialBoardLoad');
} else {
historyPushState(removeParams(['group_by']));
}
},
setEpicSwimlanes: ({ commit, dispatch }) => {
commit(types.SET_EPICS_SWIMLANES);
dispatch('fetchEpicsSwimlanes', {});
},
resetEpics: ({ commit }) => {
commit(types.RESET_EPICS);
},
......
......@@ -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_FAILURE = 'RECEIVE_ISSUES_FOR_EPIC_FAILURE';
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_SWIMLANES_FAILURE = 'RECEIVE_SWIMLANES_FAILURE';
export const RECEIVE_FIRST_EPICS_SUCCESS = 'RECEIVE_FIRST_EPICS_SUCCESS';
......
......@@ -110,6 +110,11 @@ export default {
state.epicsSwimlanesFetchInProgress = true;
},
[mutationTypes.SET_EPICS_SWIMLANES]: state => {
state.isShowingEpicsSwimlanes = true;
state.epicsSwimlanesFetchInProgress = true;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, boardLists) => {
state.boardLists = boardLists;
state.epicsSwimlanesFetchInProgress = false;
......
......@@ -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_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
stub_licensed_features(epics: true, swimlanes: true)
sign_in(user)
......@@ -30,7 +51,7 @@ RSpec.describe 'epics swimlanes', :js do
context 'switch to swimlanes view' 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')
expect(epic_lanes.length).to eq(2)
......@@ -111,4 +132,9 @@ RSpec.describe 'epics swimlanes', :js do
page.find('.dropdown-item', text: 'Epic').click
end
end
def visit_epics_swimlanes_page
visit "#{project_boards_path(project)}?group_by=epic"
wait_for_requests
end
end
......@@ -3,8 +3,11 @@ import MockAdapter from 'axios-mock-adapter';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import actions, { gqlClient } from 'ee/boards/stores/actions';
import * as types from 'ee/boards/stores/mutation_types';
import { GroupByParamType } from 'ee/boards/constants';
import testAction from 'helpers/vuex_action_helper';
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 { formatListIssues } from '~/boards/boards_util';
import {
......@@ -28,6 +31,7 @@ let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
window.gon = { features: {} };
jest.spyOn(commonUtils, 'historyPushState');
});
afterEach(() => {
......@@ -35,7 +39,7 @@ afterEach(() => {
});
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 = {
filters: {},
};
......@@ -43,17 +47,16 @@ describe('setFilters', () => {
const filters = { labelName: 'label', epicId: 1 };
const updatedFilters = { labelName: 'label', epicId: 'gid://gitlab/Epic/1' };
testAction(
return testAction(
actions.setFilters,
filters,
state,
[{ 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 = {
filters: {},
};
......@@ -61,13 +64,29 @@ describe('setFilters', () => {
const filters = { labelName: 'label', epicId: 'None' };
const updatedFilters = { labelName: 'label', epicWildcardId: 'NONE' };
testAction(
return testAction(
actions.setFilters,
filters,
state,
[{ 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', () => {
state,
[{ 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', () => {
});
});
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', () => {
it('sets epicsSwimlanesFetchInProgress to false and populates boardLists with payload', () => {
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