Commit 4262129a authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '218624-filter-roadmap-by-confidentiality' into 'master'

Add confidential filter token in Roadmap

See merge request gitlab-org/gitlab!51196
parents 4a5ce587 84be43d0
...@@ -256,7 +256,7 @@ export default { ...@@ -256,7 +256,7 @@ export default {
return { return {
...filter, ...filter,
value: { value: {
data: stripQuotes(valueString), data: typeof valueString === 'string' ? stripQuotes(valueString) : valueString,
operator: filter.value.operator, operator: filter.value.operator,
}, },
}; };
......
...@@ -67,8 +67,9 @@ You can also filter epics in the Roadmap view by: ...@@ -67,8 +67,9 @@ You can also filter epics in the Roadmap view by:
- Author - Author
- Label - Label
- Milestone - Milestone
- Confidentiality of epics
![roadmap date range in weeks](img/roadmap_filters_v13_7.png) ![roadmap date range in weeks](img/roadmap_filters_v13_8.png)
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics). Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlDropdownDivider,
GlFilteredSearchToken,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -132,10 +133,23 @@ export default { ...@@ -132,10 +133,23 @@ export default {
}); });
}, },
}, },
{
type: 'confidential',
icon: 'eye-slash',
title: __('Confidential'),
unique: true,
token: GlFilteredSearchToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
options: [
{ icon: 'eye-slash', value: true, title: __('Yes') },
{ icon: 'eye', value: false, title: __('No') },
],
},
]; ];
}, },
getFilteredSearchValue() { getFilteredSearchValue() {
const { authorUsername, labelName, milestoneTitle, search } = this.filterParams || {}; const { authorUsername, labelName, milestoneTitle, confidential, search } =
this.filterParams || {};
const filteredSearchValue = []; const filteredSearchValue = [];
if (authorUsername) { if (authorUsername) {
...@@ -161,6 +175,13 @@ export default { ...@@ -161,6 +175,13 @@ export default {
); );
} }
if (confidential !== undefined) {
filteredSearchValue.push({
type: 'confidential',
value: { data: confidential },
});
}
if (search) { if (search) {
filteredSearchValue.push(search); filteredSearchValue.push(search);
} }
...@@ -169,7 +190,8 @@ export default { ...@@ -169,7 +190,8 @@ export default {
}, },
updateUrl() { updateUrl() {
const queryParams = urlParamsToObject(window.location.search); const queryParams = urlParamsToObject(window.location.search);
const { authorUsername, labelName, milestoneTitle, search } = this.filterParams || {}; const { authorUsername, labelName, milestoneTitle, confidential, search } =
this.filterParams || {};
queryParams.state = this.epicsState; queryParams.state = this.epicsState;
queryParams.sort = this.sortedBy; queryParams.sort = this.sortedBy;
...@@ -191,6 +213,12 @@ export default { ...@@ -191,6 +213,12 @@ export default {
queryParams['label_name[]'] = labelName; queryParams['label_name[]'] = labelName;
} }
if (confidential !== undefined) {
queryParams.confidential = confidential;
} else {
delete queryParams.confidential;
}
if (search) { if (search) {
queryParams.search = search; queryParams.search = search;
} else { } else {
...@@ -229,6 +257,9 @@ export default { ...@@ -229,6 +257,9 @@ export default {
case 'label_name': case 'label_name':
labels.push(filter.value.data); labels.push(filter.value.data);
break; break;
case 'confidential':
filterParams.confidential = filter.value.data;
break;
default: default:
break; break;
} }
......
...@@ -9,6 +9,7 @@ fragment BaseEpic on Epic { ...@@ -9,6 +9,7 @@ fragment BaseEpic on Epic {
dueDate dueDate
hasChildren hasChildren
hasParent hasParent
confidential
descendantWeightSum { descendantWeightSum {
closedIssues closedIssues
openedIssues openedIssues
......
...@@ -9,6 +9,7 @@ query epicChildEpics( ...@@ -9,6 +9,7 @@ query epicChildEpics(
$dueDate: Time $dueDate: Time
$labelName: [String!] = [] $labelName: [String!] = []
$authorUsername: String = "" $authorUsername: String = ""
$confidential: Boolean
$search: String = "" $search: String = ""
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
...@@ -25,6 +26,7 @@ query epicChildEpics( ...@@ -25,6 +26,7 @@ query epicChildEpics(
endDate: $dueDate endDate: $dueDate
labelName: $labelName labelName: $labelName
authorUsername: $authorUsername authorUsername: $authorUsername
confidential: $confidential
search: $search search: $search
) { ) {
edges { edges {
......
...@@ -9,6 +9,7 @@ query groupEpics( ...@@ -9,6 +9,7 @@ query groupEpics(
$labelName: [String!] = [] $labelName: [String!] = []
$authorUsername: String = "" $authorUsername: String = ""
$milestoneTitle: String = "" $milestoneTitle: String = ""
$confidential: Boolean
$search: String = "" $search: String = ""
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
...@@ -22,6 +23,7 @@ query groupEpics( ...@@ -22,6 +23,7 @@ query groupEpics(
labelName: $labelName labelName: $labelName
authorUsername: $authorUsername authorUsername: $authorUsername
milestoneTitle: $milestoneTitle milestoneTitle: $milestoneTitle
confidential: $confidential
search: $search search: $search
) { ) {
edges { edges {
......
...@@ -57,11 +57,17 @@ export default () => { ...@@ -57,11 +57,17 @@ export default () => {
supportedPresetTypes.indexOf(dataset.presetType) > -1 supportedPresetTypes.indexOf(dataset.presetType) > -1
? dataset.presetType ? dataset.presetType
: PRESET_TYPES.MONTHS; : PRESET_TYPES.MONTHS;
const filterParams = Object.assign( const rawFilterParams = urlParamsToObject(window.location.search.substring(1));
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search.substring(1)), { const filterParams = {
...convertObjectPropsToCamelCase(rawFilterParams, {
dropKeys: ['scope', 'utf8', 'state', 'sort', 'layout'], // These keys are unsupported/unnecessary dropKeys: ['scope', 'utf8', 'state', 'sort', 'layout'], // These keys are unsupported/unnecessary
}), }),
); // We shall put parsed value of `confidential` only
// when it is defined.
...(rawFilterParams.confidential && {
confidential: parseBoolean(rawFilterParams.confidential),
}),
};
const timeframe = getTimeframeForPreset( const timeframe = getTimeframeForPreset(
presetType, presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH, window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
......
---
title: Add confidential filter token in Roadmap
merge_request: 51196
author:
type: added
import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlSegmentedControl, GlDropdown, GlDropdownItem, GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -89,6 +89,7 @@ describe('RoadmapFilters', () => { ...@@ -89,6 +89,7 @@ describe('RoadmapFilters', () => {
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0', milestoneTitle: '4.0',
confidential: true,
}); });
wrapper.vm.$store.dispatch('setSortedBy', 'end_date_asc'); wrapper.vm.$store.dispatch('setSortedBy', 'end_date_asc');
...@@ -97,7 +98,7 @@ describe('RoadmapFilters', () => { ...@@ -97,7 +98,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&milestone_title=4.0&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&confidential=true`,
); );
}); });
}); });
...@@ -143,10 +144,18 @@ describe('RoadmapFilters', () => { ...@@ -143,10 +144,18 @@ describe('RoadmapFilters', () => {
type: 'author_username', type: 'author_username',
value: { data: 'root' }, value: { data: 'root' },
}, },
{
type: 'milestone_title',
value: { data: '4.0' },
},
{ {
type: 'label_name', type: 'label_name',
value: { data: 'Bug' }, value: { data: 'Bug' },
}, },
{
type: 'confidential',
value: { data: true },
},
]; ];
let filteredSearchBar; let filteredSearchBar;
...@@ -192,6 +201,18 @@ describe('RoadmapFilters', () => { ...@@ -192,6 +201,18 @@ describe('RoadmapFilters', () => {
operators: [{ value: '=', description: 'is', default: 'true' }], operators: [{ value: '=', description: 'is', default: 'true' }],
fetchMilestones: expect.any(Function), fetchMilestones: expect.any(Function),
}, },
{
type: 'confidential',
icon: 'eye-slash',
title: 'Confidential',
unique: true,
token: GlFilteredSearchToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
options: [
{ icon: 'eye-slash', value: true, title: 'Yes' },
{ icon: 'eye', value: false, title: 'No' },
],
},
]); ]);
}); });
...@@ -220,6 +241,8 @@ describe('RoadmapFilters', () => { ...@@ -220,6 +241,8 @@ describe('RoadmapFilters', () => {
wrapper.vm.$store.dispatch('setFilterParams', { wrapper.vm.$store.dispatch('setFilterParams', {
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0',
confidential: true,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -241,6 +264,8 @@ describe('RoadmapFilters', () => { ...@@ -241,6 +264,8 @@ describe('RoadmapFilters', () => {
expect(wrapper.vm.setFilterParams).toHaveBeenCalledWith({ expect(wrapper.vm.setFilterParams).toHaveBeenCalledWith({
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0',
confidential: true,
}); });
expect(wrapper.vm.fetchEpics).toHaveBeenCalled(); expect(wrapper.vm.fetchEpics).toHaveBeenCalled();
expect(wrapper.vm.updateUrl).toHaveBeenCalled(); expect(wrapper.vm.updateUrl).toHaveBeenCalled();
......
...@@ -25,6 +25,7 @@ import { ...@@ -25,6 +25,7 @@ import {
tokenValueLabel, tokenValueLabel,
tokenValueMilestone, tokenValueMilestone,
tokenValueMembership, tokenValueMembership,
tokenValueConfidential,
} from './mock_data'; } from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...@@ -227,12 +228,13 @@ describe('FilteredSearchBarRoot', () => { ...@@ -227,12 +228,13 @@ describe('FilteredSearchBarRoot', () => {
}); });
describe('removeQuotesEnclosure', () => { describe('removeQuotesEnclosure', () => {
const mockFilters = [tokenValueAuthor, tokenValueLabel, 'foo']; const mockFilters = [tokenValueAuthor, tokenValueLabel, tokenValueConfidential, 'foo'];
it('returns filter array with unescaped strings for values which have spaces', () => { it('returns filter array with unescaped strings for values which have spaces', () => {
expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([ expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
tokenValueAuthor, tokenValueAuthor,
tokenValueLabel, tokenValueLabel,
tokenValueConfidential,
'foo', 'foo',
]); ]);
}); });
......
...@@ -155,6 +155,14 @@ export const tokenValueMembership = { ...@@ -155,6 +155,14 @@ export const tokenValueMembership = {
}, },
}; };
export const tokenValueConfidential = {
type: 'confidential',
value: {
operator: '=',
data: true,
},
};
export const tokenValuePlain = { export const tokenValuePlain = {
type: 'filtered-search-term', type: 'filtered-search-term',
value: { data: 'foo' }, value: { data: 'foo' },
......
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