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';
import EpicBody from './epic_body.vue';
import EpicHeader from './epic_header.vue';
import EpicTabs from './epic_tabs.vue';
export default {
components: {
EpicHeader,
EpicBody,
EpicTabs,
},
mounted() {
this.sidebarContext = new SidebarContext();
......@@ -19,5 +21,6 @@ export default {
<div class="epic-page-container">
<epic-header />
<epic-body />
<epic-tabs />
</div>
</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 () => {
const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const epicData = parseIssuableData(el);
const { treeElementSelector, roadmapElementSelector, containerElementSelector } = el.dataset;
// Collapse the sidebar on mobile screens by default
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm' || bpBreakpoint === 'md') {
......@@ -50,6 +52,10 @@ export default () => {
isClassicSidebar: true,
allowScopedLabels: epicMeta.scopedLabels,
labelsManagePath: epicMeta.labelsWebUrl,
allowSubEpics: parseBoolean(el.dataset.allowSubEpics),
treeElementSelector,
roadmapElementSelector,
containerElementSelector,
},
created() {
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 initEpicApp from 'ee/epic/epic_bundle';
import EpicTabs from 'ee/epic/epic_tabs';
import loadAwardsHandler from '~/awards_handler';
import initNotesApp from '~/notes';
import ZenMode from '~/zen_mode';
......@@ -11,7 +10,6 @@ initEpicApp();
requestIdleCallback(() => {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
new EpicTabs(); // eslint-disable-line no-new
new ShortcutsEpic(); // eslint-disable-line no-new
if (awardEmojiEl) {
import('~/emoji/awards_app')
......
......@@ -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_children', { fullPath: @group.full_path, iid: @epic.iid, pageSize: 100, epicEndCursor: '', issueEndCursor: '' })
%div{ class: ['js-epic-container', container_class, @content_class] }
#epic-app-root{ data: epic_show_app_data(@epic),
'data-allow-sub-epics' => allow_sub_epics }
- containerClass = 'js-epic-container'
- treeElementID = 'tree'
- roadmapElementID = 'roadmap'
.epic-tabs-holder
.epic-tabs-container.js-epic-tabs-container{ data: { allow_sub_epics: allow_sub_epics } }
%ul.epic-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.tree-tab
%a#tree-tab.active{ href: '#tree', data: { toggle: 'tab' } }
- if sub_epics_feature_available
= _('Epics and Issues')
- else
= _('Issues')
- if sub_epics_feature_available
%li.roadmap-tab
%a#roadmap-tab{ href: '#roadmap', data: { toggle: 'tab' } }
= _('Roadmap')
%div{ class: [containerClass, container_class, @content_class] }
#epic-app-root{ data: epic_show_app_data(@epic),
'data-allow-sub-epics' => allow_sub_epics,
'data-tree-element-selector' => "##{treeElementID}",
'data-roadmap-element-selector' => "##{roadmapElementID}",
'data-container-element-selector' => ".#{containerClass}" }
.epic-tabs-content.js-epic-tabs-content
#tree.tab-pane.show.active
%div{ id: treeElementID, class: ['tab-pane', 'show', 'active'] }
.row
%section.col-md-12
#js-tree{ data: { id: @epic.to_global_id,
......@@ -61,7 +54,7 @@
allow_sub_epics: allow_sub_epics,
initial: issuable_initial_data(@epic).to_json } }
- if sub_epics_feature_available
#roadmap.tab-pane.gl-display-none
%div{ id: roadmapElementID, class: ['tab-pane', 'gl-display-none'] }
.row
%section.col-md-12
#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
wait_for_requests
find('.js-epic-tabs-container #tree-tab').click
find('.js-epic-tree-tab').click
wait_for_requests
end
......@@ -199,7 +199,7 @@ RSpec.describe 'Epic Issues', :js do
wait_for_requests
find('.js-epic-tabs-container #tree-tab').click
find('.js-epic-tree-tab').click
wait_for_requests
end
......
......@@ -52,10 +52,8 @@ RSpec.describe 'Epic show', :js do
describe 'Epic metadata' do
it 'shows epic tabs `Epics and Issues` and `Roadmap`' do
page.within('.js-epic-tabs-container') do
expect(find('.epic-tabs #tree-tab')).to have_content('Epics and Issues')
expect(find('.epic-tabs #roadmap-tab')).to have_content('Roadmap')
end
expect(find('.js-epic-tree-tab')).to have_content('Epics and Issues')
expect(find('.js-epic-roadmap-tab')).to have_content('Roadmap')
end
end
......@@ -93,7 +91,7 @@ RSpec.describe 'Epic show', :js do
describe 'Roadmap tab' do
before do
find('.js-epic-tabs-container #roadmap-tab').click
find('.js-epic-roadmap-tab').click
wait_for_requests
end
......@@ -119,7 +117,7 @@ RSpec.describe 'Epic show', :js do
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
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
page.within('.js-epic-tabs-content') do
......@@ -127,7 +125,7 @@ RSpec.describe 'Epic show', :js do
expect(page).to have_selector('#tree.tab-pane', visible: false)
end
find('.js-epic-tabs-container #tree-tab').click
find('.js-epic-tree-tab').click
page.within('.js-epic-tabs-content') do
expect(page).to have_selector('#tree.tab-pane', visible: true)
......@@ -143,9 +141,7 @@ RSpec.describe 'Epic show', :js do
describe 'Epic metadata' do
it 'shows epic tab `Issues`' do
page.within('.js-epic-tabs-container') do
expect(find('.epic-tabs #tree-tab')).to have_content('Issues')
end
expect(find('.js-epic-tree-tab')).to have_content('Issues')
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