Commit 7e0d2613 authored by Coung Ngo's avatar Coung Ngo

Make reviewer changes to expand epics in roadmap feature

Made changes as a result of MR reviewer comments
parent 27062213
...@@ -17,7 +17,7 @@ When you hover over an epic bar, a popover appears with its title, start and due ...@@ -17,7 +17,7 @@ When you hover over an epic bar, a popover appears with its title, start and due
completed. completed.
You can expand epics that contain child epics to show their child epics in the roadmap. You can expand epics that contain child epics to show their child epics in the roadmap.
You can click the chevron **{angle-down}** next to the epic title to expand and collapse the child epics. You can click the chevron **{chevron-down}** next to the epic title to expand and collapse the child epics.
![roadmap view](img/roadmap_view_v12_9.png) ![roadmap view](img/roadmap_view_v12_9.png)
......
<script> <script>
import { GlIcon, GlTooltip } from '@gitlab/ui'; import { GlIcon, GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale'; import { __, n__ } from '~/locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
...@@ -30,14 +30,19 @@ export default { ...@@ -30,14 +30,19 @@ export default {
return this.epic.isChildEpic || !this.epic?.children?.edges?.length; return this.epic.isChildEpic || !this.epic?.children?.edges?.length;
}, },
expandIconName() { expandIconName() {
return this.epic.isChildEpicShowing ? 'angle-down' : 'angle-right'; return this.epic.isChildEpicShowing ? 'chevron-down' : 'chevron-right';
}, },
expandIconLabel() { expandIconLabel() {
return this.epic.isChildEpicShowing ? __('Collapse') : __('Expand'); return this.epic.isChildEpicShowing ? __('Collapse child epics') : __('Expand child epics');
}, },
childEpicsCount() { childEpicsCount() {
return this.epic.isChildEpic ? '-' : this.epic?.children?.edges?.length || 0; return this.epic.isChildEpic ? '-' : this.epic?.children?.edges?.length || 0;
}, },
childEpicsCountText() {
return Number.isInteger(this.childEpicsCount)
? n__(`%d child epic`, `%d child epics`, this.childEpicsCount)
: '';
},
}, },
methods: { methods: {
toggleIsEpicExpanded() { toggleIsEpicExpanded() {
...@@ -48,41 +53,37 @@ export default { ...@@ -48,41 +53,37 @@ export default {
</script> </script>
<template> <template>
<div class="epic-details-cell d-flex p-2" data-qa-selector="epic_details_cell"> <div class="epic-details-cell d-flex align-items-start p-2" data-qa-selector="epic_details_cell">
<div <button
:class="{ invisible: isExpandIconHidden }" :class="{ invisible: isExpandIconHidden }"
class="epic-details-cell-expand-icon cursor-pointer" class="btn-link"
tabindex="0" :aria-label="expandIconLabel"
@click="toggleIsEpicExpanded" @click="toggleIsEpicExpanded"
@keydown.enter="toggleIsEpicExpanded"
> >
<gl-icon <gl-icon :name="expandIconName" class="text-secondary" aria-hidden="true" />
:name="expandIconName" </button>
class="text-secondary width"
:aria-label="expandIconLabel"
:size="12"
/>
</div>
<div class="overflow-hidden flex-grow-1" :class="[epic.isChildEpic ? 'ml-4 mr-2' : 'mx-2']"> <div class="overflow-hidden flex-grow-1" :class="[epic.isChildEpic ? 'ml-4 mr-2' : 'mx-2']">
<a :href="epic.webUrl" :title="epic.title" class="epic-title d-block text-body bold"> <a :href="epic.webUrl" :title="epic.title" class="epic-title d-block text-body bold">
{{ epic.title }} {{ epic.title }}
</a> </a>
<div class="epic-group-timeframe text-secondary"> <div class="epic-group-timeframe d-flex text-secondary">
<span v-if="isEpicGroupDifferent" :title="epic.groupFullName" class="epic-group"> <p v-if="isEpicGroupDifferent" :title="epic.groupFullName" class="epic-group">
{{ epic.groupName }} &middot; {{ epic.groupName }}
</span> </p>
<span class="epic-timeframe" :title="timeframeString">{{ timeframeString }}</span> <span class="mx-1" aria-hidden="true">&middot;</span>
<p class="epic-timeframe" :title="timeframeString">{{ timeframeString }}</p>
</div> </div>
</div> </div>
<div <div
ref="childEpicsCount" ref="childEpicsCount"
:class="['text-secondary', 'text-nowrap', { invisible: epic.isChildEpic }]" :class="{ invisible: epic.isChildEpic }"
class="d-flex text-secondary text-nowrap"
> >
<gl-icon name="epic" class="align-text-bottom" /> <gl-icon name="epic" class="align-text-bottom mr-1" aria-hidden="true" />
{{ childEpicsCount }} <p class="m-0" :aria-label="childEpicsCountText">{{ childEpicsCount }}</p>
</div> </div>
<gl-tooltip v-if="!epic.isChildEpic" :target="() => $refs.childEpicsCount"> <gl-tooltip v-if="!epic.isChildEpic" :target="() => $refs.childEpicsCount">
{{ n__(`%d child epic`, `%d child epics`, childEpicsCount) }} {{ childEpicsCountText }}
</gl-tooltip> </gl-tooltip>
</div> </div>
</template> </template>
...@@ -142,6 +142,11 @@ export default { ...@@ -142,6 +142,11 @@ export default {
) )
: 0; : 0;
}, },
epicWeightPercentageText() {
return sprintf(__(`%{percentage}%% weight completed`), {
percentage: this.epicWeightPercentage,
});
},
popoverWeightText() { popoverWeightText() {
if (this.epic.descendantWeightSum) { if (this.epic.descendantWeightSum) {
return sprintf(__('%{completedWeight} of %{totalWeight} weight completed'), { return sprintf(__('%{completedWeight} of %{totalWeight} weight completed'), {
...@@ -170,19 +175,18 @@ export default { ...@@ -170,19 +175,18 @@ export default {
:class="['epic-bar', 'rounded', { 'epic-bar-child-epic': epic.isChildEpic }]" :class="['epic-bar', 'rounded', { 'epic-bar-child-epic': epic.isChildEpic }]"
> >
<div class="epic-bar-inner px-2 py-1" :style="timelineBarInnerStyle"> <div class="epic-bar-inner px-2 py-1" :style="timelineBarInnerStyle">
<p class="epic-bar-title text-nowrap text-truncate mb-0"> <p class="epic-bar-title text-nowrap text-truncate m-0">{{ timelineBarTitle }}</p>
{{ timelineBarTitle }}
</p>
<div v-if="!isTimelineBarSmall" class="d-flex align-items-center"> <div v-if="!isTimelineBarSmall" class="d-flex align-items-center">
<gl-progress-bar <gl-progress-bar
class="epic-bar-progress flex-grow-1 mr-1" class="epic-bar-progress flex-grow-1 mr-1"
:value="epicWeightPercentage" :value="epicWeightPercentage"
aria-hidden="true"
/> />
<span class="gl-font-size-small d-flex align-items-center text-nowrap"> <div class="gl-font-size-small d-flex align-items-center text-nowrap">
<icon class="append-right-2" :size="12" name="weight" /> <icon class="append-right-2" :size="12" name="weight" />
{{ epicWeightPercentage }}% <p class="m-0" :aria-label="epicWeightPercentageText">{{ epicWeightPercentage }}%</p>
</span> </div>
</div> </div>
</div> </div>
</a> </a>
......
...@@ -59,10 +59,9 @@ export const fetchGroupEpics = ( ...@@ -59,10 +59,9 @@ export const fetchGroupEpics = (
variables, variables,
}) })
.then(({ data }) => { .then(({ data }) => {
const { group } = data;
const edges = epicIid const edges = epicIid
? (group.epic && group.epic.children.edges) || [] ? data?.group?.epic?.children?.edges || []
: (group.epics && group.epics.edges) || []; : data?.group?.epics?.edges || [];
return epicUtils.extractGroupEpics(edges); return epicUtils.extractGroupEpics(edges);
}); });
...@@ -89,9 +88,9 @@ export const receiveEpicsSuccess = ( ...@@ -89,9 +88,9 @@ export const receiveEpicsSuccess = (
formattedEpic.children.edges = formattedEpic.children.edges formattedEpic.children.edges = formattedEpic.children.edges
.map(epicUtils.flattenGroupProperty) .map(epicUtils.flattenGroupProperty)
.map(epicUtils.addIsChildEpicTrueProperty) .map(epicUtils.addIsChildEpicTrueProperty)
.map(e => .map(childEpic =>
roadmapItemUtils.formatRoadmapItemDetails( roadmapItemUtils.formatRoadmapItemDetails(
e, childEpic,
getters.timeframeStartDate, getters.timeframeStartDate,
getters.timeframeEndDate, getters.timeframeEndDate,
), ),
...@@ -214,9 +213,9 @@ export const refreshEpicDates = ({ commit, state, getters }) => { ...@@ -214,9 +213,9 @@ export const refreshEpicDates = ({ commit, state, getters }) => {
const epics = state.epics.map(epic => { const epics = state.epics.map(epic => {
// Update child epic dates too // Update child epic dates too
if (epic?.children?.edges?.length > 0) { if (epic?.children?.edges?.length > 0) {
epic.children.edges.map(e => epic.children.edges.map(childEpic =>
roadmapItemUtils.processRoadmapItemDates( roadmapItemUtils.processRoadmapItemDates(
e, childEpic,
getters.timeframeStartDate, getters.timeframeStartDate,
getters.timeframeEndDate, getters.timeframeEndDate,
), ),
......
...@@ -7,7 +7,7 @@ export const gqClient = createGqClient( ...@@ -7,7 +7,7 @@ export const gqClient = createGqClient(
}, },
); );
export const flattenGroupProperty = ({ node, epicNode = node }) => ({ export const flattenGroupProperty = ({ node: epicNode }) => ({
...epicNode, ...epicNode,
// We can get rid of below two lines // We can get rid of below two lines
// by updating `epic_item_details.vue` // by updating `epic_item_details.vue`
......
...@@ -305,19 +305,11 @@ html.group-epics-roadmap-html { ...@@ -305,19 +305,11 @@ html.group-epics-roadmap-html {
} }
} }
.epic-details-cell-expand-icon {
height: 20px;
}
.epic-title, .epic-title,
.epic-group-timeframe { .epic-group-timeframe {
@include text-truncate; @include text-truncate;
} }
.epic-group:hover {
cursor: pointer;
}
.epic-timeline-cell { .epic-timeline-cell {
position: relative; position: relative;
width: $timeline-cell-width; width: $timeline-cell-width;
......
...@@ -27,7 +27,7 @@ const getTitle = wrapper => wrapper.find('.epic-title'); ...@@ -27,7 +27,7 @@ const getTitle = wrapper => wrapper.find('.epic-title');
const getGroupName = wrapper => wrapper.find('.epic-group'); const getGroupName = wrapper => wrapper.find('.epic-group');
const getExpandIconDiv = wrapper => wrapper.find('.epic-details-cell-expand-icon'); const getExpandIconDiv = wrapper => wrapper.find('.btn-link');
const getChildEpicsCount = wrapper => wrapper.find({ ref: 'childEpicsCount' }); const getChildEpicsCount = wrapper => wrapper.find({ ref: 'childEpicsCount' });
...@@ -36,6 +36,7 @@ describe('EpicItemDetails', () => { ...@@ -36,6 +36,7 @@ describe('EpicItemDetails', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('epic title', () => { describe('epic title', () => {
...@@ -107,13 +108,13 @@ describe('EpicItemDetails', () => { ...@@ -107,13 +108,13 @@ describe('EpicItemDetails', () => {
describe('epic', () => { describe('epic', () => {
describe('expand icon', () => { describe('expand icon', () => {
it('is hidden when epic has no sub-epics', () => { it('is hidden when epic has no child epics', () => {
wrapper = createComponent(); wrapper = createComponent();
expect(getExpandIconDiv(wrapper).classes()).toContain('invisible'); expect(getExpandIconDiv(wrapper).classes()).toContain('invisible');
}); });
it('is shown when epic has sub-epics', () => { it('is shown when epic has child epics', () => {
const epic = { const epic = {
...mockFormattedEpic, ...mockFormattedEpic,
children: { children: {
...@@ -125,36 +126,36 @@ describe('EpicItemDetails', () => { ...@@ -125,36 +126,36 @@ describe('EpicItemDetails', () => {
expect(getExpandIconDiv(wrapper).classes()).not.toContain('invisible'); expect(getExpandIconDiv(wrapper).classes()).not.toContain('invisible');
}); });
it('shows "angle-right" icon when sub-epics are not expanded', () => { it('shows "chevron-right" icon when child epics are not expanded', () => {
wrapper = createComponent(); wrapper = createComponent();
expect(wrapper.find(GlIcon).attributes('name')).toBe('angle-right'); expect(wrapper.find(GlIcon).attributes('name')).toBe('chevron-right');
}); });
it('shows "angle-down" icon when sub-epics are expanded', () => { it('shows "chevron-down" icon when child epics are expanded', () => {
const epic = { const epic = {
...mockFormattedEpic, ...mockFormattedEpic,
isChildEpicShowing: true, isChildEpicShowing: true,
}; };
wrapper = createComponent(epic); wrapper = createComponent(epic);
expect(wrapper.find(GlIcon).attributes('name')).toBe('angle-down'); expect(wrapper.find(GlIcon).attributes('name')).toBe('chevron-down');
}); });
it('has "Expand" label when sub-epics are not expanded', () => { it('has "Expand child epics" label when child epics are not expanded', () => {
wrapper = createComponent(); wrapper = createComponent();
expect(wrapper.find(GlIcon).attributes('aria-label')).toBe('Expand'); expect(getExpandIconDiv(wrapper).attributes('aria-label')).toBe('Expand child epics');
}); });
it('has "Collapse" label when sub-epics are expanded', () => { it('has "Collapse child epics" label when child epics are expanded', () => {
const epic = { const epic = {
...mockFormattedEpic, ...mockFormattedEpic,
isChildEpicShowing: true, isChildEpicShowing: true,
}; };
wrapper = createComponent(epic); wrapper = createComponent(epic);
expect(wrapper.find(GlIcon).attributes('aria-label')).toBe('Collapse'); expect(getExpandIconDiv(wrapper).attributes('aria-label')).toBe('Collapse child epics');
}); });
it('emits toggleIsEpicExpanded event when clicked', () => { it('emits toggleIsEpicExpanded event when clicked', () => {
...@@ -175,7 +176,7 @@ describe('EpicItemDetails', () => { ...@@ -175,7 +176,7 @@ describe('EpicItemDetails', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('toggleIsEpicExpanded', id); expect(eventHub.$emit).toHaveBeenCalledWith('toggleIsEpicExpanded', id);
}); });
it('is hidden when it is sub-epic', () => { it('is hidden when it is child epic', () => {
const epic = { const epic = {
...mockFormattedEpic, ...mockFormattedEpic,
isChildEpic: true, isChildEpic: true,
...@@ -186,8 +187,8 @@ describe('EpicItemDetails', () => { ...@@ -186,8 +187,8 @@ describe('EpicItemDetails', () => {
}); });
}); });
describe('sub-epics count', () => { describe('child epics count', () => {
it('shows the correct count of sub-epics', () => { it('shows the correct count of child epics', () => {
const epic = { const epic = {
...mockFormattedEpic, ...mockFormattedEpic,
children: { children: {
...@@ -199,7 +200,7 @@ describe('EpicItemDetails', () => { ...@@ -199,7 +200,7 @@ describe('EpicItemDetails', () => {
expect(getChildEpicsCount(wrapper).text()).toBe('2'); expect(getChildEpicsCount(wrapper).text()).toBe('2');
}); });
it('shows the count as 0 when there are no sub-epics', () => { it('shows the count as 0 when there are no child epics', () => {
wrapper = createComponent(); wrapper = createComponent();
expect(getChildEpicsCount(wrapper).text()).toBe('0'); expect(getChildEpicsCount(wrapper).text()).toBe('0');
...@@ -217,7 +218,7 @@ describe('EpicItemDetails', () => { ...@@ -217,7 +218,7 @@ describe('EpicItemDetails', () => {
expect(wrapper.find(GlTooltip).text()).toBe('1 child epic'); expect(wrapper.find(GlTooltip).text()).toBe('1 child epic');
}); });
it('is hidden when it is a sub-epic', () => { it('is hidden when it is a child epic', () => {
const epic = { const epic = {
...mockFormattedEpic, ...mockFormattedEpic,
isChildEpic: true, isChildEpic: true,
......
...@@ -33,30 +33,27 @@ describe('EpicItemTimelineComponent', () => { ...@@ -33,30 +33,27 @@ describe('EpicItemTimelineComponent', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('epic bar', () => { describe('epic bar', () => {
it('shows the title', () => { beforeEach(() => {
wrapper = createComponent(); wrapper = createComponent();
});
it('shows the title', () => {
expect(getEpicBar(wrapper).text()).toContain(mockFormattedEpic.title); expect(getEpicBar(wrapper).text()).toContain(mockFormattedEpic.title);
}); });
it('shows the progress bar with correct value', () => { it('shows the progress bar with correct value', () => {
wrapper = createComponent();
expect(wrapper.find(GlProgressBar).attributes('value')).toBe('60'); expect(wrapper.find(GlProgressBar).attributes('value')).toBe('60');
}); });
it('shows the percentage', () => { it('shows the percentage', () => {
wrapper = createComponent();
expect(getEpicBar(wrapper).text()).toContain('60%'); expect(getEpicBar(wrapper).text()).toContain('60%');
}); });
it('contains a link to the epic', () => { it('contains a link to the epic', () => {
wrapper = createComponent();
expect(getEpicBar(wrapper).attributes('href')).toBe(mockFormattedEpic.webUrl); expect(getEpicBar(wrapper).attributes('href')).toBe(mockFormattedEpic.webUrl);
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import VirtualList from 'vue-virtual-scroll-list'; import VirtualList from 'vue-virtual-scroll-list';
import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue'; import epicsListSectionComponent from 'ee/roadmap/components/epics_list_section.vue';
import EpicItem from 'ee/roadmap/components/epic_item.vue'; import EpicItem from 'ee/roadmap/components/epic_item.vue';
...@@ -78,6 +77,7 @@ describe('EpicsListSectionComponent', () => { ...@@ -78,6 +77,7 @@ describe('EpicsListSectionComponent', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('data', () => { describe('data', () => {
...@@ -265,7 +265,7 @@ describe('EpicsListSectionComponent', () => { ...@@ -265,7 +265,7 @@ describe('EpicsListSectionComponent', () => {
}); });
}); });
it('expands to show sub-epics when epic is toggled', done => { it('expands to show child epics when epic is toggled', done => {
const epic = mockEpics[0]; const epic = mockEpics[0];
wrapper = createComponent(); wrapper = createComponent();
...@@ -273,7 +273,8 @@ describe('EpicsListSectionComponent', () => { ...@@ -273,7 +273,8 @@ describe('EpicsListSectionComponent', () => {
wrapper.vm.toggleIsEpicExpanded(epic.id); wrapper.vm.toggleIsEpicExpanded(epic.id);
Vue.nextTick() wrapper.vm
.$nextTick()
.then(() => { .then(() => {
const expected = mockEpics.length + epic.children.edges.length; const expected = mockEpics.length + epic.children.edges.length;
......
...@@ -19,15 +19,26 @@ describe('extractGroupEpics', () => { ...@@ -19,15 +19,26 @@ describe('extractGroupEpics', () => {
}); });
describe('addIsChildEpicTrueProperty', () => { describe('addIsChildEpicTrueProperty', () => {
it('adds `isChildEpic` property with value `true`', () => { const title = 'Lorem ipsum dolar sit';
const description = 'Beatae suscipit dolorum nihil quidem est accusamus';
const obj = { const obj = {
title: 'Lorem ipsum dolar sit', title,
description,
}; };
let newObj;
const newObj = epicUtils.addIsChildEpicTrueProperty(obj); beforeEach(() => {
newObj = epicUtils.addIsChildEpicTrueProperty(obj);
});
it('adds `isChildEpic` property with value `true`', () => {
expect(newObj.isChildEpic).toBe(true); expect(newObj.isChildEpic).toBe(true);
}); });
it('has original properties in returned object', () => {
expect(newObj.title).toBe(title);
expect(newObj.description).toBe(description);
});
}); });
describe('generateKey', () => { describe('generateKey', () => {
......
...@@ -395,6 +395,9 @@ msgstr "" ...@@ -395,6 +395,9 @@ msgstr ""
msgid "%{openedIssues} open, %{closedIssues} closed" msgid "%{openedIssues} open, %{closedIssues} closed"
msgstr "" msgstr ""
msgid "%{percentage}%% weight completed"
msgstr ""
msgid "%{percent}%% complete" msgid "%{percent}%% complete"
msgstr "" msgstr ""
...@@ -4988,6 +4991,9 @@ msgstr "" ...@@ -4988,6 +4991,9 @@ msgstr ""
msgid "Collapse approvers" msgid "Collapse approvers"
msgstr "" msgstr ""
msgid "Collapse child epics"
msgstr ""
msgid "Collapse sidebar" msgid "Collapse sidebar"
msgstr "" msgstr ""
...@@ -8321,6 +8327,9 @@ msgstr "" ...@@ -8321,6 +8327,9 @@ msgstr ""
msgid "Expand approvers" msgid "Expand approvers"
msgstr "" msgstr ""
msgid "Expand child epics"
msgstr ""
msgid "Expand down" msgid "Expand down"
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