Commit bd73aa74 authored by Simon Knox's avatar Simon Knox

Merge branch...

Merge branch '287604-boards-refactor-refactor-filters-so-that-it-doesn-t-rely-on-boardsstore' into 'master'

Refactor GraphQL boards filters and board scope [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!49533
parents 394eb465 8520d876
......@@ -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,8 +57,10 @@ class BoardsStoreEE {
this.store.scopedLabels = {
enabled: parseBoolean(scopedLabels),
};
if (!gon.features.graphqlBoardLists) {
this.initBoardFilters();
}
}
};
this.store.updateFiltersUrl = (replaceState = false) => {
......
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