Commit 788e8143 authored by Andrew Smith's avatar Andrew Smith

Show progress of an epic on epic board cards

Changelog: added
EE: true
parent e70e3415
<script> <script>
import { GlLabel, GlTooltip, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import {
GlLabel,
GlTooltip,
GlTooltipDirective,
GlIcon,
GlLoadingIcon,
GlSprintf,
} from '@gitlab/ui';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner'; import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
...@@ -26,6 +33,7 @@ export default { ...@@ -26,6 +33,7 @@ export default {
IssueTimeEstimate, IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
BoardBlockedIcon, BoardBlockedIcon,
GlSprintf,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -103,6 +111,9 @@ export default { ...@@ -103,6 +111,9 @@ export default {
shouldRenderEpicCountables() { shouldRenderEpicCountables() {
return this.isEpicBoard && this.item.hasIssues; return this.isEpicBoard && this.item.hasIssues;
}, },
shouldRenderEpicProgress() {
return this.totalWeight > 0;
},
showLabelFooter() { showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel); return this.isShowingLabels && this.item.labels.find(this.showLabel);
}, },
...@@ -130,6 +141,9 @@ export default { ...@@ -130,6 +141,9 @@ export default {
this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues
); );
}, },
totalProgress() {
return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100);
},
}, },
methods: { methods: {
...mapActions(['performSearch', 'setError']), ...mapActions(['performSearch', 'setError']),
...@@ -246,40 +260,51 @@ export default { ...@@ -246,40 +260,51 @@ export default {
<gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip"> <gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
<p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0"> <p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
{{ __('Epics') }} &#8226; {{ __('Epics') }} &#8226;
<span class="gl-font-weight-normal" <span class="gl-font-weight-normal">
>{{ <gl-sprintf :message="__('%{openedEpics} open, %{closedEpics} closed')">
sprintf(__('%{openedEpics} open, %{closedEpics} closed'), { <template #openedEpics>{{ item.descendantCounts.openedEpics }}</template>
openedEpics: item.descendantCounts.openedEpics, <template #closedEpics>{{ item.descendantCounts.closedEpics }}</template>
closedEpics: item.descendantCounts.closedEpics, </gl-sprintf>
})
}}
</span> </span>
</p> </p>
<p class="gl-font-weight-bold gl-m-0"> <p class="gl-font-weight-bold gl-m-0">
{{ __('Issues') }} &#8226; {{ __('Issues') }} &#8226;
<span class="gl-font-weight-normal" <span class="gl-font-weight-normal">
>{{ <gl-sprintf :message="__('%{openedIssues} open, %{closedIssues} closed')">
sprintf(__('%{openedIssues} open, %{closedIssues} closed'), { <template #openedIssues>{{ item.descendantCounts.openedIssues }}</template>
openedIssues: item.descendantCounts.openedIssues, <template #closedIssues>{{ item.descendantCounts.closedIssues }}</template>
closedIssues: item.descendantCounts.closedIssues, </gl-sprintf>
}) </span>
}} </p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Total weight') }} &#8226;
<span class="gl-font-weight-normal" data-testid="epic-countables-total-weight">
{{ totalWeight }}
</span> </span>
</p> </p>
</gl-tooltip>
<gl-tooltip
v-if="shouldRenderEpicProgress"
:target="() => $refs.progressBadge"
data-testid="epic-progress-tooltip"
>
<p class="gl-font-weight-bold gl-m-0"> <p class="gl-font-weight-bold gl-m-0">
{{ __('Weight') }} &#8226; {{ __('Progress') }} &#8226;
<span class="gl-font-weight-normal" data-testid="epic-countables-total-weight" <span class="gl-font-weight-normal" data-testid="epic-progress-tooltip-content">
>{{ <gl-sprintf
sprintf(__('%{closedWeight} complete, %{openWeight} incomplete'), { :message="__('%{completedWeight} of %{totalWeight} weight completed')"
openWeight: item.descendantWeightSum.openedIssues, >
closedWeight: item.descendantWeightSum.closedIssues, <template #completedWeight>{{
}) item.descendantWeightSum.closedIssues
}} }}</template>
<template #totalWeight>{{ totalWeight }}</template>
</gl-sprintf>
</span> </span>
</p> </p>
</gl-tooltip> </gl-tooltip>
<span ref="countBadge" class="issue-count-badge board-card-info"> <span ref="countBadge" class="issue-count-badge board-card-info gl-mr-0 gl-pr-0">
<span v-if="allowSubEpics" class="gl-mr-3"> <span v-if="allowSubEpics" class="gl-mr-3">
<gl-icon name="epic" /> <gl-icon name="epic" />
{{ totalEpicsCount }} {{ totalEpicsCount }}
...@@ -293,6 +318,17 @@ export default { ...@@ -293,6 +318,17 @@ export default {
{{ totalWeight }} {{ totalWeight }}
</span> </span>
</span> </span>
<span
v-if="shouldRenderEpicProgress"
ref="progressBadge"
class="issue-count-badge board-card-info gl-pl-0"
>
<span class="gl-mr-3" data-testid="epic-progress">
<gl-icon name="progress" />
{{ totalProgress }}%
</span>
</span>
</span> </span>
<span v-if="!isEpicBoard"> <span v-if="!isEpicBoard">
<issue-due-date <issue-due-date
......
...@@ -130,13 +130,14 @@ You can filter by the following: ...@@ -130,13 +130,14 @@ You can filter by the following:
- Author - Author
- Label - Label
### View count of issues and weight in an epic ### View count of issues, weight, and progress of an epic
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331330) in GitLab 14.1. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331330) in GitLab 14.1.
Epics on an epic board show a summary of their issues and weight. Epics on an epic board show a summary of their issues, weight, and progress.
To see the number of open and closed issues and the completed and incomplete weight, To see the number of open and closed issues and the completed and incomplete
hover over the issues icon **{issues}** or weight icon **{weight}**. weight, hover over the issues icon **{issues}**, weight icon **{weight}**, or
progress icon **{progress}**.
### Move epics and lists ### Move epics and lists
......
...@@ -444,9 +444,6 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests" ...@@ -444,9 +444,6 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{closedWeight} complete, %{openWeight} incomplete"
msgstr ""
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements." msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr "" msgstr ""
......
...@@ -40,7 +40,9 @@ describe('Board card component', () => { ...@@ -40,7 +40,9 @@ describe('Board card component', () => {
const findEpicCountables = () => wrapper.findByTestId('epic-countables'); const findEpicCountables = () => wrapper.findByTestId('epic-countables');
const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues'); const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues'); const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
const findEpicBadgeProgress = () => wrapper.findByTestId('epic-progress');
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content');
const createStore = ({ isEpicBoard = false } = {}) => { const createStore = ({ isEpicBoard = false } = {}) => {
store = new Vuex.Store({ store = new Vuex.Store({
...@@ -466,7 +468,7 @@ describe('Board card component', () => { ...@@ -466,7 +468,7 @@ describe('Board card component', () => {
expect(findEpicCountablesBadgeIssues().exists()).toBe(false); expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
}); });
it('shows render item countBadge and weights correctly', () => { it('shows render item countBadge, weights, and progress correctly', () => {
createWrapper({ createWrapper({
item: { item: {
...issue, ...issue,
...@@ -475,15 +477,32 @@ describe('Board card component', () => { ...@@ -475,15 +477,32 @@ describe('Board card component', () => {
openedIssues: 1, openedIssues: 1,
}, },
descendantWeightSum: { descendantWeightSum: {
...descendantWeightSum, closedIssues: 10,
openedIssues: 2, openedIssues: 5,
}, },
hasIssues: true, hasIssues: true,
}, },
}); });
expect(findEpicCountablesBadgeIssues().text()).toBe('1'); expect(findEpicCountablesBadgeIssues().text()).toBe('1');
expect(findEpicCountablesBadgeWeight().text()).toBe('2'); expect(findEpicCountablesBadgeWeight().text()).toBe('15');
expect(findEpicBadgeProgress().text()).toBe('67%');
});
it('does not render progress when weight is zero', () => {
createWrapper({
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
},
descendantWeightSum,
hasIssues: true,
},
});
expect(findEpicBadgeProgress().exists()).toBe(false);
}); });
it('renders the tooltip with the correct data', () => { it('renders the tooltip with the correct data', () => {
...@@ -502,7 +521,8 @@ describe('Board card component', () => { ...@@ -502,7 +521,8 @@ describe('Board card component', () => {
const tooltip = findEpicCountablesTotalTooltip(); const tooltip = findEpicCountablesTotalTooltip();
expect(tooltip).toBeDefined(); expect(tooltip).toBeDefined();
expect(findEpicCountablesTotalWeight().text()).toBe('10 complete, 5 incomplete'); expect(findEpicCountablesTotalWeight().text()).toBe('15');
expect(findEpicProgressTooltip().text()).toBe('10 of 15 weight completed');
}); });
}); });
}); });
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