Commit 8520d876 authored by Florie Guibert's avatar Florie Guibert Committed by Simon Knox

Refactor GraphQL boards filters and board scope

Decouple filtering and board scope from boardsStore for GraphQL boards
parent 95069124
......@@ -130,6 +130,11 @@ export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}
// EE-specific feature. Find the implementation in the `ee/`-folder
export function transformBoardConfig() {
return '';
}
export default {
getMilestone,
formatIssue,
......
......@@ -7,7 +7,6 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import BoardNewIssue from './board_new_issue_new.vue';
import BoardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
export default {
......@@ -44,7 +43,6 @@ export default {
data() {
return {
scrollOffset: 250,
filters: boardsStore.state.filters,
showCount: false,
showIssueForm: false,
};
......@@ -94,12 +92,6 @@ export default {
},
},
watch: {
filters: {
handler() {
this.listRef.scrollTop = 0;
},
deep: true,
},
issues() {
this.$nextTick(() => {
this.showCount = this.scrollHeight() > Math.ceil(this.listHeight());
......
import FilteredSearchBoards from '../../filtered_search_boards';
import FilteredSearchContainer from '../../../filtered_search/container';
import vuexstore from '~/boards/stores';
export default {
name: 'modal-filters',
......@@ -13,7 +12,7 @@ export default {
mounted() {
FilteredSearchContainer.container = this.$el;
this.filteredSearch = new FilteredSearchBoards(this.store, vuexstore);
this.filteredSearch = new FilteredSearchBoards(this.store);
this.filteredSearch.setup();
this.filteredSearch.removeTokens();
this.filteredSearch.handleInputPlaceholder();
......
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
import { transformBoardConfig } from 'ee_else_ce/boards/boards_util';
import FilteredSearchContainer from '../filtered_search/container';
import boardsStore from './stores/boards_store';
import vuexstore from './stores';
import { updateHistory } from '~/lib/utils/url_utility';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, vuexstore, updateUrl = false, cantEdit = []) {
constructor(store, updateUrl = false, cantEdit = []) {
super({
page: 'boards',
isGroupDecendent: true,
......@@ -23,16 +26,26 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
this.vuexstore = vuexstore;
if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) {
const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig);
if (boardConfigPath !== '') {
const filterPath = window.location.search ? `${window.location.search}&` : '?';
updateHistory({
url: `${filterPath}${transformBoardConfig(vuexstore.state.boardConfig)}`,
});
}
}
}
updateObject(path) {
const groupByParam = new URLSearchParams(window.location.search).get('group_by');
this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`;
if (this.vuexstore.getters.shouldUseGraphQL) {
boardsStore.updateFiltersUrl();
boardsStore.performSearch();
if (vuexstore.getters.shouldUseGraphQL) {
updateHistory({
url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
});
vuexstore.dispatch('performSearch');
} else if (this.updateUrl) {
boardsStore.updateFiltersUrl();
}
......
......@@ -40,7 +40,6 @@ import {
NavigationType,
convertObjectPropsToCamelCase,
parseBoolean,
urlParamsToObject,
} from '~/lib/utils/common_utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
......@@ -112,7 +111,7 @@ export default () => {
};
},
computed: {
...mapGetters(['isSwimlanesOn', 'shouldUseGraphQL']),
...mapGetters(['shouldUseGraphQL']),
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
......@@ -130,6 +129,17 @@ export default () => {
...endpoints,
boardType: this.parent,
disabled: this.disabled,
boardConfig: {
milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
iterationId: parseInt($boardApp.dataset.boardIterationId, 10),
iterationTitle: $boardApp.dataset.boardIterationTitle || '',
assigneeUsername: $boardApp.dataset.boardAssigneeUsername,
labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels || []) : [],
weight: $boardApp.dataset.boardWeight
? parseInt($boardApp.dataset.boardWeight, 10)
: null,
},
});
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
......@@ -138,7 +148,6 @@ export default () => {
eventHub.$on('newDetailIssue', this.updateDetailIssue);
eventHub.$on('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
eventHub.$on('performSearch', this.performSearch);
eventHub.$on('initialBoardLoad', this.initialBoardLoad);
},
beforeDestroy() {
......@@ -146,16 +155,10 @@ export default () => {
eventHub.$off('newDetailIssue', this.updateDetailIssue);
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
eventHub.$off('performSearch', this.performSearch);
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
this.filterManager = new FilteredSearchBoards(
boardsStore.filter,
store,
true,
boardsStore.cantEdit,
);
this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
this.filterManager.setup();
this.performSearch();
......@@ -167,14 +170,7 @@ export default () => {
}
},
methods: {
...mapActions([
'setInitialBoardData',
'setFilters',
'fetchEpicsSwimlanes',
'resetIssues',
'resetEpics',
'fetchLists',
]),
...mapActions(['setInitialBoardData', 'performSearch']),
initialBoardLoad() {
boardsStore
.all()
......@@ -190,17 +186,6 @@ export default () => {
updateTokens() {
this.filterManager.updateTokens();
},
performSearch() {
this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)));
if (this.isSwimlanesOn) {
this.resetEpics();
this.resetIssues();
this.fetchEpicsSwimlanes({});
} else if (gon.features.graphqlBoardLists) {
this.fetchLists();
this.resetIssues();
}
},
updateDetailIssue(newIssue, multiSelect = false) {
const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
......
......@@ -3,6 +3,7 @@ import { pick } from 'lodash';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
import { BoardType, ListType, inactiveId } from '~/boards/constants';
import * as types from './mutation_types';
import {
......@@ -64,6 +65,18 @@ export default {
commit(types.SET_FILTERS, filterParams);
},
performSearch({ dispatch }) {
dispatch(
'setFilters',
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
);
if (gon.features.graphqlBoardLists) {
dispatch('fetchLists');
dispatch('resetIssues');
}
},
fetchLists: ({ commit, state, dispatch }) => {
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
......
......@@ -492,10 +492,6 @@ const boardsStore = {
eventHub.$emit('updateTokens');
},
performSearch() {
eventHub.$emit('performSearch');
},
setListDetail(newList) {
this.detail.list = newList;
},
......
......@@ -32,10 +32,11 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, ...endpoints } = data;
const { boardType, disabled, boardConfig, ...endpoints } = data;
state.endpoints = endpoints;
state.boardType = boardType;
state.disabled = disabled;
state.boardConfig = boardConfig;
},
[mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
......
......@@ -14,6 +14,7 @@ export default () => ({
pageInfoByListId: {},
issues: {},
filterParams: {},
boardConfig: {},
error: undefined,
// TODO: remove after ce/ee split of board_content.vue
isShowingEpicsSwimlanes: false,
......
import {
IterationFilterType,
IterationIDs,
MilestoneFilterType,
MilestoneIDs,
WeightFilterType,
WeightIDs,
} from './constants';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
export function getMilestone({ milestone }) {
return milestone || null;
}
......@@ -14,9 +25,66 @@ export function fullUserId(userId) {
return `gid://gitlab/User/${userId}`;
}
export function transformBoardConfig(boardConfig) {
const updatedBoardConfig = {};
const passedFilterParams = urlParamsToObject(window.location.search);
const updateScopeObject = (key, value = '') => {
if (value === null || value === '') return;
// Comparing with value string because weight can be a number
if (!passedFilterParams[key] || passedFilterParams[key] !== value.toString()) {
updatedBoardConfig[key] = value;
}
};
let { milestoneTitle } = boardConfig;
if (boardConfig.milestoneId === MilestoneIDs.NONE) {
milestoneTitle = MilestoneFilterType.none;
}
if (milestoneTitle) {
updateScopeObject('milestone_title', milestoneTitle);
}
let { iterationTitle } = boardConfig;
if (boardConfig.iterationId === IterationIDs.NONE) {
iterationTitle = IterationFilterType.none;
}
if (iterationTitle) {
updateScopeObject('iteration_id', iterationTitle);
}
let { weight } = boardConfig;
if (weight !== WeightIDs.ANY) {
if (weight === WeightIDs.NONE) {
weight = WeightFilterType.none;
}
updateScopeObject('weight', weight);
}
updateScopeObject('assignee_username', boardConfig.assigneeUsername);
let updatedFilterPath = objectToQuery(updatedBoardConfig);
const filterPath = updatedFilterPath ? updatedFilterPath.split('&') : [];
boardConfig.labels.forEach(label => {
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = passedFilterParams.label_name?.indexOf(labelTitle);
if (labelIndex === -1 || labelIndex === undefined) {
filterPath.push(param);
}
});
updatedFilterPath = filterPath.join('&');
return updatedFilterPath;
}
export default {
getMilestone,
fullEpicId,
fullMilestoneId,
fullUserId,
transformBoardConfig,
};
......@@ -12,6 +12,28 @@ export const IterationFilterType = {
current: 'Current',
};
export const IterationIDs = {
NONE: 0,
};
export const MilestoneFilterType = {
any: 'Any',
none: 'None',
};
export const MilestoneIDs = {
NONE: 0,
};
export const WeightFilterType = {
none: 'None',
};
export const WeightIDs = {
NONE: -2,
ANY: -1,
};
export const GroupByParamType = {
epic: 'epic',
};
......
import { pick } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store';
import { historyPushState } from '~/lib/utils/common_utils';
import {
historyPushState,
convertObjectPropsToCamelCase,
urlParamsToObject,
} from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import actionsCE from '~/boards/stores/actions';
import { BoardType } from '~/boards/constants';
......@@ -105,6 +109,22 @@ export default {
commit(types.SET_FILTERS, filterParams);
},
performSearch({ dispatch, getters }) {
dispatch(
'setFilters',
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
);
if (getters.isSwimlanesOn) {
dispatch('resetEpics');
dispatch('resetIssues');
dispatch('fetchEpicsSwimlanes', {});
} else if (gon.features.graphqlBoardLists) {
dispatch('fetchLists');
dispatch('resetIssues');
}
},
fetchEpicsSwimlanes({ state, commit, dispatch }, { withLists = true, endCursor = null }) {
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
......@@ -291,14 +311,18 @@ export default {
commit(types.TOGGLE_EPICS_SWIMLANES);
if (state.isShowingEpicsSwimlanes) {
historyPushState(mergeUrlParams({ group_by: GroupByParamType.epic }, window.location.href));
historyPushState(
mergeUrlParams({ group_by: GroupByParamType.epic }, window.location.href, {
spreadArrays: true,
}),
);
dispatch('fetchEpicsSwimlanes', {});
} else if (!gon.features.graphqlBoardLists) {
historyPushState(removeParams(['group_by']));
historyPushState(removeParams(['group_by']), window.location.href, true);
boardsStore.create();
eventHub.$emit('initialBoardLoad');
} else {
historyPushState(removeParams(['group_by']));
historyPushState(removeParams(['group_by']), window.location.href, true);
}
},
......
......@@ -57,7 +57,9 @@ class BoardsStoreEE {
this.store.scopedLabels = {
enabled: parseBoolean(scopedLabels),
};
this.initBoardFilters();
if (!gon.features.graphqlBoardLists) {
this.initBoardFilters();
}
}
};
......
import { transformBoardConfig } from 'ee/boards/boards_util';
describe('transformBoardConfig', () => {
beforeEach(() => {
delete window.location;
});
const boardConfig = {
milestoneTitle: 'milestone',
assigneeUsername: 'username',
labels: [
{ id: 5, title: 'Deliverable', color: '#34ebec', type: 'GroupLabel', textColor: '#333333' },
],
weight: 0,
};
it('formats url parameters from boardConfig object', () => {
window.location = { search: '' };
const result = transformBoardConfig(boardConfig);
expect(result).toContain(
'milestone_title=milestone&weight=0&assignee_username=username&label_name[]=Deliverable',
);
});
it('formats url parameters from boardConfig object preventing duplicates with passed filter query', () => {
window.location = { search: 'label_name[]=Deliverable' };
const result = transformBoardConfig(boardConfig);
expect(result).toContain('milestone_title=milestone&weight=0&assignee_username=username');
});
});
......@@ -100,6 +100,41 @@ describe('setFilters', () => {
});
});
describe('performSearch', () => {
it('should dispatch setFilters action', done => {
testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
});
it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', async () => {
window.gon = { features: { graphqlBoardLists: true } };
const getters = { isSwimlanesOn: false };
await testAction({
action: actions.performSearch,
state: { ...getters },
expectedActions: [
{ type: 'setFilters', payload: {} },
{ type: 'fetchLists' },
{ type: 'resetIssues' },
],
});
});
it('should dispatch setFilters, resetEpics, fetchEpicsSwimlanes and resetIssues action when isSwimlanesOn', async () => {
const getters = { isSwimlanesOn: true };
await testAction({
action: actions.performSearch,
state: { isShowingEpicsSwimlanes: true, ...getters },
expectedActions: [
{ type: 'setFilters', payload: {} },
{ type: 'resetEpics' },
{ type: 'resetIssues' },
{ type: 'fetchEpicsSwimlanes', payload: {} },
],
});
});
});
describe('fetchEpicsSwimlanes', () => {
const state = {
endpoints: {
......@@ -421,8 +456,9 @@ describe('fetchIssuesForEpic', () => {
describe('toggleEpicSwimlanes', () => {
it('should commit mutation TOGGLE_EPICS_SWIMLANES', () => {
const startURl = `${TEST_HOST}/groups/gitlab-org/-/boards/1?group_by=epic`;
global.jsdom.reconfigure({
url: `${TEST_HOST}/groups/gitlab-org/-/boards/1?group_by=epic`,
url: startURl,
});
const state = {
......@@ -440,7 +476,11 @@ describe('toggleEpicSwimlanes', () => {
[{ type: types.TOGGLE_EPICS_SWIMLANES }],
[],
() => {
expect(commonUtils.historyPushState).toHaveBeenCalledWith(removeParams(['group_by']));
expect(commonUtils.historyPushState).toHaveBeenCalledWith(
removeParams(['group_by']),
startURl,
true,
);
expect(global.window.location.href).toBe(`${TEST_HOST}/groups/gitlab-org/-/boards/1`);
},
);
......
......@@ -31,6 +31,10 @@ const expectNotImplemented = action => {
// subgroups when the movIssue action is called.
const getProjectPath = path => path.split('#')[0];
beforeEach(() => {
window.gon = { features: {} };
});
describe('setInitialBoardData', () => {
it('sets data object', () => {
const mockData = {
......@@ -67,6 +71,24 @@ describe('setFilters', () => {
});
});
describe('performSearch', () => {
it('should dispatch setFilters action', done => {
testAction(actions.performSearch, {}, {}, [], [{ type: 'setFilters', payload: {} }], done);
});
it('should dispatch setFilters, fetchLists and resetIssues action when graphqlBoardLists FF is on', done => {
window.gon = { features: { graphqlBoardLists: true } };
testAction(
actions.performSearch,
{},
{},
[],
[{ type: 'setFilters', payload: {} }, { type: 'fetchLists' }, { type: 'resetIssues' }],
done,
);
});
});
describe('setActiveId', () => {
it('should commit mutation SET_ACTIVE_ID', done => {
const state = {
......
......@@ -33,16 +33,21 @@ describe('Board Store Mutations', () => {
};
const boardType = 'group';
const disabled = false;
const boardConfig = {
milestoneTitle: 'Milestone 1',
};
mutations[types.SET_INITIAL_BOARD_DATA](state, {
...endpoints,
boardType,
disabled,
boardConfig,
});
expect(state.endpoints).toEqual(endpoints);
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
expect(state.boardConfig).toEqual(boardConfig);
});
});
......
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