Commit 9d161a73 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '350830-cleanup-roadmap-settings-ff' into 'master'

Clean up roadmap_settings feature flag

See merge request gitlab-org/gitlab!80975
parents 882b1407 fb7bd832
---
name: roadmap_settings
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78626
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350830
milestone: '14.8'
type: development
group: group::product planning
default_enabled: true
......@@ -47,10 +47,6 @@ Filtering roadmaps by milestone might not be available to you. Check the **versi
When you want to explore a roadmap, there are several ways to make it easier by sorting epics or
filtering them by what's important for you.
A dropdown list lets you show only open or closed epics. By default, all epics are shown.
![epics state dropdown list](img/epics_state_dropdown_v14_3.png)
You can sort epics in the Roadmap view by:
- Start date
......@@ -74,11 +70,8 @@ Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-ep
### Roadmap settings
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345158) in GitLab 14.8 [with a flag](../../../administration/feature_flags.md) named `roadmap_settings`. Enabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `roadmap_settings`.
On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345158) in GitLab 14.8 [with a flag](../../../administration/feature_flags.md) named `roadmap_settings`. Enabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/350830) in GitLab 14.9. Feature flag `roadmap_settings`removed.
When you enable the roadmap settings sidebar, you can use it to refine epics shown in the roadmap.
......
......@@ -2,8 +2,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EpicsListEmpty from './epics_list_empty.vue';
import RoadmapFilters from './roadmap_filters.vue';
import RoadmapSettings from './roadmap_settings.vue';
......@@ -17,7 +15,6 @@ export default {
RoadmapSettings,
RoadmapShell,
},
mixins: [glFeatureFlagMixin()],
props: {
emptyStateIllustrationPath: {
type: String,
......@@ -75,11 +72,7 @@ export default {
<template>
<div class="roadmap-app-container gl-h-full">
<roadmap-filters
v-if="showFilteredSearchbar && !epicIid"
:timeframe-range-type="timeframeRangeType"
@toggleSettings="toggleSettings"
/>
<roadmap-filters v-if="showFilteredSearchbar && !epicIid" @toggleSettings="toggleSettings" />
<div :class="{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container gl-relative">
<gl-loading-icon v-if="epicsFetchInProgress" class="gl-mt-5" size="md" />
<epics-list-empty
......@@ -102,7 +95,6 @@ export default {
:is-settings-sidebar-open="isSettingsSidebarOpen"
/>
<roadmap-settings
v-if="glFeatures.roadmapSettings"
:is-open="isSettingsSidebarOpen"
:timeframe-range-type="timeframeRangeType"
data-testid="roadmap-settings"
......
<script>
import {
GlButton,
GlFormGroup,
GlSegmentedControl,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
} from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EPICS_STATES, PRESET_TYPES, DATE_RANGES } from '../constants';
import EpicsFilteredSearchMixin from '../mixins/filtered_search_mixin';
import { getPresetTypeForTimeframeRangeType } from '../utils/roadmap_utils';
const pickerType = {
Start: 'start',
End: 'end',
};
export default {
pickerType,
epicStates: EPICS_STATES,
availableDateRanges: [
{ text: s__('GroupRoadmap|This quarter'), value: DATE_RANGES.CURRENT_QUARTER },
{ text: s__('GroupRoadmap|This year'), value: DATE_RANGES.CURRENT_YEAR },
{ text: s__('GroupRoadmap|Within 3 years'), value: DATE_RANGES.THREE_YEARS },
],
availableSortOptions: [
{
id: 1,
......@@ -51,25 +29,9 @@ export default {
],
components: {
GlButton,
GlFormGroup,
GlSegmentedControl,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
FilteredSearchBar,
},
mixins: [EpicsFilteredSearchMixin, glFeatureFlagMixin()],
props: {
timeframeRangeType: {
type: String,
required: true,
},
},
data() {
return {
selectedDaterange: this.timeframeRangeType,
};
},
mixins: [EpicsFilteredSearchMixin],
computed: {
...mapState([
'presetType',
......@@ -81,38 +43,6 @@ export default {
'isShowingMilestones',
'milestonesType',
]),
selectedEpicStateTitle() {
if (this.epicsState === EPICS_STATES.ALL) {
return __('All epics');
} else if (this.epicsState === EPICS_STATES.OPENED) {
return __('Open epics');
}
return __('Closed epics');
},
daterangeDropdownText() {
switch (this.selectedDaterange) {
case DATE_RANGES.CURRENT_QUARTER:
return s__('GroupRoadmap|This quarter');
case DATE_RANGES.CURRENT_YEAR:
return s__('GroupRoadmap|This year');
case DATE_RANGES.THREE_YEARS:
return s__('GroupRoadmap|Within 3 years');
default:
return '';
}
},
availablePresets() {
const quarters = { text: __('Quarters'), value: PRESET_TYPES.QUARTERS };
const months = { text: __('Months'), value: PRESET_TYPES.MONTHS };
const weeks = { text: __('Weeks'), value: PRESET_TYPES.WEEKS };
if (this.selectedDaterange === DATE_RANGES.CURRENT_YEAR) {
return [months, weeks];
} else if (this.selectedDaterange === DATE_RANGES.THREE_YEARS) {
return [quarters, months, weeks];
}
return [];
},
},
watch: {
urlParams: {
......@@ -130,38 +60,7 @@ export default {
},
},
methods: {
...mapActions(['setEpicsState', 'setFilterParams', 'setSortedBy', 'fetchEpics']),
handleDaterangeSelect(value) {
this.selectedDaterange = value;
},
handleDaterangeDropdownOpen() {
this.initialSelectedDaterange = this.selectedDaterange;
},
handleDaterangeDropdownClose() {
if (this.initialSelectedDaterange !== this.selectedDaterange) {
visitUrl(
mergeUrlParams(
{
timeframe_range_type: this.selectedDaterange,
layout: getPresetTypeForTimeframeRangeType(this.selectedDaterange),
},
window.location.href,
),
);
}
},
handleRoadmapLayoutChange(presetType) {
visitUrl(
mergeUrlParams(
{ timeframe_range_type: this.selectedDaterange, layout: presetType },
window.location.href,
),
);
},
handleEpicStateChange(epicsState) {
this.setEpicsState(epicsState);
this.fetchEpics();
},
...mapActions(['setFilterParams', 'setSortedBy', 'fetchEpics']),
handleFilterEpics(filters, cleared) {
if (filters.length || cleared) {
this.setFilterParams(this.getFilterParams(filters));
......@@ -184,62 +83,6 @@ export default {
<div
class="epics-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-xl-flex-direction-row gl-pb-3 row-content-block second-block"
>
<gl-dropdown
v-if="!glFeatures.roadmapSettings"
icon="calendar"
class="gl-mr-0 gl-lg-mr-3 mb-sm-2 roadmap-daterange-dropdown"
toggle-class="gl-rounded-base!"
:text="daterangeDropdownText"
data-testid="daterange-dropdown"
@show="handleDaterangeDropdownOpen"
@hide="handleDaterangeDropdownClose"
>
<gl-dropdown-item
v-for="dateRange in $options.availableDateRanges"
:key="dateRange.value"
:value="dateRange.value"
@click="handleDaterangeSelect(dateRange.value)"
>{{ dateRange.text }}</gl-dropdown-item
>
</gl-dropdown>
<gl-form-group
v-if="availablePresets.length && !glFeatures.roadmapSettings"
class="gl-mr-0 gl-lg-mr-3 mb-sm-2"
>
<gl-segmented-control
:checked="presetType"
:options="availablePresets"
class="gl-display-flex d-xl-block"
buttons
@input="handleRoadmapLayoutChange"
/>
</gl-form-group>
<gl-dropdown
v-if="!glFeatures.roadmapSettings"
:text="selectedEpicStateTitle"
class="gl-mr-0 gl-lg-mr-3 mb-sm-2 dropdown-epics-state"
toggle-class="gl-rounded-base!"
>
<gl-dropdown-item
:is-check-item="true"
:is-checked="epicsState === $options.epicStates.ALL"
@click="handleEpicStateChange('all')"
>{{ __('All epics') }}</gl-dropdown-item
>
<gl-dropdown-divider />
<gl-dropdown-item
:is-check-item="true"
:is-checked="epicsState === $options.epicStates.OPENED"
@click="handleEpicStateChange('opened')"
>{{ __('Open epics') }}</gl-dropdown-item
>
<gl-dropdown-item
:is-check-item="true"
:is-checked="epicsState === $options.epicStates.CLOSED"
@click="handleEpicStateChange('closed')"
>{{ __('Closed epics') }}</gl-dropdown-item
>
</gl-dropdown>
<filtered-search-bar
:namespace="groupFullPath"
:search-input-placeholder="__('Search or filter results...')"
......@@ -253,7 +96,6 @@ export default {
@onSort="handleSortEpics"
/>
<gl-button
v-if="glFeatures.roadmapSettings"
icon="settings"
class="gl-mb-3 gl-xl-ml-3 gl-inset-border-1-gray-400!"
:aria-label="$options.i18n.settings"
......
......@@ -19,7 +19,7 @@ import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/m
import EpicToken from 'ee/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
export default {
inject: ['groupFullPath', 'groupMilestonesPath', 'listEpicsPath'],
inject: ['groupFullPath', 'groupMilestonesPath'],
computed: {
urlParams() {
const {
......
......@@ -8,10 +8,6 @@ module Groups
before_action :check_epics_available!
before_action :persist_roadmap_layout, only: [:show]
before_action do
push_frontend_feature_flag(:roadmap_settings, @group, default_enabled: :yaml)
end
feature_category :portfolio_management
urgency :medium, [:show]
......
......@@ -46,102 +46,6 @@ RSpec.describe 'group epic roadmap', :js do
sign_in(user)
end
context 'with roadmap_settings feature flag off' 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(:state_dropdown) { find('.dropdown-epics-state') }
before do
stub_feature_flags(roadmap_settings: false)
visit group_roadmap_path(group)
wait_for_requests
end
context 'roadmap daterange filtering' do
def select_date_range(range_type)
page.within('.epics-roadmap-filters') do
page.find('[data-testid="daterange-dropdown"] button.dropdown-toggle').click
click_button(range_type)
end
end
it 'renders daterange filtering dropdown with "This quarter" selected by default no layout presets available', :aggregate_failures do
page.within('.epics-roadmap-filters') do
expect(page).to have_selector('[data-testid="daterange-dropdown"]')
expect(page).not_to have_selector('.gl-segmented-control')
expect(page.find('[data-testid="daterange-dropdown"] button.dropdown-toggle')).to have_content('This quarter')
end
end
it 'selecting "This year" as daterange shows `Months` and `Weeks` layout presets', :aggregate_failures do
select_date_range('This year')
page.within('.epics-roadmap-filters') do
expect(page).to have_selector('.gl-segmented-control')
expect(page).to have_selector('input[value="MONTHS"]')
expect(page).to have_selector('input[value="WEEKS"]')
end
end
it 'selecting "Within 3 years" as daterange shows `Quarters`, `Months` and `Weeks` layout presets', :aggregate_failures do
select_date_range('Within 3 years')
page.within('.epics-roadmap-filters') do
expect(page).to have_selector('.gl-segmented-control')
expect(page).to have_selector('input[value="QUARTERS"]')
expect(page).to have_selector('input[value="MONTHS"]')
expect(page).to have_selector('input[value="WEEKS"]')
end
end
end
it 'renders the epics state dropdown' do
page.within('.content-wrapper .content .epics-filters') do
expect(page).to have_css('.dropdown-epics-state')
end
end
describe 'roadmap page with epics state filter' do
before do
state_dropdown.find('.dropdown-toggle').click
end
it 'renders open epics only' do
state_dropdown.find('button', text: 'Open epics').click
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 2)
end
end
it 'renders closed epics only' do
state_dropdown.find('button', text: 'Closed epics').click
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 1)
end
end
end
describe 'roadmap page with filter applied' do
before do
search_for_label(bug_label)
end
it 'keeps label filter when filtering by state' do
state_dropdown.find('.dropdown-toggle').click
state_dropdown.find('button', text: 'Open epics').click
page.within('.roadmap-container .epics-list-section') do
expect(page).to have_selector('.epics-list-item .epic-title', count: 1)
expect(page).to have_content(epic_with_bug.title)
end
end
end
end
context 'when epics exist for the group' do
available_tokens = %w[Author Label Milestone Epic My-Reaction]
available_sort_options = ['Start date', 'Due date']
......
......@@ -37,7 +37,7 @@ describe('RoadmapApp', () => {
initialDate: mockTimeframeInitialDate,
});
const createComponent = ({ roadmapSettings = false } = {}) => {
const createComponent = () => {
return shallowMountExtended(RoadmapApp, {
propsData: {
emptyStateIllustrationPath,
......@@ -46,7 +46,6 @@ describe('RoadmapApp', () => {
groupFullPath: 'gitlab-org',
groupMilestonesPath: '/groups/gitlab-org/-/milestones.json',
listEpicsPath: '/groups/gitlab-org/-/epics',
glFeatures: { roadmapSettings },
},
store,
});
......@@ -154,18 +153,8 @@ describe('RoadmapApp', () => {
});
});
it('does not render settings sidebar', () => {
expect(findSettingsSidebar().exists()).toBe(false);
});
describe('when roadmapSettings feature flag is on', () => {
beforeEach(() => {
wrapper = createComponent({ roadmapSettings: true });
});
it('renders settings button', () => {
expect(findSettingsSidebar().exists()).toBe(true);
});
it('renders settings sidebar', () => {
expect(findSettingsSidebar().exists()).toBe(true);
});
});
});
import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
......@@ -25,12 +24,10 @@ import {
import { TEST_HOST } from 'helpers/test_constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
jest.mock('~/lib/utils/url_utility', () => ({
mergeUrlParams: jest.fn(),
visitUrl: jest.fn(),
setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams,
updateHistory: jest.requireActual('~/lib/utils/url_utility').updateHistory,
}));
......@@ -42,7 +39,6 @@ const createComponent = ({
epicsState = EPICS_STATES.ALL,
sortedBy = mockSortedBy,
groupFullPath = 'gitlab-org',
listEpicsPath = '/groups/gitlab-org/-/epics',
groupMilestonesPath = '/groups/gitlab-org/-/milestones.json',
timeframe = getTimeframeForRangeType({
timeframeRangeType: DATE_RANGES.THREE_YEARS,
......@@ -50,8 +46,6 @@ const createComponent = ({
initialDate: mockTimeframeInitialDate,
}),
filterParams = {},
timeframeRangeType = DATE_RANGES.THREE_YEARS,
roadmapSettings = false,
} = {}) => {
const store = createStore();
......@@ -71,11 +65,6 @@ const createComponent = ({
provide: {
groupFullPath,
groupMilestonesPath,
listEpicsPath,
glFeatures: { roadmapSettings },
},
props: {
timeframeRangeType,
},
});
};
......@@ -92,24 +81,6 @@ describe('RoadmapFilters', () => {
wrapper.destroy();
});
describe('computed', () => {
describe('selectedEpicStateTitle', () => {
it.each`
returnValue | epicsState
${'All epics'} | ${EPICS_STATES.ALL}
${'Open epics'} | ${EPICS_STATES.OPENED}
${'Closed epics'} | ${EPICS_STATES.CLOSED}
`(
'returns string "$returnValue" when epicsState represents `$epicsState`',
({ returnValue, epicsState }) => {
wrapper.vm.$store.dispatch('setEpicsState', epicsState);
expect(wrapper.vm.selectedEpicStateTitle).toBe(returnValue);
},
);
});
});
describe('watch', () => {
describe('urlParams', () => {
it('updates window URL based on presence of props for state, filtered search and sort criteria', async () => {
......@@ -132,39 +103,18 @@ describe('RoadmapFilters', () => {
});
describe('template', () => {
const quarters = { text: 'Quarters', value: PRESET_TYPES.QUARTERS };
const months = { text: 'Months', value: PRESET_TYPES.MONTHS };
const weeks = { text: 'Weeks', value: PRESET_TYPES.WEEKS };
beforeEach(() => {
updateHistory({ url: TEST_HOST, title: document.title, replace: true });
});
it('switching layout using roadmap layout switching buttons causes page to reload with selected layout', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ selectedDaterange: DATE_RANGES.THREE_YEARS });
await nextTick();
wrapper.findComponent(GlSegmentedControl).vm.$emit('input', PRESET_TYPES.OPENED);
expect(mergeUrlParams).toHaveBeenCalledWith(
expect.objectContaining({ layout: PRESET_TYPES.OPENED }),
`${TEST_HOST}/`,
);
expect(visitUrl).toHaveBeenCalled();
it('renders settings button', () => {
expect(findSettingsButton().exists()).toBe(true);
});
it('renders epics state toggling dropdown', () => {
const epicsStateDropdown = wrapper.findComponent(GlDropdown);
expect(epicsStateDropdown.exists()).toBe(true);
expect(epicsStateDropdown.findAllComponents(GlDropdownItem)).toHaveLength(3);
});
it('emits toggleSettings event on click settings button', () => {
findSettingsButton().vm.$emit('click');
it('does not render settings button', () => {
expect(findSettingsButton().exists()).toBe(false);
expect(wrapper.emitted('toggleSettings')).toBeTruthy();
});
describe('FilteredSearchBar', () => {
......@@ -335,82 +285,5 @@ describe('RoadmapFilters', () => {
});
});
});
describe('daterange filtering', () => {
let wrapperWithDaterangeFilter;
const availableRanges = [
{ text: 'This quarter', value: DATE_RANGES.CURRENT_QUARTER },
{ text: 'This year', value: DATE_RANGES.CURRENT_YEAR },
{ text: 'Within 3 years', value: DATE_RANGES.THREE_YEARS },
];
beforeEach(async () => {
wrapperWithDaterangeFilter = createComponent({
timeframeRangeType: DATE_RANGES.CURRENT_QUARTER,
});
await nextTick();
});
afterEach(() => {
wrapperWithDaterangeFilter.destroy();
});
it('renders daterange dropdown', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapperWithDaterangeFilter.setData({ selectedDaterange: DATE_RANGES.CURRENT_QUARTER });
await nextTick();
const daterangeDropdown = wrapperWithDaterangeFilter.findByTestId('daterange-dropdown');
expect(daterangeDropdown.exists()).toBe(true);
expect(daterangeDropdown.props('text')).toBe('This quarter');
daterangeDropdown.findAllComponents(GlDropdownItem).wrappers.forEach((item, index) => {
expect(item.text()).toBe(availableRanges[index].text);
expect(item.attributes('value')).toBe(availableRanges[index].value);
});
});
it.each`
selectedDaterange | availablePresets
${DATE_RANGES.CURRENT_QUARTER} | ${[]}
${DATE_RANGES.CURRENT_YEAR} | ${[months, weeks]}
${DATE_RANGES.THREE_YEARS} | ${[quarters, months, weeks]}
`(
'renders $availablePresets.length items when selected daterange is "$selectedDaterange"',
async ({ selectedDaterange, availablePresets }) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapperWithDaterangeFilter.setData({ selectedDaterange });
await nextTick();
const layoutSwitches = wrapperWithDaterangeFilter.findComponent(GlSegmentedControl);
if (selectedDaterange === DATE_RANGES.CURRENT_QUARTER) {
expect(layoutSwitches.exists()).toBe(false);
} else {
expect(layoutSwitches.exists()).toBe(true);
expect(layoutSwitches.props('options')).toEqual(availablePresets);
}
},
);
});
});
describe('when roadmapSettings feature flag is on', () => {
beforeEach(() => {
wrapper = createComponent({ roadmapSettings: true });
});
it('renders settings button', () => {
expect(findSettingsButton().exists()).toBe(true);
});
it('emits toggleSettings event on click settings button', () => {
findSettingsButton().vm.$emit('click');
expect(wrapper.emitted('toggleSettings')).toBeTruthy();
});
});
});
......@@ -3519,9 +3519,6 @@ msgstr ""
msgid "All environments"
msgstr ""
msgid "All epics"
msgstr ""
msgid "All groups and projects"
msgstr ""
......@@ -7576,9 +7573,6 @@ msgstr ""
msgid "Closed MRs"
msgstr ""
msgid "Closed epics"
msgstr ""
msgid "Closed issues"
msgstr ""
......@@ -25608,9 +25602,6 @@ msgstr ""
msgid "Open a CLI and connect to the cluster you want to install the agent in. Use this installation method to minimize any manual steps. The token is already included in the command."
msgstr ""
msgid "Open epics"
msgstr ""
msgid "Open errors"
msgstr ""
......@@ -29885,9 +29876,6 @@ msgstr ""
msgid "QualitySummary|Project quality"
msgstr ""
msgid "Quarters"
msgstr ""
msgid "Query"
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