Commit 930271f4 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '352331-roadmap-settings-milestones' into 'master'

Roadmap milestones settings

See merge request gitlab-org/gitlab!80248
parents 319c3fa5 64a7b7b5
......@@ -71,7 +71,15 @@ export default {
};
},
computed: {
...mapState(['presetType', 'epicsState', 'sortedBy', 'filterParams', 'progressTracking']),
...mapState([
'presetType',
'epicsState',
'sortedBy',
'filterParams',
'progressTracking',
'isShowingMilestones',
'milestonesType',
]),
selectedEpicStateTitle() {
if (this.epicsState === EPICS_STATES.ALL) {
return __('All epics');
......
<script>
import { GlFormGroup, GlFormRadioGroup, GlToggle } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import { MILESTONES_OPTIONS } from '../constants';
export default {
components: {
GlFormGroup,
GlFormRadioGroup,
GlToggle,
},
computed: {
...mapState(['milestonesType', 'isShowingMilestones']),
},
methods: {
...mapActions(['setMilestonesType', 'toggleMilestones']),
handleMilestonesChange(option) {
if (option !== this.milestonesType) {
this.setMilestonesType(option);
}
},
},
i18n: {
header: __('Milestones'),
toggleLabel: __('Display milestones'),
},
MILESTONES_OPTIONS,
tracking: {
label: 'roadmap_settings_milestones',
property: 'toggle_milestones',
},
};
</script>
<template>
<div>
<gl-form-group
class="gl-mb-0"
:label="$options.i18n.header"
data-testid="roadmap-milestones-settings"
>
<gl-toggle
:value="isShowingMilestones"
:label="$options.i18n.toggleLabel"
label-position="hidden"
data-track-action="click_toggle"
:data-track-label="$options.tracking.label"
:data-track-property="$options.tracking.property"
@change="toggleMilestones"
/>
<gl-form-radio-group
v-if="isShowingMilestones"
:checked="milestonesType"
stacked
:options="$options.MILESTONES_OPTIONS"
class="gl-mt-3"
data-track-action="click_radio"
:data-track-label="$options.tracking.label"
:data-track-property="$options.tracking.property"
@change="handleMilestonesChange"
/>
</gl-form-group>
</div>
</template>
......@@ -2,12 +2,14 @@
import { GlDrawer } from '@gitlab/ui';
import RoadmapDaterange from './roadmap_daterange.vue';
import RoadmapEpicsState from './roadmap_epics_state.vue';
import RoadmapMilestones from './roadmap_milestones.vue';
import RoadmapProgressTracking from './roadmap_progress_tracking.vue';
export default {
components: {
GlDrawer,
RoadmapDaterange,
RoadmapMilestones,
RoadmapEpicsState,
RoadmapProgressTracking,
},
......@@ -48,6 +50,7 @@ export default {
</template>
<template #default>
<roadmap-daterange :timeframe-range-type="timeframeRangeType" />
<roadmap-milestones />
<roadmap-epics-state />
<roadmap-progress-tracking />
</template>
......
......@@ -2,6 +2,7 @@
import { mapState } from 'vuex';
import eventHub from '../event_hub';
import { MILESTONES_GROUP, MILESTONES_SUBGROUP, MILESTONES_PROJECT } from '../constants';
import epicsListSection from './epics_list_section.vue';
import milestonesListSection from './milestones_list_section.vue';
......@@ -40,9 +41,21 @@ export default {
},
},
computed: {
...mapState(['defaultInnerHeight']),
...mapState(['defaultInnerHeight', 'isShowingMilestones', 'milestonesType']),
displayMilestones() {
return Boolean(this.milestones.length);
return Boolean(this.milestones.length) && this.isShowingMilestones;
},
milestonesToShow() {
switch (this.milestonesType) {
case MILESTONES_GROUP:
return this.milestones.filter((m) => m.groupMilestone && !m.subgroupMilestone);
case MILESTONES_SUBGROUP:
return this.milestones.filter((m) => m.subgroupMilestone);
case MILESTONES_PROJECT:
return this.milestones.filter((m) => m.projectMilestone);
default:
return this.milestones;
}
},
},
methods: {
......@@ -70,7 +83,7 @@ export default {
<milestones-list-section
v-if="displayMilestones"
:preset-type="presetType"
:milestones="milestones"
:milestones="milestonesToShow"
:timeframe="timeframe"
:current-group-id="currentGroupId"
/>
......
......@@ -79,3 +79,15 @@ export const UNSUPPORTED_ROADMAP_PARAMS = [
'layout',
'progress',
];
export const MILESTONES_ALL = 'ALL';
export const MILESTONES_GROUP = 'GROUP';
export const MILESTONES_SUBGROUP = 'SUBGROUP';
export const MILESTONES_PROJECT = 'PROJECT';
export const MILESTONES_OPTIONS = [
{ text: __('Show all milestones'), value: MILESTONES_ALL },
{ text: __('Show group milestones'), value: MILESTONES_GROUP },
{ text: __('Show sub-group milestones'), value: MILESTONES_SUBGROUP },
{ text: __('Show project milestones'), value: MILESTONES_PROJECT },
];
......@@ -54,6 +54,8 @@ export default {
'not[my_reaction_emoji]': notMyReactionEmoji,
'not[label_name][]': notLabelName,
progress: this.progressTracking,
show_milestones: this.isShowingMilestones,
milestones_type: this.milestonesType,
};
},
},
......
......@@ -11,7 +11,12 @@ import EpicItem from './components/epic_item.vue';
import EpicItemContainer from './components/epic_item_container.vue';
import roadmapApp from './components/roadmap_app.vue';
import { DATE_RANGES, PROGRESS_WEIGHT, UNSUPPORTED_ROADMAP_PARAMS } from './constants';
import {
DATE_RANGES,
PROGRESS_WEIGHT,
UNSUPPORTED_ROADMAP_PARAMS,
MILESTONES_ALL,
} from './constants';
import createStore from './store';
import {
......@@ -104,6 +109,11 @@ export default () => {
presetType,
timeframe,
progressTracking: rawFilterParams.progress || PROGRESS_WEIGHT,
isShowingMilestones:
rawFilterParams.show_milestones === undefined
? true
: parseBoolean(rawFilterParams.show_milestones),
milestonesType: rawFilterParams.milestones_type || MILESTONES_ALL,
};
},
created() {
......@@ -123,6 +133,8 @@ export default () => {
hasFiltersApplied: this.hasFiltersApplied,
allowSubEpics: this.allowSubEpics,
progressTracking: this.progressTracking,
isShowingMilestones: this.isShowingMilestones,
milestonesType: this.milestonesType,
});
},
methods: {
......
......@@ -329,3 +329,8 @@ export const setSortedBy = ({ commit }, sortedBy) => commit(types.SET_SORTED_BY,
export const setProgressTracking = ({ commit }, progressTracking) =>
commit(types.SET_PROGRESS_TRACKING, progressTracking);
export const setMilestonesType = ({ commit }, milestonesType) =>
commit(types.SET_MILESTONES_TYPE, milestonesType);
export const toggleMilestones = ({ commit }) => commit(types.TOGGLE_MILESTONES);
......@@ -33,3 +33,5 @@ export const SET_DATERANGE = 'SET_DATERANGE';
export const SET_FILTER_PARAMS = 'SET_FILTER_PARAMS';
export const SET_SORTED_BY = 'SET_SORTED_BY';
export const SET_PROGRESS_TRACKING = 'SET_PROGRESS_TRACKING';
export const SET_MILESTONES_TYPE = 'SET_MILESTONES_TYPE';
export const TOGGLE_MILESTONES = 'TOGGLE_MILESTONES';
......@@ -144,4 +144,12 @@ export default {
[types.SET_PROGRESS_TRACKING](state, progressTracking) {
state.progressTracking = progressTracking;
},
[types.SET_MILESTONES_TYPE](state, milestonesType) {
state.milestonesType = milestonesType;
},
[types.TOGGLE_MILESTONES](state) {
state.isShowingMilestones = !state.isShowingMilestones;
},
};
......@@ -4,6 +4,8 @@ export default () => ({
epicsState: '',
progressTracking: '',
filterParams: null,
isShowingMilestones: true,
milestonesType: '',
// Data
epicIid: '',
......
......@@ -8,8 +8,9 @@ RSpec.describe 'group epic roadmap', :js do
let(:user) { create(:user) }
let(:user_dev) { create(:user) }
let(:group) { create(:group) }
let(:milestone) { create(:milestone, group: group) }
let(:group) { create(:group, :public) }
let(:subgroup) { create(:group, :public, parent: group) }
let(:project) { create(:project, :public, group: group) }
let!(:bug_label) { create(:group_label, group: group, title: 'Bug') }
let!(:critical_label) { create(:group_label, group: group, title: 'Critical') }
......@@ -148,6 +149,10 @@ RSpec.describe 'group epic roadmap', :js do
let!(:epic_with_bug) { create(:labeled_epic, group: group, start_date: 10.days.ago, end_date: 1.day.ago, labels: [bug_label]) }
let!(:epic_with_critical) { create(:labeled_epic, group: group, start_date: 20.days.ago, end_date: 2.days.ago, labels: [critical_label]) }
let!(:closed_epic) { create(:epic, :closed, group: group, start_date: 20.days.ago, end_date: 2.days.ago) }
let!(:milestone) { create(:milestone, :with_dates, group: group, start_date: 10.days.ago, due_date: 1.day.ago) }
let!(:milestone_subgroup) { create(:milestone, :with_dates, group: subgroup, start_date: 10.days.ago, due_date: 1.day.ago) }
let!(:milestone_project) { create(:milestone, :with_dates, project: project, start_date: 10.days.ago, due_date: 1.day.ago) }
let!(:milestone_project_2) { create(:milestone, :with_dates, project: project, start_date: 10.days.ago, due_date: 1.day.ago) }
before do
visit group_roadmap_path(group)
......@@ -242,6 +247,55 @@ RSpec.describe 'group epic roadmap', :js do
end
end
describe 'roadmap milestones settings' do
def select_milestones(milestones)
page.within('[data-testid="roadmap-milestones-settings"]') do
choose milestones
end
end
def expect_milestones_count(count)
page.within('.roadmap-container .milestones-list-section') do
expect(page).to have_selector('.milestone-item-details', count: count)
end
end
before do
open_settings_sidebar
end
it 'renders milestones section' do
page.within('.roadmap-container') do
expect(page).to have_selector('.milestones-list-section')
end
end
it 'renders milestones based on filter' do
milestones_counts = {
'Show all milestones' => 4,
'Show group milestones' => 1,
'Show sub-group milestones' => 1,
'Show project milestones' => 2
}
milestones_counts.each do |filter, count|
select_milestones(filter)
expect_milestones_count(count)
end
end
it 'turns off milestones' do
page.within('[data-testid="roadmap-milestones-settings"]') do
click_button class: 'gl-toggle'
end
page.within('.roadmap-container') do
expect(page).not_to have_selector('.milestones-list-section')
end
end
end
it 'renders the filtered search bar correctly' do
page.within('.content-wrapper .content .epics-filters') do
expect(page).to have_css('.vue-filtered-search-bar-container')
......
......@@ -3,7 +3,13 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue';
import { PRESET_TYPES, EPICS_STATES, DATE_RANGES, PROGRESS_WEIGHT } from 'ee/roadmap/constants';
import {
PRESET_TYPES,
EPICS_STATES,
DATE_RANGES,
PROGRESS_WEIGHT,
MILESTONES_ALL,
} from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
import {
......@@ -56,6 +62,7 @@ const createComponent = ({
filterParams,
timeframe,
progressTracking: PROGRESS_WEIGHT,
milestonesType: MILESTONES_ALL,
});
return shallowMountExtended(RoadmapFilters, {
......@@ -117,7 +124,7 @@ describe('RoadmapFilters', () => {
await nextTick();
expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&layout=MONTHS&author_username=root&label_name%5B%5D=Bug&milestone_title=4.0&confidential=true&progress=WEIGHT`,
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&layout=MONTHS&author_username=root&label_name%5B%5D=Bug&milestone_title=4.0&confidential=true&progress=WEIGHT&show_milestones=true&milestones_type=ALL`,
);
});
});
......
import { GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createStore from 'ee/roadmap/store';
import RoadmapMilestones from 'ee/roadmap/components/roadmap_milestones.vue';
import { MILESTONES_ALL, MILESTONES_OPTIONS } from 'ee/roadmap/constants';
describe('RoadmapMilestones', () => {
let wrapper;
const createComponent = ({ isShowingMilestones = true } = {}) => {
const store = createStore();
store.dispatch('setInitialData', {
milestonesType: MILESTONES_ALL,
isShowingMilestones,
});
wrapper = shallowMountExtended(RoadmapMilestones, {
store,
});
};
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders form group', () => {
expect(findFormGroup().exists()).toBe(true);
expect(findFormGroup().attributes('label')).toBe('Milestones');
});
it.each`
isShowingMilestones
${true}
${false}
`('displays radio form group depending on isShowingMilestones', ({ isShowingMilestones }) => {
createComponent({ isShowingMilestones });
expect(findFormRadioGroup().exists()).toBe(isShowingMilestones);
if (isShowingMilestones) {
expect(findFormRadioGroup().props('options')).toEqual(MILESTONES_OPTIONS);
}
});
});
});
......@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RoadmapSettings from 'ee/roadmap/components/roadmap_settings.vue';
import RoadmapDaterange from 'ee/roadmap/components/roadmap_daterange.vue';
import RoadmapEpicsState from 'ee/roadmap/components/roadmap_epics_state.vue';
import RoadmapMilestones from 'ee/roadmap/components/roadmap_milestones.vue';
import RoadmapProgressTracking from 'ee/roadmap/components/roadmap_progress_tracking.vue';
describe('RoadmapSettings', () => {
......@@ -16,6 +17,7 @@ describe('RoadmapSettings', () => {
const findSettingsDrawer = () => wrapper.findComponent(GlDrawer);
const findDaterange = () => wrapper.findComponent(RoadmapDaterange);
const findMilestones = () => wrapper.findComponent(RoadmapMilestones);
const findEpicsSate = () => wrapper.findComponent(RoadmapEpicsState);
const findProgressTracking = () => wrapper.findComponent(RoadmapProgressTracking);
......@@ -37,6 +39,10 @@ describe('RoadmapSettings', () => {
expect(findDaterange().exists()).toBe(true);
});
it('renders roadmap milestones component', () => {
expect(findMilestones().exists()).toBe(true);
});
it('renders roadmap epics state component', () => {
expect(findEpicsSate().exists()).toBe(true);
});
......
import MockAdapter from 'axios-mock-adapter';
import { DATE_RANGES, PRESET_TYPES } from 'ee/roadmap/constants';
import { DATE_RANGES, PRESET_TYPES, MILESTONES_GROUP } from 'ee/roadmap/constants';
import groupMilestones from 'ee/roadmap/queries/groupMilestones.query.graphql';
import epicChildEpics from 'ee/roadmap/queries/epicChildEpics.query.graphql';
import * as actions from 'ee/roadmap/store/actions';
......@@ -672,4 +672,28 @@ describe('Roadmap Vuex Actions', () => {
);
});
});
describe('setMilestonesType', () => {
it('should set milestonesType in store state', () => {
return testAction(
actions.setMilestonesType,
MILESTONES_GROUP,
state,
[{ type: types.SET_MILESTONES_TYPE, payload: MILESTONES_GROUP }],
[],
);
});
});
describe('toggleMilestones', () => {
it('commit TOGGLE_MILESTONES mutation', () => {
return testAction(
actions.toggleMilestones,
undefined,
state,
[{ type: types.TOGGLE_MILESTONES }],
[],
);
});
});
});
import * as types from 'ee/roadmap/store/mutation_types';
import mutations from 'ee/roadmap/store/mutations';
import { PROGRESS_COUNT } from 'ee/roadmap/constants';
import { PROGRESS_COUNT, MILESTONES_GROUP } from 'ee/roadmap/constants';
import defaultState from 'ee/roadmap/store/state';
import { getTimeframeForRangeType } from 'ee/roadmap/utils/roadmap_utils';
......@@ -356,4 +356,31 @@ describe('Roadmap Store Mutations', () => {
});
});
});
describe('SET_MILESTONES_TYPE', () => {
it('Should set `milestonesType` to the state', () => {
const milestonesType = MILESTONES_GROUP;
setEpicMockData(state);
mutations[types.SET_MILESTONES_TYPE](state, milestonesType);
expect(state).toMatchObject({
milestonesType,
});
});
});
describe('TOGGLE_MILESTONES', () => {
it('Should toggle `isShowingMilestones` on state', () => {
expect(state).toMatchObject({
isShowingMilestones: true,
});
mutations[types.TOGGLE_MILESTONES](state);
expect(state).toMatchObject({
isShowingMilestones: false,
});
});
});
});
......@@ -12833,6 +12833,9 @@ msgstr ""
msgid "Display alerts from all configured monitoring tools."
msgstr ""
msgid "Display milestones"
msgstr ""
msgid "Display name"
msgstr ""
......@@ -33406,6 +33409,9 @@ msgstr ""
msgid "Show all issues."
msgstr ""
msgid "Show all milestones"
msgstr ""
msgid "Show all test cases."
msgstr ""
......@@ -33442,6 +33448,9 @@ msgstr ""
msgid "Show file contents"
msgstr ""
msgid "Show group milestones"
msgstr ""
msgid "Show labels"
msgstr ""
......@@ -33457,6 +33466,12 @@ msgstr ""
msgid "Show open epics"
msgstr ""
msgid "Show project milestones"
msgstr ""
msgid "Show sub-group milestones"
msgstr ""
msgid "Show the Closed list"
msgstr ""
......
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