Commit ac10aa1a authored by Phil Hughes's avatar Phil Hughes

Merge branch '218621-add-milestone-filter-roadmap' into 'master'

Add milestone filtering support in Roadmap

Closes #218621

See merge request gitlab-org/gitlab!39181
parents 10a69a10 6b5b7b4e
...@@ -16,6 +16,7 @@ import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/uti ...@@ -16,6 +16,7 @@ import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/uti
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import { EPICS_STATES, PRESET_TYPES } from '../constants'; import { EPICS_STATES, PRESET_TYPES } from '../constants';
...@@ -59,6 +60,7 @@ export default { ...@@ -59,6 +60,7 @@ export default {
'sortedBy', 'sortedBy',
'fullPath', 'fullPath',
'groupLabelsEndpoint', 'groupLabelsEndpoint',
'groupMilestonesEndpoint',
'filterParams', 'filterParams',
]), ]),
selectedEpicStateTitle() { selectedEpicStateTitle() {
...@@ -108,10 +110,32 @@ export default { ...@@ -108,10 +110,32 @@ export default {
}); });
}, },
}, },
{
type: 'milestone_title',
icon: 'clock',
title: __('Milestone'),
unique: true,
symbol: '%',
token: MilestoneToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchMilestones: (search = '') => {
return axios.get(this.groupMilestonesEndpoint).then(({ data }) => {
// TODO: Remove below condition check once either of the following is supported.
// a) Milestones Private API supports search param.
// b) Milestones Public API supports including child projects' milestones.
if (search) {
return {
data: data.filter(m => m.title.toLowerCase().includes(search.toLowerCase())),
};
}
return { data };
});
},
},
]; ];
}, },
getFilteredSearchValue() { getFilteredSearchValue() {
const { authorUsername, labelName, search } = this.filterParams || {}; const { authorUsername, labelName, milestoneTitle, search } = this.filterParams || {};
const filteredSearchValue = []; const filteredSearchValue = [];
if (authorUsername) { if (authorUsername) {
...@@ -121,6 +145,13 @@ export default { ...@@ -121,6 +145,13 @@ export default {
}); });
} }
if (milestoneTitle) {
filteredSearchValue.push({
type: 'milestone_title',
value: { data: milestoneTitle },
});
}
if (labelName?.length) { if (labelName?.length) {
filteredSearchValue.push( filteredSearchValue.push(
...labelName.map(label => ({ ...labelName.map(label => ({
...@@ -138,7 +169,7 @@ export default { ...@@ -138,7 +169,7 @@ export default {
}, },
updateUrl() { updateUrl() {
const queryParams = urlParamsToObject(window.location.search); const queryParams = urlParamsToObject(window.location.search);
const { authorUsername, labelName, search } = this.filterParams || {}; const { authorUsername, labelName, milestoneTitle, search } = this.filterParams || {};
queryParams.state = this.epicsState; queryParams.state = this.epicsState;
queryParams.sort = this.sortedBy; queryParams.sort = this.sortedBy;
...@@ -149,6 +180,12 @@ export default { ...@@ -149,6 +180,12 @@ export default {
delete queryParams.author_username; delete queryParams.author_username;
} }
if (milestoneTitle) {
queryParams.milestone_title = milestoneTitle;
} else {
delete queryParams.milestone_title;
}
delete queryParams.label_name; delete queryParams.label_name;
if (labelName?.length) { if (labelName?.length) {
queryParams['label_name[]'] = labelName; queryParams['label_name[]'] = labelName;
...@@ -182,10 +219,18 @@ export default { ...@@ -182,10 +219,18 @@ export default {
filters.forEach(filter => { filters.forEach(filter => {
if (typeof filter === 'object') { if (typeof filter === 'object') {
if (filter.type === 'author_username') { switch (filter.type) {
filterParams.authorUsername = filter.value.data; case 'author_username':
} else if (filter.type === 'label_name') { filterParams.authorUsername = filter.value.data;
labels.push(filter.value.data); break;
case 'milestone_title':
filterParams.milestoneTitle = filter.value.data;
break;
case 'label_name':
labels.push(filter.value.data);
break;
default:
break;
} }
} else { } else {
filterParams.search = filter; filterParams.search = filter;
......
...@@ -8,6 +8,7 @@ query groupEpics( ...@@ -8,6 +8,7 @@ query groupEpics(
$dueDate: Time $dueDate: Time
$labelName: [String!] = [] $labelName: [String!] = []
$authorUsername: String = "" $authorUsername: String = ""
$milestoneTitle: String = ""
$search: String = "" $search: String = ""
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
...@@ -20,6 +21,7 @@ query groupEpics( ...@@ -20,6 +21,7 @@ query groupEpics(
endDate: $dueDate endDate: $dueDate
labelName: $labelName labelName: $labelName
authorUsername: $authorUsername authorUsername: $authorUsername
milestoneTitle: $milestoneTitle
search: $search search: $search
) { ) {
edges { edges {
......
...@@ -87,6 +87,7 @@ export default () => { ...@@ -87,6 +87,7 @@ export default () => {
epicIid: dataset.iid, epicIid: dataset.iid,
newEpicEndpoint: dataset.newEpicEndpoint, newEpicEndpoint: dataset.newEpicEndpoint,
groupLabelsEndpoint: dataset.groupLabelsEndpoint, groupLabelsEndpoint: dataset.groupLabelsEndpoint,
groupMilestonesEndpoint: dataset.groupMilestonesEndpoint,
epicsState: dataset.epicsState, epicsState: dataset.epicsState,
sortedBy: dataset.sortedBy, sortedBy: dataset.sortedBy,
filterQueryString, filterQueryString,
...@@ -110,6 +111,7 @@ export default () => { ...@@ -110,6 +111,7 @@ export default () => {
filterParams: this.filterParams, filterParams: this.filterParams,
initialEpicsPath: this.initialEpicsPath, initialEpicsPath: this.initialEpicsPath,
groupLabelsEndpoint: this.groupLabelsEndpoint, groupLabelsEndpoint: this.groupLabelsEndpoint,
groupMilestonesEndpoint: this.groupMilestonesEndpoint,
defaultInnerHeight: this.defaultInnerHeight, defaultInnerHeight: this.defaultInnerHeight,
isChildEpics: this.isChildEpics, isChildEpics: this.isChildEpics,
hasFiltersApplied: this.hasFiltersApplied, hasFiltersApplied: this.hasFiltersApplied,
......
...@@ -6,6 +6,7 @@ export default () => ({ ...@@ -6,6 +6,7 @@ export default () => ({
initialEpicsPath: '', initialEpicsPath: '',
filterParams: null, filterParams: null,
groupLabelsEndpoint: '', groupLabelsEndpoint: '',
groupMilestonesEndpoint: '',
// Data // Data
epicIid: '', epicIid: '',
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
has_filters_applied: "#{has_filters_applied}", has_filters_applied: "#{has_filters_applied}",
new_epic_endpoint: group_epics_path(@group), new_epic_endpoint: group_epics_path(@group),
group_labels_endpoint: group_labels_path(@group, format: :json), group_labels_endpoint: group_labels_path(@group, format: :json),
group_milestones_endpoint: group_milestones_path(@group, format: :json),
preset_type: roadmap_layout, preset_type: roadmap_layout,
epics_state: @epics_state, epics_state: @epics_state,
sorted_by: @sort, sorted_by: @sort,
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
...@@ -93,6 +94,7 @@ describe('RoadmapFilters', () => { ...@@ -93,6 +94,7 @@ describe('RoadmapFilters', () => {
wrapper.vm.$store.dispatch('setFilterParams', { wrapper.vm.$store.dispatch('setFilterParams', {
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0',
}); });
wrapper.vm.$store.dispatch('setSortedBy', 'end_date_asc'); wrapper.vm.$store.dispatch('setSortedBy', 'end_date_asc');
...@@ -101,7 +103,7 @@ describe('RoadmapFilters', () => { ...@@ -101,7 +103,7 @@ describe('RoadmapFilters', () => {
wrapper.vm.updateUrl(); wrapper.vm.updateUrl();
expect(global.window.location.href).toBe( expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&author_username=root&label_name%5B%5D=Bug`, `${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&author_username=root&milestone_title=4.0&label_name%5B%5D=Bug`,
); );
}); });
}); });
...@@ -186,6 +188,16 @@ describe('RoadmapFilters', () => { ...@@ -186,6 +188,16 @@ describe('RoadmapFilters', () => {
operators: [{ value: '=', description: 'is', default: 'true' }], operators: [{ value: '=', description: 'is', default: 'true' }],
fetchLabels: expect.any(Function), fetchLabels: expect.any(Function),
}, },
{
type: 'milestone_title',
icon: 'clock',
title: 'Milestone',
unique: true,
symbol: '%',
token: MilestoneToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
fetchMilestones: expect.any(Function),
},
]); ]);
}); });
......
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