Commit 186fe257 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Mark Florian

Migrate Epics tabs to Vue

This commit migrates the epics tabs
from HAML / JS to Vue.

Changelog: fixed
EE: true
parent da120cce
...@@ -3,11 +3,13 @@ import SidebarContext from '../sidebar_context'; ...@@ -3,11 +3,13 @@ import SidebarContext from '../sidebar_context';
import EpicBody from './epic_body.vue'; import EpicBody from './epic_body.vue';
import EpicHeader from './epic_header.vue'; import EpicHeader from './epic_header.vue';
import EpicTabs from './epic_tabs.vue';
export default { export default {
components: { components: {
EpicHeader, EpicHeader,
EpicBody, EpicBody,
EpicTabs,
}, },
mounted() { mounted() {
this.sidebarContext = new SidebarContext(); this.sidebarContext = new SidebarContext();
...@@ -19,5 +21,6 @@ export default { ...@@ -19,5 +21,6 @@ export default {
<div class="epic-page-container"> <div class="epic-page-container">
<epic-header /> <epic-header />
<epic-body /> <epic-body />
<epic-tabs />
</div> </div>
</template> </template>
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import initRelatedItemsTree from 'ee/related_items_tree/related_items_tree_bundle';
const displayNoneClass = 'gl-display-none';
const containerClass = 'container-limited';
export default {
components: {
GlTabs,
GlTab,
},
inject: {
allowSubEpics: {
default: false,
},
treeElementSelector: {
default: null,
},
roadmapElementSelector: {
default: null,
},
containerElementSelector: {
default: null,
},
},
data() {
return {
roadmapLoaded: false,
};
},
computed: {
shouldLoadRoadmap() {
return !this.roadmapLoaded && this.allowSubEpics;
},
},
mounted() {
initRelatedItemsTree();
},
beforeMount() {
this.treeElement = document.querySelector(this.treeElementSelector);
this.roadmapElement = document.querySelector(this.roadmapElementSelector);
this.containerElement = document.querySelector(this.containerElementSelector);
},
methods: {
initRoadmap() {
return import('ee/roadmap/roadmap_bundle')
.then((roadmapBundle) => {
roadmapBundle.default();
})
.catch(() => {});
},
onTreeTabClick() {
this.roadmapElement.classList.add(displayNoneClass);
this.treeElement.classList.remove(displayNoneClass);
this.containerElement.classList.add(containerClass);
},
showRoadmapTabContent() {
this.roadmapElement.classList.remove(displayNoneClass);
this.treeElement.classList.add(displayNoneClass);
this.containerElement.classList.remove(containerClass);
},
onRoadmapTabClick() {
if (this.shouldLoadRoadmap) {
this.initRoadmap()
.then(() => {
this.roadmapLoaded = true;
this.showRoadmapTabContent();
})
.catch(() => {});
} else {
this.showRoadmapTabContent();
}
},
},
};
</script>
<template>
<gl-tabs
content-class="gl-display-none"
nav-wrapper-class="epic-tabs-container"
nav-class="gl-border-bottom-0"
class="epic-tabs-holder"
data-testid="tabs"
>
<gl-tab title-link-class="js-epic-tree-tab" data-testid="epic-tree-tab" @click="onTreeTabClick">
<template #title>{{ allowSubEpics ? __('Epics and Issues') : __('Issues') }}</template>
</gl-tab>
<gl-tab
v-if="allowSubEpics"
title-link-class="js-epic-roadmap-tab"
data-testid="epic-roadmap-tab"
@click="onRoadmapTabClick"
>
<template #title>{{ __('Roadmap') }}</template>
</gl-tab>
</gl-tabs>
</template>
...@@ -30,6 +30,8 @@ export default () => { ...@@ -30,6 +30,8 @@ export default () => {
const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true }); const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const epicData = parseIssuableData(el); const epicData = parseIssuableData(el);
const { treeElementSelector, roadmapElementSelector, containerElementSelector } = el.dataset;
// Collapse the sidebar on mobile screens by default // Collapse the sidebar on mobile screens by default
const bpBreakpoint = bp.getBreakpointSize(); const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm' || bpBreakpoint === 'md') { if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm' || bpBreakpoint === 'md') {
...@@ -50,6 +52,10 @@ export default () => { ...@@ -50,6 +52,10 @@ export default () => {
isClassicSidebar: true, isClassicSidebar: true,
allowScopedLabels: epicMeta.scopedLabels, allowScopedLabels: epicMeta.scopedLabels,
labelsManagePath: epicMeta.labelsWebUrl, labelsManagePath: epicMeta.labelsWebUrl,
allowSubEpics: parseBoolean(el.dataset.allowSubEpics),
treeElementSelector,
roadmapElementSelector,
containerElementSelector,
}, },
created() { created() {
this.setEpicMeta({ this.setEpicMeta({
......
import $ from 'jquery';
import initRelatedItemsTree from 'ee/related_items_tree/related_items_tree_bundle';
import { parseBoolean } from '~/lib/utils/common_utils';
export default class EpicTabs {
constructor() {
this.wrapper = document.querySelector('.js-epic-container');
this.epicTabs = this.wrapper.querySelector('.js-epic-tabs-container');
this.treeTabPane = document.querySelector('#tree.tab-pane');
this.roadmapTabPane = document.querySelector('#roadmap.tab-pane');
this.discussionFilterContainer = this.epicTabs.querySelector('.js-discussion-filter-container');
const allowSubEpics = parseBoolean(this.epicTabs.dataset.allowSubEpics);
initRelatedItemsTree();
// We need to execute Roadmap tab related
// logic only when sub-epics feature is available.
if (allowSubEpics) {
this.roadmapTabLoaded = false;
this.loadRoadmapBundle();
this.bindEvents();
}
}
/**
* This method loads Roadmap app bundle asynchronously.
*
* @param {boolean} allowSubEpics
*/
loadRoadmapBundle() {
import('ee/roadmap/roadmap_bundle')
.then((roadmapBundle) => {
this.initRoadmap = roadmapBundle.default;
})
.catch(() => {});
}
bindEvents() {
const $roadmapTab = $('#roadmap-tab', this.epicTabs);
$roadmapTab.on('show.bs.tab', this.onRoadmapShow.bind(this));
$roadmapTab.on('hide.bs.tab', this.onRoadmapHide.bind(this));
}
onRoadmapShow() {
this.wrapper.classList.remove('container-limited');
if (!this.roadmapTabLoaded) {
this.initRoadmap();
this.roadmapTabLoaded = true;
}
this.roadmapTabPane.classList.remove('gl-display-none', 'show');
this.treeTabPane.classList.add('gl-display-none', 'show');
}
onRoadmapHide() {
this.wrapper.classList.add('container-limited');
this.roadmapTabPane.classList.add('gl-display-none', 'show');
this.treeTabPane.classList.remove('gl-display-none', 'show');
}
}
import ShortcutsEpic from 'ee/behaviors/shortcuts/shortcuts_epic'; import ShortcutsEpic from 'ee/behaviors/shortcuts/shortcuts_epic';
import initEpicApp from 'ee/epic/epic_bundle'; import initEpicApp from 'ee/epic/epic_bundle';
import EpicTabs from 'ee/epic/epic_tabs';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import initNotesApp from '~/notes'; import initNotesApp from '~/notes';
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
...@@ -11,7 +10,6 @@ initEpicApp(); ...@@ -11,7 +10,6 @@ initEpicApp();
requestIdleCallback(() => { requestIdleCallback(() => {
const awardEmojiEl = document.getElementById('js-vue-awards-block'); const awardEmojiEl = document.getElementById('js-vue-awards-block');
new EpicTabs(); // eslint-disable-line no-new
new ShortcutsEpic(); // eslint-disable-line no-new new ShortcutsEpic(); // eslint-disable-line no-new
if (awardEmojiEl) { if (awardEmojiEl) {
import('~/emoji/awards_app') import('~/emoji/awards_app')
......
...@@ -25,26 +25,19 @@ ...@@ -25,26 +25,19 @@
- add_page_startup_graphql_call('epic/epic_details', { fullPath: @group.full_path, iid: @epic.iid }) - add_page_startup_graphql_call('epic/epic_details', { fullPath: @group.full_path, iid: @epic.iid })
- add_page_startup_graphql_call('epic/epic_children', { fullPath: @group.full_path, iid: @epic.iid, pageSize: 100, epicEndCursor: '', issueEndCursor: '' }) - add_page_startup_graphql_call('epic/epic_children', { fullPath: @group.full_path, iid: @epic.iid, pageSize: 100, epicEndCursor: '', issueEndCursor: '' })
%div{ class: ['js-epic-container', container_class, @content_class] } - containerClass = 'js-epic-container'
#epic-app-root{ data: epic_show_app_data(@epic), - treeElementID = 'tree'
'data-allow-sub-epics' => allow_sub_epics } - roadmapElementID = 'roadmap'
.epic-tabs-holder %div{ class: [containerClass, container_class, @content_class] }
.epic-tabs-container.js-epic-tabs-container{ data: { allow_sub_epics: allow_sub_epics } } #epic-app-root{ data: epic_show_app_data(@epic),
%ul.epic-tabs.nav-tabs.nav.nav-links.scrolling-tabs 'data-allow-sub-epics' => allow_sub_epics,
%li.tree-tab 'data-tree-element-selector' => "##{treeElementID}",
%a#tree-tab.active{ href: '#tree', data: { toggle: 'tab' } } 'data-roadmap-element-selector' => "##{roadmapElementID}",
- if sub_epics_feature_available 'data-container-element-selector' => ".#{containerClass}" }
= _('Epics and Issues')
- else
= _('Issues')
- if sub_epics_feature_available
%li.roadmap-tab
%a#roadmap-tab{ href: '#roadmap', data: { toggle: 'tab' } }
= _('Roadmap')
.epic-tabs-content.js-epic-tabs-content .epic-tabs-content.js-epic-tabs-content
#tree.tab-pane.show.active %div{ id: treeElementID, class: ['tab-pane', 'show', 'active'] }
.row .row
%section.col-md-12 %section.col-md-12
#js-tree{ data: { id: @epic.to_global_id, #js-tree{ data: { id: @epic.to_global_id,
...@@ -61,7 +54,7 @@ ...@@ -61,7 +54,7 @@
allow_sub_epics: allow_sub_epics, allow_sub_epics: allow_sub_epics,
initial: issuable_initial_data(@epic).to_json } } initial: issuable_initial_data(@epic).to_json } }
- if sub_epics_feature_available - if sub_epics_feature_available
#roadmap.tab-pane.gl-display-none %div{ id: roadmapElementID, class: ['tab-pane', 'gl-display-none'] }
.row .row
%section.col-md-12 %section.col-md-12
#js-roadmap{ data: { epics_path: group_epics_path(@group, parent_id: @epic.id, format: :json), #js-roadmap{ data: { epics_path: group_epics_path(@group, parent_id: @epic.id, format: :json),
......
...@@ -38,7 +38,7 @@ RSpec.describe 'Epic Issues', :js do ...@@ -38,7 +38,7 @@ RSpec.describe 'Epic Issues', :js do
wait_for_requests wait_for_requests
find('.js-epic-tabs-container #tree-tab').click find('.js-epic-tree-tab').click
wait_for_requests wait_for_requests
end end
...@@ -199,7 +199,7 @@ RSpec.describe 'Epic Issues', :js do ...@@ -199,7 +199,7 @@ RSpec.describe 'Epic Issues', :js do
wait_for_requests wait_for_requests
find('.js-epic-tabs-container #tree-tab').click find('.js-epic-tree-tab').click
wait_for_requests wait_for_requests
end end
......
...@@ -52,10 +52,8 @@ RSpec.describe 'Epic show', :js do ...@@ -52,10 +52,8 @@ RSpec.describe 'Epic show', :js do
describe 'Epic metadata' do describe 'Epic metadata' do
it 'shows epic tabs `Epics and Issues` and `Roadmap`' do it 'shows epic tabs `Epics and Issues` and `Roadmap`' do
page.within('.js-epic-tabs-container') do expect(find('.js-epic-tree-tab')).to have_content('Epics and Issues')
expect(find('.epic-tabs #tree-tab')).to have_content('Epics and Issues') expect(find('.js-epic-roadmap-tab')).to have_content('Roadmap')
expect(find('.epic-tabs #roadmap-tab')).to have_content('Roadmap')
end
end end
end end
...@@ -93,7 +91,7 @@ RSpec.describe 'Epic show', :js do ...@@ -93,7 +91,7 @@ RSpec.describe 'Epic show', :js do
describe 'Roadmap tab' do describe 'Roadmap tab' do
before do before do
find('.js-epic-tabs-container #roadmap-tab').click find('.js-epic-roadmap-tab').click
wait_for_requests wait_for_requests
end end
...@@ -119,7 +117,7 @@ RSpec.describe 'Epic show', :js do ...@@ -119,7 +117,7 @@ RSpec.describe 'Epic show', :js do
end end
it 'switches between Epics and Issues tab and Roadmap tab when clicking on tab links', :aggregate_failures, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/342232' do it 'switches between Epics and Issues tab and Roadmap tab when clicking on tab links', :aggregate_failures, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/342232' do
find('.js-epic-tabs-container #roadmap-tab').click find('.js-epic-roadmap-tab').click
wait_for_all_requests # Wait for Roadmap bundle load and then Epics fetch load wait_for_all_requests # Wait for Roadmap bundle load and then Epics fetch load
page.within('.js-epic-tabs-content') do page.within('.js-epic-tabs-content') do
...@@ -127,7 +125,7 @@ RSpec.describe 'Epic show', :js do ...@@ -127,7 +125,7 @@ RSpec.describe 'Epic show', :js do
expect(page).to have_selector('#tree.tab-pane', visible: false) expect(page).to have_selector('#tree.tab-pane', visible: false)
end end
find('.js-epic-tabs-container #tree-tab').click find('.js-epic-tree-tab').click
page.within('.js-epic-tabs-content') do page.within('.js-epic-tabs-content') do
expect(page).to have_selector('#tree.tab-pane', visible: true) expect(page).to have_selector('#tree.tab-pane', visible: true)
...@@ -143,9 +141,7 @@ RSpec.describe 'Epic show', :js do ...@@ -143,9 +141,7 @@ RSpec.describe 'Epic show', :js do
describe 'Epic metadata' do describe 'Epic metadata' do
it 'shows epic tab `Issues`' do it 'shows epic tab `Issues`' do
page.within('.js-epic-tabs-container') do expect(find('.js-epic-tree-tab')).to have_content('Issues')
expect(find('.epic-tabs #tree-tab')).to have_content('Issues')
end
end end
end end
......
import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EpicTabs from 'ee/epic/components/epic_tabs.vue';
import waitForPromises from 'helpers/wait_for_promises';
const treeTabpaneID = 'tree';
const roadmapTabpaneID = 'roadmap';
const containerSelector = 'js-epic-container';
const displayNoneClass = 'gl-display-none';
const containerClass = 'container-limited';
describe('EpicTabs', () => {
let wrapper;
const createComponent = ({ provide = {}, mountType = shallowMountExtended } = {}) => {
return mountType(EpicTabs, {
provide: {
treeElementSelector: `#${treeTabpaneID}`,
roadmapElementSelector: `#${roadmapTabpaneID}`,
containerElementSelector: `.${containerSelector}`,
...provide,
},
stubs: {
GlTab,
},
});
};
const findEpicTreeTab = () => wrapper.findByTestId('epic-tree-tab');
const findEpicRoadmapTab = () => wrapper.findByTestId('epic-roadmap-tab');
afterEach(() => {
wrapper.destroy();
});
describe('default bahviour', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('displays the tabs component', () => {
expect(wrapper.findByTestId('tabs').exists()).toBe(true);
});
it('displays the tree tab', () => {
const treeTab = findEpicTreeTab();
expect(treeTab.exists()).toBe(true);
expect(treeTab.text()).toBe('Issues');
});
it('does not display the roadmap tab', () => {
expect(findEpicRoadmapTab().exists()).toBe(false);
});
});
describe('allowSubEpics = true', () => {
it('displays the correct tree tab text', () => {
wrapper = createComponent({ provide: { allowSubEpics: true } });
const treeTab = findEpicTreeTab();
expect(treeTab.exists()).toBe(true);
expect(treeTab.text()).toBe('Epics and Issues');
});
it('displays the roadmap tab', () => {
wrapper = createComponent({ provide: { allowSubEpics: true } });
const treeTab = findEpicRoadmapTab();
expect(treeTab.exists()).toBe(true);
expect(treeTab.text()).toBe('Roadmap');
});
const treeTabFixture = `
<div class="${containerSelector}">
<div id="${treeTabpaneID}" class="${displayNoneClass}"></div>
<div id="${roadmapTabpaneID}"></div>
</div>
`;
const roadmapFixture = `
<div class="${containerSelector} ${containerClass}">
<div id="${treeTabpaneID}"></div>
<div id="${roadmapTabpaneID}" class="${displayNoneClass}"></div>
</div>
`;
const treeExamples = [
['hides the roadmap tab content', `#${roadmapTabpaneID}`, false, displayNoneClass],
['displays the tree tab content', `#${treeTabpaneID}`, true, displayNoneClass],
['sets the container to limtied width', `.${containerSelector}`, false, containerClass],
];
const roadmapExamples = [
['hides the tree tab content', `#${treeTabpaneID}`, false, displayNoneClass],
['displays the roadmap tab content', `#${roadmapTabpaneID}`, true, displayNoneClass],
['removes the container width', `.${containerSelector}`, true, containerClass],
];
describe.each`
targetTab | tabSelector | fixture | examples
${treeTabpaneID} | ${'.js-epic-tree-tab'} | ${treeTabFixture} | ${treeExamples}
${roadmapTabpaneID} | ${'.js-epic-roadmap-tab'} | ${roadmapFixture} | ${roadmapExamples}
`('on $targetTab tab click', ({ tabSelector, fixture, examples }) => {
beforeEach(() => {
setFixtures(fixture);
wrapper = createComponent({ provide: { allowSubEpics: true }, mountType: mount });
});
it.each(examples)('%s', async (description, tabPaneSelector, hasClassName, className) => {
const element = document.querySelector(tabPaneSelector);
expect(element.classList.contains(className)).toBe(hasClassName);
wrapper.find(tabSelector).trigger('click');
await waitForPromises();
expect(element.classList.contains(className)).not.toBe(hasClassName);
});
});
});
});
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