Commit b505eb38 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '352329-related-items-tree' into 'master'

Move Roadmap App inside Epic tree

See merge request gitlab-org/gitlab!82795
parents 1cb1fc31 4b5230e3
<script> <script>
import initRelatedItemsTree from 'ee/related_items_tree/related_items_tree_bundle';
import SidebarContext from '../sidebar_context'; 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();
initRelatedItemsTree();
}, },
}; };
</script> </script>
...@@ -21,6 +22,5 @@ export default { ...@@ -21,6 +22,5 @@ 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 { GlButtonGroup, GlButton } 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: {
GlButton,
GlButtonGroup,
},
inject: {
allowSubEpics: {
default: false,
},
treeElementSelector: {
default: null,
},
roadmapElementSelector: {
default: null,
},
containerElementSelector: {
default: null,
},
},
data() {
return {
roadmapLoaded: false,
activeButton: this.$options.TABS.TREE,
};
},
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.activeButton = this.$options.TABS.TREE;
this.roadmapElement.classList.add(displayNoneClass);
this.treeElement.classList.remove(displayNoneClass);
this.containerElement.classList.add(containerClass);
},
showRoadmapTabContent() {
this.activeButton = this.$options.TABS.ROADMAP;
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();
}
},
},
TABS: {
TREE: 'related_items_tree',
ROADMAP: 'roadmap',
},
};
</script>
<template>
<div class="epic-tabs-holder gl-pl-0 gl-pr-0 gl-ml-0 gl-mr-0">
<div class="epic-tabs-container gl-pt-3 gl-pb-3">
<gl-button-group data-testid="tabs">
<gl-button
class="js-epic-tree-tab"
data-testid="epic-tree-tab"
:selected="activeButton === $options.TABS.TREE"
@click="onTreeTabClick"
>
{{ allowSubEpics ? __('Epics and Issues') : __('Issues') }}
</gl-button>
<gl-button
v-if="allowSubEpics"
class="js-epic-roadmap-tab"
data-testid="epic-roadmap-tab"
:selected="activeButton === $options.TABS.ROADMAP"
@click="onRoadmapTabClick"
>
{{ __('Roadmap') }}
</gl-button>
</gl-button-group>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlAlert,
},
inject: ['roadmapAppData'],
data() {
return {
loadingError: false,
roadmapLoaded: false,
};
},
computed: {
...mapState(['allowSubEpics']),
roadmapAttrs() {
if (!this.roadmapAppData) {
return {};
}
return Object.keys(this.roadmapAppData).reduce((acc, key) => {
const hypenCasedKey = key.replace(/_/g, '-');
acc[`data-${hypenCasedKey}`] = this.roadmapAppData[key];
return acc;
}, {});
},
shouldLoadRoadmap() {
return !this.roadmapLoaded && this.allowSubEpics;
},
},
mounted() {
if (this.shouldLoadRoadmap) {
this.initRoadmap();
}
},
methods: {
initRoadmap() {
return import('ee/roadmap/roadmap_bundle')
.then((roadmapBundle) => {
roadmapBundle.default();
this.roadmapLoaded = true;
})
.catch(() => {
this.loadingError = true;
});
},
},
loadingFailedText: __('Failed to load Roadmap'),
};
</script>
<template>
<div class="gl-px-3 gl-py-3 gl-bg-gray-10">
<gl-alert v-if="loadingError" variant="danger" :dismissible="false">
{{ $options.loadingFailedText }}
</gl-alert>
<div id="roadmap" class="roadmap-app border gl-rounded-base gl-bg-white">
<div id="js-roadmap" v-bind="roadmapAttrs"></div>
</div>
</div>
</template>
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { mapState } from 'vuex';
import { ITEM_TABS } from '../constants';
import ToggleLabels from '../../boards/components/toggle_labels.vue';
export default {
ITEM_TABS,
components: {
GlButtonGroup,
GlButton,
ToggleLabels,
},
props: {
activeTab: {
type: String,
required: true,
},
},
computed: {
...mapState(['allowSubEpics']),
},
};
</script>
<template>
<div class="card-header d-flex gl-px-5 gl-pt-4 gl-pt-3 flex-column flex-sm-row border-bottom-0">
<div>
<gl-button-group data-testid="buttons" class="gl-flex-grow-1 gl-display-flex">
<gl-button
class="js-epic-tree-tab"
data-testid="tree-view-button"
:selected="activeTab === $options.ITEM_TABS.TREE"
@click="() => $emit('tab-change', this.$options.ITEM_TABS.TREE)"
>
{{ __('Tree view') }}
</gl-button>
<gl-button
v-if="allowSubEpics"
class="js-epic-roadmap-tab"
data-testid="roadmap-view-button"
:selected="activeTab === $options.ITEM_TABS.ROADMAP"
@click="() => $emit('tab-change', this.$options.ITEM_TABS.ROADMAP)"
>
{{ __('Roadmap view') }}
</gl-button>
</gl-button-group>
</div>
<div class="ml-auto gl-display-none gl-sm-display-flex">
<!-- empty -->
</div>
<div
v-if="activeTab === $options.ITEM_TABS.TREE"
class="gl-sm-display-inline-flex gl-display-flex gl-mt-3 gl-sm-mt-0"
>
<toggle-labels class="gl-sm-ml-3! gl-ml-0!" />
</div>
</div>
</template>
...@@ -6,11 +6,13 @@ import { __, sprintf } from '~/locale'; ...@@ -6,11 +6,13 @@ import { __, sprintf } from '~/locale';
import AddItemForm from '~/related_issues/components/add_issuable_form.vue'; import AddItemForm from '~/related_issues/components/add_issuable_form.vue';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue'; import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
import { issuableTypesMap } from '~/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
import { OVERFLOW_AFTER } from '../constants'; import { ITEM_TABS, OVERFLOW_AFTER } from '../constants';
import CreateEpicForm from './create_epic_form.vue'; import CreateEpicForm from './create_epic_form.vue';
import CreateIssueForm from './create_issue_form.vue'; import CreateIssueForm from './create_issue_form.vue';
import RelatedItemsTreeBody from './related_items_tree_body.vue'; import RelatedItemsTreeBody from './related_items_tree_body.vue';
import RelatedItemsTreeHeader from './related_items_tree_header.vue'; import RelatedItemsTreeHeader from './related_items_tree_header.vue';
import RelatedItemsTreeActions from './related_items_tree_actions.vue';
import RelatedItemsRoadmapApp from './related_items_roadmap_app.vue';
import TreeItemRemoveModal from './tree_item_remove_modal.vue'; import TreeItemRemoveModal from './tree_item_remove_modal.vue';
const FORM_SLOTS = { const FORM_SLOTS = {
...@@ -22,11 +24,14 @@ const FORM_SLOTS = { ...@@ -22,11 +24,14 @@ const FORM_SLOTS = {
export default { export default {
OVERFLOW_AFTER, OVERFLOW_AFTER,
FORM_SLOTS, FORM_SLOTS,
ITEM_TABS,
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlIcon, GlIcon,
RelatedItemsTreeHeader, RelatedItemsTreeHeader,
RelatedItemsTreeBody, RelatedItemsTreeBody,
RelatedItemsTreeActions,
RelatedItemsRoadmapApp,
AddItemForm, AddItemForm,
CreateEpicForm, CreateEpicForm,
TreeItemRemoveModal, TreeItemRemoveModal,
...@@ -36,6 +41,11 @@ export default { ...@@ -36,6 +41,11 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
data() {
return {
activeTab: ITEM_TABS.TREE,
};
},
computed: { computed: {
...mapState([ ...mapState([
'parentItem', 'parentItem',
...@@ -157,24 +167,28 @@ export default { ...@@ -157,24 +167,28 @@ export default {
this.toggleCreateEpicForm({ toggleState: false }); this.toggleCreateEpicForm({ toggleState: false });
this.setItemInputValue(''); this.setItemInputValue('');
}, },
handleTabChange(value) {
this.activeTab = value;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="related-items-tree-container"> <div class="related-items-tree-container gl-mt-5">
<div v-if="itemsFetchInProgress" class="mt-2"> <div v-if="itemsFetchInProgress" class="mt-2">
<gl-loading-icon size="md" /> <gl-loading-icon size="md" />
</div> </div>
<div <div
v-else v-else
class="related-items-tree card card-slim border-top-0" class="related-items-tree card card-slim"
:class="{ :class="{
'disabled-content': disableContents, 'disabled-content': disableContents,
'overflow-auto': directChildren.length > $options.OVERFLOW_AFTER, 'overflow-auto': directChildren.length > $options.OVERFLOW_AFTER,
}" }"
> >
<related-items-tree-header :class="{ 'border-bottom-0': itemsFetchResultEmpty }" /> <related-items-tree-header :class="{ 'border-bottom-0': itemsFetchResultEmpty }" />
<slot-switch <slot-switch
v-if="visibleForm && parentItem.confidential" v-if="visibleForm && parentItem.confidential"
:active-slot-names="[visibleForm]" :active-slot-names="[visibleForm]"
...@@ -240,11 +254,19 @@ export default { ...@@ -240,11 +254,19 @@ export default {
/> />
</template> </template>
</slot-switch> </slot-switch>
<related-items-tree-body
<related-items-tree-actions
v-if="!itemsFetchResultEmpty" v-if="!itemsFetchResultEmpty"
:active-tab="activeTab"
@tab-change="handleTabChange"
/>
<related-items-tree-body
v-if="!itemsFetchResultEmpty && activeTab === $options.ITEM_TABS.TREE"
:parent-item="parentItem" :parent-item="parentItem"
:children="directChildren" :children="directChildren"
/> />
<related-items-roadmap-app v-if="activeTab === $options.ITEM_TABS.ROADMAP" />
<tree-item-remove-modal /> <tree-item-remove-modal />
</div> </div>
</div> </div>
......
...@@ -3,8 +3,6 @@ import { GlTooltip, GlIcon } from '@gitlab/ui'; ...@@ -3,8 +3,6 @@ import { GlTooltip, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { issuableTypesMap } from '~/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
import ToggleLabels from '../../boards/components/toggle_labels.vue';
import EpicHealthStatus from './epic_health_status.vue'; import EpicHealthStatus from './epic_health_status.vue';
import EpicActionsSplitButton from './epic_issue_actions_split_button.vue'; import EpicActionsSplitButton from './epic_issue_actions_split_button.vue';
...@@ -14,7 +12,6 @@ export default { ...@@ -14,7 +12,6 @@ export default {
GlIcon, GlIcon,
EpicHealthStatus, EpicHealthStatus,
EpicActionsSplitButton, EpicActionsSplitButton,
ToggleLabels,
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -73,72 +70,83 @@ export default { ...@@ -73,72 +70,83 @@ export default {
</script> </script>
<template> <template>
<div class="card-header d-flex px-2 flex-column flex-sm-row"> <div class="card-header d-flex gl-px-5 gl-py-3 flex-column flex-sm-row">
<div class="d-inline-flex flex-grow-1 lh-100 align-middle mb-2 mb-sm-0"> <div class="flex flex-grow-1 flex-shrink-0 gl-flex-wrap flex-column flex-sm-row">
<gl-tooltip :target="() => $refs.countBadge"> <div class="flex flex-shrink-0 align-items-center gl-flex-wrap">
<p v-if="allowSubEpics" class="font-weight-bold m-0"> <h3 class="card-title h5 gl-my-0 flex-shrink-0">
{{ __('Epics') }} &#8226; {{ allowSubEpics ? __('Child issues and epics') : __('Issues') }}
<span class="font-weight-normal" </h3>
>{{ <div class="d-inline-flex lh-100 align-middle gl-ml-5 gl-flex-wrap">
sprintf(__('%{openedEpics} open, %{closedEpics} closed'), { <gl-tooltip :target="() => $refs.countBadge">
openedEpics: descendantCounts.openedEpics, <p v-if="allowSubEpics" class="font-weight-bold m-0">
closedEpics: descendantCounts.closedEpics, {{ __('Epics') }} &#8226;
}) <span class="font-weight-normal"
}} >{{
</span> sprintf(__('%{openedEpics} open, %{closedEpics} closed'), {
</p> openedEpics: descendantCounts.openedEpics,
<p class="font-weight-bold m-0"> closedEpics: descendantCounts.closedEpics,
{{ __('Issues') }} &#8226; })
<span class="font-weight-normal" }}
>{{ </span>
sprintf(__('%{openedIssues} open, %{closedIssues} closed'), { </p>
openedIssues: descendantCounts.openedIssues, <p class="font-weight-bold m-0">
closedIssues: descendantCounts.closedIssues, {{ __('Issues') }} &#8226;
}) <span class="font-weight-normal"
}} >{{
</span> sprintf(__('%{openedIssues} open, %{closedIssues} closed'), {
</p> openedIssues: descendantCounts.openedIssues,
<p class="font-weight-bold m-0"> closedIssues: descendantCounts.closedIssues,
{{ __('Total weight') }} &#8226; })
<span class="font-weight-normal">{{ totalWeight }} </span> }}
</p> </span>
</gl-tooltip> </p>
<p class="font-weight-bold m-0">
{{ __('Total weight') }} &#8226;
<span class="font-weight-normal">{{ totalWeight }} </span>
</p>
</gl-tooltip>
<div
ref="countBadge"
class="issue-count-badge gl-display-inline-flex text-secondary p-0 pr-3"
>
<span v-if="allowSubEpics" class="d-inline-flex align-items-center">
<gl-icon name="epic" class="mr-1" />
{{ totalEpicsCount }}
</span>
<span class="d-inline-flex align-items-center" :class="{ 'gl-ml-3': allowSubEpics }">
<gl-icon name="issues" class="mr-1" />
{{ totalIssuesCount }}
</span>
<span class="d-inline-flex align-items-center" :class="{ 'gl-ml-3': allowSubEpics }">
<gl-icon name="weight" class="mr-1" />
{{ totalWeight }}
</span>
</div>
</div>
</div>
<div <div
ref="countBadge" class="gl-display-flex gl-sm-display-inline-flex lh-100 align-middle gl-sm-ml-2 gl-ml-0 gl-flex-wrap gl-mt-2 gl-sm-mt-0"
class="issue-count-badge gl-display-inline-flex text-secondary p-0 pr-3"
> >
<span v-if="allowSubEpics" class="d-inline-flex align-items-center"> <epic-health-status v-if="showHealthStatus" :health-status="healthStatus" />
<gl-icon name="epic" class="mr-1" />
{{ totalEpicsCount }}
</span>
<span class="d-inline-flex align-items-center" :class="{ 'ml-3': allowSubEpics }">
<gl-icon name="issues" class="mr-1" />
{{ totalIssuesCount }}
</span>
<span class="d-inline-flex align-items-center" :class="{ 'ml-3': allowSubEpics }">
<gl-icon name="weight" class="mr-1" />
{{ totalWeight }}
</span>
</div> </div>
<epic-health-status v-if="showHealthStatus" :health-status="healthStatus" />
</div>
<div class="gl-display-inline-flex gl-mr-3">
<toggle-labels />
</div> </div>
<div <div
v-if="parentItem.userPermissions.adminEpic" class="gl-display-flex gl-sm-display-inline-flex gl-sm-ml-auto lh-100 align-middle gl-mt-3 gl-sm-mt-0 gl-pl-0 gl-sm-pl-7"
class="d-inline-flex flex-column flex-sm-row js-button-container"
> >
<epic-actions-split-button <div
:allow-sub-epics="allowSubEpics" v-if="parentItem.userPermissions.adminEpic"
class="js-add-epics-issues-button qa-add-epics-button mb-2 mb-sm-0" class="gl-flex-grow-1 flex-column flex-sm-row js-button-container"
@showAddIssueForm="showAddIssueForm" >
@showCreateIssueForm="showCreateIssueForm" <epic-actions-split-button
@showAddEpicForm="showAddEpicForm" :allow-sub-epics="allowSubEpics"
@showCreateEpicForm="showCreateEpicForm" class="js-add-epics-issues-button qa-add-epics-button w-100"
/> @showAddIssueForm="showAddIssueForm"
@showCreateIssueForm="showCreateIssueForm"
@showAddEpicForm="showAddEpicForm"
@showCreateEpicForm="showCreateEpicForm"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
</gl-button> </gl-button>
<gl-loading-icon v-if="childrenFetchInProgress" class="loading-icon" size="sm" /> <gl-loading-icon v-if="childrenFetchInProgress" class="loading-icon" size="sm" />
<tree-item-body <tree-item-body
class="tree-item-row" class="tree-item-row gl-mb-3"
:parent-item="parentItem" :parent-item="parentItem"
:item="item" :item="item"
:class="{ :class="{
......
...@@ -313,7 +313,7 @@ export default { ...@@ -313,7 +313,7 @@ export default {
<item-assignees <item-assignees
v-if="hasAssignees" v-if="hasAssignees"
:assignees="item.assignees" :assignees="item.assignees"
class="item-assignees gl-display-inline-flex gl-align-items-center gl-mr-5 mb-md-0 flex-xl-grow-0" class="item-assignees gl-display-inline-flex gl-align-items-center gl-mr-5 gl-mb-3 flex-xl-grow-0"
/> />
<epic-health-status <epic-health-status
......
...@@ -87,7 +87,7 @@ export default { ...@@ -87,7 +87,7 @@ export default {
<component <component
:is="treeRootWrapper" :is="treeRootWrapper"
v-bind="treeRootOptions" v-bind="treeRootOptions"
class="list-unstyled related-items-list tree-root" class="list-unstyled related-items-list tree-root gl-px-3 gl-py-3"
:move="onMove" :move="onMove"
@start="handleDragOnStart" @start="handleDragOnStart"
@end="handleDragOnEnd" @end="handleDragOnEnd"
......
...@@ -60,3 +60,8 @@ export const issueHealthStatusCSSMapping = { ...@@ -60,3 +60,8 @@ export const issueHealthStatusCSSMapping = {
}; };
export const trackingAddedIssue = 'g_project_management_users_epic_issue_added_from_epic'; export const trackingAddedIssue = 'g_project_management_users_epic_issue_added_from_epic';
export const ITEM_TABS = {
TREE: 'tree',
ROADMAP: 'roadmap',
};
...@@ -32,6 +32,7 @@ export default () => { ...@@ -32,6 +32,7 @@ export default () => {
allowSubEpics, allowSubEpics,
} = el.dataset; } = el.dataset;
const initialData = JSON.parse(el.dataset.initial); const initialData = JSON.parse(el.dataset.initial);
const roadmapAppData = JSON.parse(el.dataset.roadmapAppData);
Vue.component('TreeRoot', TreeRoot); Vue.component('TreeRoot', TreeRoot);
Vue.component('TreeItem', TreeItem); Vue.component('TreeItem', TreeItem);
...@@ -41,6 +42,9 @@ export default () => { ...@@ -41,6 +42,9 @@ export default () => {
name: 'RelatedItemsTreeRoot', name: 'RelatedItemsTreeRoot',
store: createStore(), store: createStore(),
components: { RelatedItemsTreeApp }, components: { RelatedItemsTreeApp },
provide: {
roadmapAppData,
},
created() { created() {
this.setInitialParentItem({ this.setInitialParentItem({
fullPath, fullPath,
......
.related-items-tree { .related-items-tree {
border-top-left-radius: 0;
border-top-right-radius: 0;
.add-item-form-container { .add-item-form-container {
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
...@@ -65,6 +62,9 @@ ...@@ -65,6 +62,9 @@
} }
.related-items-tree-body { .related-items-tree-body {
border-bottom-left-radius: $gl-border-radius-base;
border-bottom-right-radius: $gl-border-radius-base;
> .tree-root { > .tree-root {
padding-top: $gl-vert-padding; padding-top: $gl-vert-padding;
padding-bottom: 0; padding-bottom: 0;
...@@ -81,3 +81,24 @@ ...@@ -81,3 +81,24 @@
margin-bottom: $gl-vert-padding; margin-bottom: $gl-vert-padding;
} }
} }
.related-items-tree-container {
.roadmap-app-container {
.js-roadmap-shell {
border-radius: $gl-border-radius-base;
}
.epics-list-item-empty {
display: none;
}
// This is a hacky CSS to remove the border-bottom from the
// last list in the roadmap.
.epic-item-container:nth-last-child(4) {
.epic-details-cell,
.epic-timeline-cell {
border-bottom: 0;
}
}
}
}
...@@ -37,42 +37,37 @@ ...@@ -37,42 +37,37 @@
'data-roadmap-element-selector' => "##{roadmapElementID}", 'data-roadmap-element-selector' => "##{roadmapElementID}",
'data-container-element-selector' => ".#{containerClass}" } 'data-container-element-selector' => ".#{containerClass}" }
.epic-tabs-content.js-epic-tabs-content
%div{ id: treeElementID, class: ['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,
numerical_id: @epic.id, numerical_id: @epic.id,
iid: @epic.iid, iid: @epic.iid,
group_name: @group.name, group_name: @group.name,
group_id: @group.id,
full_path: @group.full_path,
auto_complete_epics: allow_sub_epics,
auto_complete_issues: 'true',
user_signed_in: current_user.present? ? 'true' : 'false',
allow_issuable_health_status: allow_issuable_health_status,
allow_scoped_labels: allow_scoped_labels,
allow_sub_epics: allow_sub_epics,
initial: issuable_initial_data(@epic).to_json,
roadmap_app_data: sub_epics_feature_available ? { epics_path: group_epics_path(@group, parent_id: @epic.id, format: :json),
group_id: @group.id, group_id: @group.id,
iid: @epic.iid,
full_path: @group.full_path, full_path: @group.full_path,
auto_complete_epics: allow_sub_epics, empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
auto_complete_issues: 'true', has_filters_applied: false,
user_signed_in: current_user.present? ? 'true' : 'false', new_epic_path: new_group_epic_path(@group),
allow_issuable_health_status: allow_issuable_health_status, list_epics_path: group_epics_path(@group),
allow_scoped_labels: allow_scoped_labels, epics_docs_path: help_page_path('user/group/epics/index'),
allow_sub_epics: allow_sub_epics, preset_type: roadmap_layout,
initial: issuable_initial_data(@epic).to_json } } epics_state: 'all',
- if sub_epics_feature_available sorted_by: roadmap_sort_order,
%div{ id: roadmapElementID, class: ['tab-pane', 'gl-display-none'] } inner_height: '600',
.row child_epics: true }.to_json : 'null' } }
%section.col-md-12
#js-roadmap{ data: { epics_path: group_epics_path(@group, parent_id: @epic.id, format: :json),
group_id: @group.id,
iid: @epic.iid,
full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: 'false',
new_epic_path: new_group_epic_path(@group),
list_epics_path: group_epics_path(@group),
epics_docs_path: help_page_path('user/group/epics/index'),
preset_type: roadmap_layout,
epics_state: 'all',
sorted_by: roadmap_sort_order,
inner_height: '600',
child_epics: 'true' } }
- if related_epics_feature_available && Feature.enabled?(:related_epics_widget, @group, default_enabled: :yaml) - if related_epics_feature_available && Feature.enabled?(:related_epics_widget, @group, default_enabled: :yaml)
#js-related-epics{ data: { endpoint: group_epic_related_epic_links_path(@group, @epic), #js-related-epics{ data: { endpoint: group_epic_related_epic_links_path(@group, @epic),
can_add_related_epics: "#{can?(current_user, :admin_related_epic_link, @epic)}", can_add_related_epics: "#{can?(current_user, :admin_related_epic_link, @epic)}",
......
...@@ -198,10 +198,6 @@ RSpec.describe 'Epic Issues', :js do ...@@ -198,10 +198,6 @@ RSpec.describe 'Epic Issues', :js do
visit group_epic_path(group, last_child) visit group_epic_path(group, last_child)
wait_for_requests wait_for_requests
find('.js-epic-tree-tab').click
wait_for_requests
end end
it 'user cannot add new epic when hierarchy level limit has been reached' do it 'user cannot add new epic when hierarchy level limit has been reached' do
......
...@@ -18,14 +18,10 @@ RSpec.describe 'Related Epics', :js do ...@@ -18,14 +18,10 @@ RSpec.describe 'Related Epics', :js do
visit group_epic_path(group, epic1) visit group_epic_path(group, epic1)
wait_for_requests wait_for_requests
find('.js-epic-tree-tab').click
wait_for_requests
end end
def open_add_epic_form def open_add_epic_form
page.within('.js-epic-container .card-title') do page.within('.related-issues-block .card-title') do
page.find('button').click page.find('button').click
end end
end end
...@@ -50,9 +46,7 @@ RSpec.describe 'Related Epics', :js do ...@@ -50,9 +46,7 @@ RSpec.describe 'Related Epics', :js do
describe 'epic body section' do describe 'epic body section' do
it 'user can view related epics section under epic description', :aggregate_failures do it 'user can view related epics section under epic description', :aggregate_failures do
page.within('.js-epic-container') do page.within('#related-issues') do
expect(page).to have_selector('#related-issues')
card_title = page.find('.card-title') card_title = page.find('.card-title')
expect(card_title).to have_content('Linked epics') expect(card_title).to have_content('Linked epics')
expect(card_title).to have_link('', href: '/help/user/group/epics/linked_epics') expect(card_title).to have_link('', href: '/help/user/group/epics/linked_epics')
......
...@@ -38,7 +38,7 @@ RSpec.describe 'Epic show', :js do ...@@ -38,7 +38,7 @@ RSpec.describe 'Epic show', :js do
button_name = type == 'issue' ? 'Add an existing issue' : 'Add an existing epic' button_name = type == 'issue' ? 'Add an existing issue' : 'Add an existing epic'
input_character = type == 'issue' ? '#' : '&' input_character = type == 'issue' ? '#' : '&'
page.within('.js-epic-tabs-content #tree') do page.within('.related-items-tree-container') do
find('.js-add-epics-issues-button .dropdown-toggle').click find('.js-add-epics-issues-button .dropdown-toggle').click
click_button button_name click_button button_name
fill_in "Paste #{type} link", with: input_character fill_in "Paste #{type} link", with: input_character
...@@ -52,15 +52,15 @@ RSpec.describe 'Epic show', :js do ...@@ -52,15 +52,15 @@ RSpec.describe 'Epic show', :js do
end end
describe 'Epic metadata' do describe 'Epic metadata' do
it 'shows epic tabs `Epics and Issues` and `Roadmap`' do it 'shows buttons `Tree view` and `Roadmap view`' do
expect(find('.js-epic-tree-tab')).to have_content('Epics and Issues') expect(find('[data-testid="tree-view-button"]')).to have_content('Tree view')
expect(find('.js-epic-roadmap-tab')).to have_content('Roadmap') expect(find('[data-testid="roadmap-view-button"]')).to have_content('Roadmap view')
end end
end end
describe 'Epics and Issues tab' do describe 'Epics and Issues tab' do
it 'shows Related items tree with child epics' do it 'shows Related items tree with child epics' do
page.within('.js-epic-tabs-content #tree') do page.within('.js-epic-container') do
expect(page).to have_selector('.related-items-tree-container') expect(page).to have_selector('.related-items-tree-container')
page.within('.related-items-tree-container') do page.within('.related-items-tree-container') do
...@@ -92,12 +92,12 @@ RSpec.describe 'Epic show', :js do ...@@ -92,12 +92,12 @@ RSpec.describe 'Epic show', :js do
describe 'Roadmap tab' do describe 'Roadmap tab' do
before do before do
find('.js-epic-roadmap-tab').click find('[data-testid="roadmap-view-button"]').click
wait_for_requests wait_for_requests
end end
it 'shows Roadmap timeline with child epics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299298' do it 'shows Roadmap timeline with child epics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/299298' do
page.within('.js-epic-tabs-content #roadmap') do page.within('.related-items-tree-container #roadmap') do
expect(page).to have_selector('.roadmap-container .js-roadmap-shell') expect(page).to have_selector('.roadmap-container .js-roadmap-shell')
page.within('.js-roadmap-shell .epics-list-section') do page.within('.js-roadmap-shell .epics-list-section') do
...@@ -121,14 +121,14 @@ RSpec.describe 'Epic show', :js do ...@@ -121,14 +121,14 @@ RSpec.describe 'Epic show', :js do
find('.js-epic-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('.related-items-tree-container') do
expect(page).to have_selector('#roadmap.tab-pane', visible: true) expect(page).to have_selector('#roadmap.tab-pane', visible: true)
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-tree-tab').click find('.js-epic-tree-tab').click
page.within('.js-epic-tabs-content') do page.within('.related-items-tree-container') do
expect(page).to have_selector('#tree.tab-pane', visible: true) expect(page).to have_selector('#tree.tab-pane', visible: true)
expect(page).to have_selector('#roadmap.tab-pane', visible: false) expect(page).to have_selector('#roadmap.tab-pane', visible: false)
end end
...@@ -137,23 +137,22 @@ RSpec.describe 'Epic show', :js do ...@@ -137,23 +137,22 @@ RSpec.describe 'Epic show', :js do
describe 'when the sub-epics feature is not available' do describe 'when the sub-epics feature is not available' do
before do before do
stub_licensed_features(epics: true, subepics: false)
visit group_epic_path(group, epic) visit group_epic_path(group, epic)
end end
describe 'Epic metadata' do describe 'Epic metadata' do
it 'shows epic tab `Issues`' do it 'shows epic tab `Issues`' do
expect(find('.js-epic-tree-tab')).to have_content('Issues') page.within('.related-items-tree-container') do
expect(find('h3.card-title')).to have_content('Issues')
end
end end
end end
describe 'Issues tab' do describe 'Issues tab' do
it 'shows Related items tree with child epics' do it 'shows Related items tree with child epics' do
page.within('.js-epic-tabs-content #tree') do page.within('.related-items-tree-container') do
expect(page).to have_selector('.related-items-tree-container') expect(page.find('.issue-count-badge', text: '1')).to be_present
page.within('.related-items-tree-container') do
expect(page.find('.issue-count-badge', text: '1')).to be_present
end
end end
end end
end end
......
import { GlTab } from '@gitlab/ui';
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 = {} } = {}) => {
return shallowMountExtended(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');
expect(treeTab.props().selected).toBe(true);
});
it('displays the roadmap tab', () => {
wrapper = createComponent({ provide: { allowSubEpics: true } });
const treeTab = findEpicRoadmapTab();
expect(treeTab.exists()).toBe(true);
expect(treeTab.text()).toBe('Roadmap');
expect(treeTab.props().selected).toBe(false);
});
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 | tabTestId | fixture | examples
${treeTabpaneID} | ${'epic-tree-tab'} | ${treeTabFixture} | ${treeExamples}
${roadmapTabpaneID} | ${'epic-roadmap-tab'} | ${roadmapFixture} | ${roadmapExamples}
`('on $targetTab tab click', ({ tabTestId, fixture, examples }) => {
beforeEach(() => {
setFixtures(fixture);
wrapper = createComponent({ provide: { allowSubEpics: true } });
});
it.each(examples)('%s', async (description, tabPaneSelector, hasClassName, className) => {
const element = document.querySelector(tabPaneSelector);
expect(element.classList.contains(className)).toBe(hasClassName);
wrapper.findByTestId(tabTestId).vm.$emit('click');
await waitForPromises();
expect(element.classList.contains(className)).not.toBe(hasClassName);
});
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedItemsTree RelatedItemsRoadmapApp template renders html 1`] = `
<div
class="gl-px-3 gl-py-3 gl-bg-gray-10"
>
<!---->
<div
class="roadmap-app border gl-rounded-base gl-bg-white"
id="roadmap"
>
<div
data-child-epics="true"
data-empty-state-illustration=""
data-epics-docs-path="/help/user/group/epics/index"
data-epics-path="/groups/group1/-/epics.json?parent_id=1"
data-epics-state="all"
data-full-path="group1"
data-group-id="2"
data-iid="1"
data-inner-height="600"
data-list-epics-path="/groups/group1/-/epics"
data-new-epic-path="/groups/group1/-/epics/new"
data-preset-type="MONTHS"
data-sorted-by="start_date_asc"
id="js-roadmap"
/>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RelatedItemsTree RelatedItemsTreeActions template renders button group, tree view and roadmap view buttons 1`] = `
<div
class="card-header d-flex gl-px-5 gl-pt-4 gl-pt-3 flex-column flex-sm-row border-bottom-0"
>
<div>
<gl-button-group-stub
class="gl-flex-grow-1 gl-display-flex"
data-testid="buttons"
>
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-epic-tree-tab"
data-testid="tree-view-button"
icon=""
selected="true"
size="medium"
variant="default"
>
Tree view
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-epic-roadmap-tab"
data-testid="roadmap-view-button"
icon=""
size="medium"
variant="default"
>
Roadmap view
</gl-button-stub>
</gl-button-group-stub>
</div>
<div
class="ml-auto gl-display-none gl-sm-display-flex"
/>
<div
class="gl-sm-display-inline-flex gl-display-flex gl-mt-3 gl-sm-mt-0"
>
<toggle-labels-stub
class="gl-sm-ml-3! gl-ml-0!"
/>
</div>
</div>
`;
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createDefaultStore from 'ee/related_items_tree/store';
import RelatedItemsRoadmapApp from 'ee/related_items_tree/components/related_items_roadmap_app.vue';
import { mockInitialConfig, mockRoadmapAppData } from '../mock_data';
Vue.use(Vuex);
const createComponent = ({ initialConfig = {} } = {}) => {
const store = createDefaultStore();
store.dispatch('setInitialConfig', { ...mockInitialConfig, ...initialConfig });
return shallowMountExtended(RelatedItemsRoadmapApp, {
store,
provide: {
roadmapAppData: mockRoadmapAppData,
},
});
};
describe('RelatedItemsTree', () => {
describe('RelatedItemsRoadmapApp', () => {
describe('template', () => {
let wrapper = null;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders html', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders data-* attrs', () => {
const el = wrapper.find('#js-roadmap');
const normalizedData = Object.keys(mockRoadmapAppData).reduce((acc, key) => {
const hypenCasedKey = key.replace(/_/g, '-');
acc[`data-${hypenCasedKey}`] = mockRoadmapAppData[key];
return acc;
}, {});
Object.keys(normalizedData).forEach((key) => {
expect(el.attributes()[key]).toBe(normalizedData[key]);
});
});
});
describe('initRoadmap', () => {
let wrapper = null;
let initRoadmap = null;
beforeEach(() => {
initRoadmap = jest
.spyOn(RelatedItemsRoadmapApp.methods, 'initRoadmap')
.mockReturnValue(Promise.resolve());
});
afterEach(() => {
wrapper.destroy();
});
it('does not load roadmap', () => {
wrapper = createComponent({
initialConfig: {
allowSubEpics: false,
},
});
expect(initRoadmap).not.toHaveBeenCalled();
});
it('loads roadmap', () => {
wrapper = createComponent({});
expect(initRoadmap).toHaveBeenCalled();
});
});
});
});
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ToggleLabels from 'ee/boards/components/toggle_labels.vue';
import RelatedItemsTreeActions from 'ee/related_items_tree/components/related_items_tree_actions.vue';
import { ITEM_TABS } from 'ee/related_items_tree/constants';
import createDefaultStore from 'ee/related_items_tree/store';
import { mockInitialConfig } from '../mock_data';
Vue.use(Vuex);
const createComponent = ({ slots } = {}) => {
const store = createDefaultStore();
store.dispatch('setInitialConfig', mockInitialConfig);
return shallowMountExtended(RelatedItemsTreeActions, {
store,
slots,
propsData: {
activeTab: ITEM_TABS.TREE,
},
});
};
describe('RelatedItemsTree', () => {
describe('RelatedItemsTreeActions', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('renders button group, tree view and roadmap view buttons', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('does not render roadmap view button when subEpics are not present', async () => {
wrapper.vm.$store.dispatch('setInitialConfig', {
...mockInitialConfig,
allowSubEpics: false,
});
await nextTick();
const roadmapViewEl = wrapper.findByTestId('roadmap-view-button');
expect(roadmapViewEl.exists()).toBe(false);
});
describe('ToggleLabels', () => {
it('renders when view is tree', () => {
expect(wrapper.find(ToggleLabels).exists()).toBe(true);
});
it('does not render when view is roadmap', async () => {
await wrapper.setProps({ activeTab: ITEM_TABS.ROADMAP });
expect(wrapper.find(ToggleLabels).exists()).toBe(false);
});
});
});
describe('emit tab-change', () => {
beforeEach(() => {
wrapper = createComponent();
});
it.each`
viewName | testid | name
${'tree view'} | ${'tree-view-button'} | ${ITEM_TABS.TREE}
${'roadmap view'} | ${'roadmap-view-button'} | ${ITEM_TABS.ROADMAP}
`('emits tab-change event when $viewName button is clicked', ({ testid, name }) => {
const button = wrapper.findByTestId(testid);
button.vm.$emit('click');
expect(wrapper.emitted('tab-change')[0]).toEqual([name]);
});
});
});
});
...@@ -10,8 +10,13 @@ import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue'; ...@@ -10,8 +10,13 @@ import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue'; import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
import RelatedItemsTreeApp from 'ee/related_items_tree/components/related_items_tree_app.vue'; import RelatedItemsTreeApp from 'ee/related_items_tree/components/related_items_tree_app.vue';
import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue'; import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue';
import RelatedItemsTreeActions from 'ee/related_items_tree/components/related_items_tree_actions.vue';
import RelatedItemsTreeBody from 'ee/related_items_tree/components/related_items_tree_body.vue';
import RelatedItemsRoadmapApp from 'ee/related_items_tree/components/related_items_roadmap_app.vue';
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { ITEM_TABS } from 'ee/related_items_tree/constants';
import { issuableTypesMap } from '~/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
import { mockInitialConfig, mockParentItem, mockEpics, mockIssues } from '../mock_data'; import { mockInitialConfig, mockParentItem, mockEpics, mockIssues } from '../mock_data';
...@@ -270,5 +275,38 @@ describe('RelatedItemsTreeApp', () => { ...@@ -270,5 +275,38 @@ describe('RelatedItemsTreeApp', () => {
}); });
}, },
); );
it('switches tab to Roadmap', async () => {
wrapper.vm.$store.state.itemsFetchResultEmpty = false;
await nextTick();
wrapper.findComponent(RelatedItemsTreeActions).vm.$emit('tab-change', ITEM_TABS.ROADMAP);
await nextTick();
expect(wrapper.vm.activeTab).toBe(ITEM_TABS.ROADMAP);
});
it.each`
visibleApp | activeTab
${'Tree View'} | ${ITEM_TABS.TREE}
${'Roadmap View'} | ${ITEM_TABS.ROADMAP}
`('renders $visibleApp when activeTab is $activeTab', async ({ activeTab }) => {
wrapper.vm.$store.state.itemsFetchResultEmpty = false;
await nextTick();
wrapper.findComponent(RelatedItemsTreeActions).vm.$emit('tab-change', activeTab);
await nextTick();
const appMapping = {
[ITEM_TABS.TREE]: RelatedItemsTreeBody,
[ITEM_TABS.ROADMAP]: RelatedItemsRoadmapApp,
};
expect(wrapper.findComponent(appMapping[activeTab]).isVisible()).toBe(true);
});
}); });
}); });
...@@ -6,7 +6,6 @@ import Vuex from 'vuex'; ...@@ -6,7 +6,6 @@ import Vuex from 'vuex';
import EpicHealthStatus from 'ee/related_items_tree/components/epic_health_status.vue'; import EpicHealthStatus from 'ee/related_items_tree/components/epic_health_status.vue';
import EpicActionsSplitButton from 'ee/related_items_tree/components/epic_issue_actions_split_button.vue'; import EpicActionsSplitButton from 'ee/related_items_tree/components/epic_issue_actions_split_button.vue';
import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue'; import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue';
import ToggleLabels from 'ee/boards/components/toggle_labels.vue';
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
...@@ -59,12 +58,12 @@ describe('RelatedItemsTree', () => { ...@@ -59,12 +58,12 @@ describe('RelatedItemsTree', () => {
it('returns string containing epic count based on available direct children within state', () => { it('returns string containing epic count based on available direct children within state', () => {
expect(wrapper.findComponent(GlTooltip).text()).toContain(`Epics • expect(wrapper.findComponent(GlTooltip).text()).toContain(`Epics •
1 open, 1 closed`); 1 open, 1 closed`);
}); });
it('returns string containing issue count based on available direct children within state', () => { it('returns string containing issue count based on available direct children within state', () => {
expect(wrapper.findComponent(GlTooltip).text()).toContain(`Issues • expect(wrapper.findComponent(GlTooltip).text()).toContain(`Issues •
2 open, 1 closed`); 2 open, 1 closed`);
}); });
}); });
...@@ -78,16 +77,6 @@ describe('RelatedItemsTree', () => { ...@@ -78,16 +77,6 @@ describe('RelatedItemsTree', () => {
}); });
}); });
describe('toggleLabels', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('toggle labels component is visible', () => {
expect(wrapper.findComponent(ToggleLabels).isVisible()).toBe(true);
});
});
describe('epic issue actions split button', () => { describe('epic issue actions split button', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
......
...@@ -465,3 +465,19 @@ export const mockMixedFrequentlyUsedProjects = [ ...@@ -465,3 +465,19 @@ export const mockMixedFrequentlyUsedProjects = [
frequency: 3, frequency: 3,
}, },
]; ];
export const mockRoadmapAppData = {
epics_path: '/groups/group1/-/epics.json?parent_id=1',
group_id: '2',
iid: '1',
full_path: 'group1',
empty_state_illustration: '',
new_epic_path: '/groups/group1/-/epics/new',
list_epics_path: '/groups/group1/-/epics',
epics_docs_path: '/help/user/group/epics/index',
preset_type: 'MONTHS',
epics_state: 'all',
sorted_by: 'start_date_asc',
inner_height: '600',
child_epics: 'true',
};
...@@ -7402,6 +7402,9 @@ msgstr "" ...@@ -7402,6 +7402,9 @@ msgstr ""
msgid "Child epic doesn't exist." msgid "Child epic doesn't exist."
msgstr "" msgstr ""
msgid "Child issues and epics"
msgstr ""
msgid "Chinese language support using" msgid "Chinese language support using"
msgstr "" msgstr ""
...@@ -14419,9 +14422,6 @@ msgstr "" ...@@ -14419,9 +14422,6 @@ msgstr ""
msgid "Epics Roadmap" msgid "Epics Roadmap"
msgstr "" msgstr ""
msgid "Epics and Issues"
msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort" msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "" msgstr ""
...@@ -15413,6 +15413,9 @@ msgstr "" ...@@ -15413,6 +15413,9 @@ msgstr ""
msgid "Failed to load" msgid "Failed to load"
msgstr "" msgstr ""
msgid "Failed to load Roadmap"
msgstr ""
msgid "Failed to load assignees." msgid "Failed to load assignees."
msgstr "" msgstr ""
...@@ -32148,6 +32151,9 @@ msgstr "" ...@@ -32148,6 +32151,9 @@ msgstr ""
msgid "Roadmap settings" msgid "Roadmap settings"
msgstr "" msgstr ""
msgid "Roadmap view"
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