Commit 3d301090 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '345158-roadmap-add-settings-drawer' into 'master'

Introduce Roadmap settings sidebar

See merge request gitlab-org/gitlab!78626
parents ebc6128a 94e71a8e
---
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: false
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DATE_RANGES } from '../constants'; import { DATE_RANGES } from '../constants';
import EpicsListEmpty from './epics_list_empty.vue'; import EpicsListEmpty from './epics_list_empty.vue';
import RoadmapFilters from './roadmap_filters.vue'; import RoadmapFilters from './roadmap_filters.vue';
import RoadmapSettings from './roadmap_settings.vue';
import RoadmapShell from './roadmap_shell.vue'; import RoadmapShell from './roadmap_shell.vue';
export default { export default {
...@@ -12,8 +15,10 @@ export default { ...@@ -12,8 +15,10 @@ export default {
EpicsListEmpty, EpicsListEmpty,
GlLoadingIcon, GlLoadingIcon,
RoadmapFilters, RoadmapFilters,
RoadmapSettings,
RoadmapShell, RoadmapShell,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
timeframeRangeType: { timeframeRangeType: {
type: String, type: String,
...@@ -29,6 +34,11 @@ export default { ...@@ -29,6 +34,11 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
isSettingsSidebarOpen: false,
};
},
computed: { computed: {
...mapState([ ...mapState([
'currentGroupId', 'currentGroupId',
...@@ -66,6 +76,9 @@ export default { ...@@ -66,6 +76,9 @@ export default {
}, },
methods: { methods: {
...mapActions(['fetchEpics', 'fetchMilestones']), ...mapActions(['fetchEpics', 'fetchMilestones']),
toggleSettings() {
this.isSettingsSidebarOpen = !this.isSettingsSidebarOpen;
},
}, },
}; };
</script> </script>
...@@ -75,6 +88,7 @@ export default { ...@@ -75,6 +88,7 @@ export default {
<roadmap-filters <roadmap-filters
v-if="showFilteredSearchbar && !epicIid" v-if="showFilteredSearchbar && !epicIid"
:timeframe-range-type="timeframeRangeType" :timeframe-range-type="timeframeRangeType"
@toggleSettings="toggleSettings"
/> />
<div :class="{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container"> <div :class="{ 'overflow-reset': epicsFetchResultEmpty }" class="roadmap-container">
<gl-loading-icon v-if="epicsFetchInProgress" class="gl-mt-5" size="md" /> <gl-loading-icon v-if="epicsFetchInProgress" class="gl-mt-5" size="md" />
...@@ -96,6 +110,13 @@ export default { ...@@ -96,6 +110,13 @@ export default {
:timeframe="timeframe" :timeframe="timeframe"
:current-group-id="currentGroupId" :current-group-id="currentGroupId"
:has-filters-applied="hasFiltersApplied" :has-filters-applied="hasFiltersApplied"
:is-settings-sidebar-open="isSettingsSidebarOpen"
/>
<roadmap-settings
v-if="glFeatures.roadmapSettings"
:is-open="isSettingsSidebarOpen"
data-testid="roadmap-settings"
@toggleSettings="toggleSettings"
/> />
</div> </div>
</div> </div>
......
<script> <script>
import { import {
GlButton,
GlFormGroup, GlFormGroup,
GlSegmentedControl, GlSegmentedControl,
GlDropdown, GlDropdown,
...@@ -11,6 +12,7 @@ import { mapState, mapActions } from 'vuex'; ...@@ -11,6 +12,7 @@ import { mapState, mapActions } from 'vuex';
import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { visitUrl, mergeUrlParams, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
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 glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EPICS_STATES, PRESET_TYPES, DATE_RANGES } from '../constants'; import { EPICS_STATES, PRESET_TYPES, DATE_RANGES } from '../constants';
import EpicsFilteredSearchMixin from '../mixins/filtered_search_mixin'; import EpicsFilteredSearchMixin from '../mixins/filtered_search_mixin';
...@@ -48,6 +50,7 @@ export default { ...@@ -48,6 +50,7 @@ export default {
}, },
], ],
components: { components: {
GlButton,
GlFormGroup, GlFormGroup,
GlSegmentedControl, GlSegmentedControl,
GlDropdown, GlDropdown,
...@@ -55,7 +58,7 @@ export default { ...@@ -55,7 +58,7 @@ export default {
GlDropdownDivider, GlDropdownDivider,
FilteredSearchBar, FilteredSearchBar,
}, },
mixins: [EpicsFilteredSearchMixin], mixins: [EpicsFilteredSearchMixin, glFeatureFlagMixin()],
props: { props: {
timeframeRangeType: { timeframeRangeType: {
type: String, type: String,
...@@ -161,6 +164,9 @@ export default { ...@@ -161,6 +164,9 @@ export default {
this.fetchEpics(); this.fetchEpics();
}, },
}, },
i18n: {
settings: __('Settings'),
},
}; };
</script> </script>
...@@ -232,6 +238,16 @@ export default { ...@@ -232,6 +238,16 @@ export default {
@onFilter="handleFilterEpics" @onFilter="handleFilterEpics"
@onSort="handleSortEpics" @onSort="handleSortEpics"
/> />
<gl-button
v-if="glFeatures.roadmapSettings"
icon="settings"
class="gl-mb-3 gl-lg-ml-3 gl-sm-mt-3"
:aria-label="$options.i18n.settings"
data-testid="settings-button"
@click="$emit('toggleSettings', $event)"
>
{{ $options.i18n.settings }}
</gl-button>
</div> </div>
</div> </div>
</template> </template>
<script>
import { GlDrawer } from '@gitlab/ui';
export default {
components: {
GlDrawer,
},
props: {
isOpen: {
type: Boolean,
required: true,
},
},
methods: {
getDrawerHeaderHeight() {
const wrapperEl = document.querySelector('.roadmap-container');
if (wrapperEl) {
return `${wrapperEl.offsetTop}px`;
}
return '';
},
},
};
</script>
<template>
<gl-drawer
v-bind="$attrs"
:open="isOpen"
:header-height="getDrawerHeaderHeight()"
@close="$emit('toggleSettings', $event)"
>
<template #title>
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Roadmap settings') }}</h2>
</template>
</gl-drawer>
</template>
...@@ -135,7 +135,7 @@ html.group-epics-roadmap-html { ...@@ -135,7 +135,7 @@ html.group-epics-roadmap-html {
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: 0; top: 0;
z-index: 20; z-index: 9;
.timeline-header-blank, .timeline-header-blank,
.timeline-header-item { .timeline-header-item {
......
...@@ -8,6 +8,10 @@ module Groups ...@@ -8,6 +8,10 @@ module Groups
before_action :check_epics_available! before_action :check_epics_available!
before_action :persist_roadmap_layout, only: [:show] 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 feature_category :portfolio_management
# show roadmap for a group # show roadmap for a group
......
...@@ -119,6 +119,19 @@ RSpec.describe 'group epic roadmap', :js do ...@@ -119,6 +119,19 @@ RSpec.describe 'group epic roadmap', :js do
expect(page).to have_selector('.epics-list-item .epic-title', count: 3) expect(page).to have_selector('.epics-list-item .epic-title', count: 3)
end end
end end
it 'toggles settings sidebar on click settings button' do
page.within('.content-wrapper .content') do
expect(page).not_to have_selector('[data-testid="roadmap-sidebar"]')
expect(page).to have_selector('[data-testid="settings-button"]')
click_button 'Settings'
expect(page).to have_selector('[data-testid="roadmap-settings"]')
click_button 'Settings'
expect(page).not_to have_selector('[data-testid="roadmap-settings"]')
end
end
end end
describe 'roadmap page with epics state filter' do describe 'roadmap page with epics state filter' do
......
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EpicsListEmpty from 'ee/roadmap/components/epics_list_empty.vue'; import EpicsListEmpty from 'ee/roadmap/components/epics_list_empty.vue';
import RoadmapApp from 'ee/roadmap/components/roadmap_app.vue'; import RoadmapApp from 'ee/roadmap/components/roadmap_app.vue';
import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue'; import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue';
...@@ -18,13 +19,12 @@ import { ...@@ -18,13 +19,12 @@ import {
mockTimeframeInitialDate, mockTimeframeInitialDate,
} from 'ee_jest/roadmap/mock_data'; } from 'ee_jest/roadmap/mock_data';
Vue.use(Vuex);
describe('RoadmapApp', () => { describe('RoadmapApp', () => {
const localVue = createLocalVue();
let store; let store;
let wrapper; let wrapper;
localVue.use(Vuex);
const currentGroupId = mockGroupId; const currentGroupId = mockGroupId;
const emptyStateIllustrationPath = mockSvgPath; const emptyStateIllustrationPath = mockSvgPath;
const epics = [mockFormattedEpic]; const epics = [mockFormattedEpic];
...@@ -36,9 +36,8 @@ describe('RoadmapApp', () => { ...@@ -36,9 +36,8 @@ describe('RoadmapApp', () => {
initialDate: mockTimeframeInitialDate, initialDate: mockTimeframeInitialDate,
}); });
const createComponent = (mountFunction = shallowMount) => { const createComponent = ({ roadmapSettings = false } = {}) => {
return mountFunction(RoadmapApp, { return shallowMountExtended(RoadmapApp, {
localVue,
propsData: { propsData: {
emptyStateIllustrationPath, emptyStateIllustrationPath,
presetType, presetType,
...@@ -47,11 +46,14 @@ describe('RoadmapApp', () => { ...@@ -47,11 +46,14 @@ describe('RoadmapApp', () => {
groupFullPath: 'gitlab-org', groupFullPath: 'gitlab-org',
groupMilestonesPath: '/groups/gitlab-org/-/milestones.json', groupMilestonesPath: '/groups/gitlab-org/-/milestones.json',
listEpicsPath: '/groups/gitlab-org/-/epics', listEpicsPath: '/groups/gitlab-org/-/epics',
glFeatures: { roadmapSettings },
}, },
store, store,
}); });
}; };
const findSettingsSidebar = () => wrapper.findByTestId('roadmap-settings');
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
store.dispatch('setInitialData', { store.dispatch('setInitialData', {
...@@ -67,7 +69,6 @@ describe('RoadmapApp', () => { ...@@ -67,7 +69,6 @@ describe('RoadmapApp', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe.each` describe.each`
...@@ -152,5 +153,19 @@ describe('RoadmapApp', () => { ...@@ -152,5 +153,19 @@ describe('RoadmapApp', () => {
milestones: [], milestones: [],
}); });
}); });
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);
});
});
}); });
}); });
import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue'; import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue';
...@@ -29,6 +29,8 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -29,6 +29,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
updateHistory: jest.requireActual('~/lib/utils/url_utility').updateHistory, updateHistory: jest.requireActual('~/lib/utils/url_utility').updateHistory,
})); }));
Vue.use(Vuex);
const createComponent = ({ const createComponent = ({
presetType = PRESET_TYPES.MONTHS, presetType = PRESET_TYPES.MONTHS,
epicsState = EPICS_STATES.ALL, epicsState = EPICS_STATES.ALL,
...@@ -43,12 +45,10 @@ const createComponent = ({ ...@@ -43,12 +45,10 @@ const createComponent = ({
}), }),
filterParams = {}, filterParams = {},
timeframeRangeType = DATE_RANGES.THREE_YEARS, timeframeRangeType = DATE_RANGES.THREE_YEARS,
roadmapSettings = false,
} = {}) => { } = {}) => {
const localVue = createLocalVue();
const store = createStore(); const store = createStore();
localVue.use(Vuex);
store.dispatch('setInitialData', { store.dispatch('setInitialData', {
presetType, presetType,
epicsState, epicsState,
...@@ -58,12 +58,12 @@ const createComponent = ({ ...@@ -58,12 +58,12 @@ const createComponent = ({
}); });
return shallowMountExtended(RoadmapFilters, { return shallowMountExtended(RoadmapFilters, {
localVue,
store, store,
provide: { provide: {
groupFullPath, groupFullPath,
groupMilestonesPath, groupMilestonesPath,
listEpicsPath, listEpicsPath,
glFeatures: { roadmapSettings },
}, },
props: { props: {
timeframeRangeType, timeframeRangeType,
...@@ -73,6 +73,7 @@ const createComponent = ({ ...@@ -73,6 +73,7 @@ const createComponent = ({
describe('RoadmapFilters', () => { describe('RoadmapFilters', () => {
let wrapper; let wrapper;
const findSettingsButton = () => wrapper.findByTestId('settings-button');
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
...@@ -153,6 +154,10 @@ describe('RoadmapFilters', () => { ...@@ -153,6 +154,10 @@ describe('RoadmapFilters', () => {
expect(epicsStateDropdown.findAll(GlDropdownItem)).toHaveLength(3); expect(epicsStateDropdown.findAll(GlDropdownItem)).toHaveLength(3);
}); });
it('does not render settings button', () => {
expect(findSettingsButton().exists()).toBe(false);
});
describe('FilteredSearchBar', () => { describe('FilteredSearchBar', () => {
const mockInitialFilterValue = [ const mockInitialFilterValue = [
{ {
...@@ -383,4 +388,20 @@ describe('RoadmapFilters', () => { ...@@ -383,4 +388,20 @@ describe('RoadmapFilters', () => {
); );
}); });
}); });
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();
});
});
}); });
import { GlDrawer } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RoadmapSettings from 'ee/roadmap/components/roadmap_settings.vue';
describe('RoadmapSettings', () => {
let wrapper;
const createComponent = ({ isOpen = false } = {}) => {
wrapper = shallowMountExtended(RoadmapSettings, {
propsData: { isOpen },
});
};
const findSettingsDrawer = () => wrapper.findComponent(GlDrawer);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('render drawer and title', () => {
expect(findSettingsDrawer().exists()).toBe(true);
expect(findSettingsDrawer().text()).toContain('Roadmap settings');
});
});
});
...@@ -30633,6 +30633,9 @@ msgstr "" ...@@ -30633,6 +30633,9 @@ msgstr ""
msgid "Roadmap" msgid "Roadmap"
msgstr "" msgstr ""
msgid "Roadmap settings"
msgstr ""
msgid "Role" msgid "Role"
msgstr "" 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