Commit 6935c9c8 authored by Andrew Smith (EspadaV8)'s avatar Andrew Smith (EspadaV8) Committed by David O'Regan

Show open issues and remaining weight on epics

parent ae6fe648
<script>
import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { GlLabel, GlTooltip, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
......@@ -16,6 +16,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default {
components: {
GlTooltip,
GlLabel,
GlLoadingIcon,
GlIcon,
......@@ -55,7 +56,7 @@ export default {
};
},
computed: {
...mapState(['isShowingLabels', 'issuableType']),
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
......@@ -99,6 +100,9 @@ export default {
}
return false;
},
shouldRenderEpicCountables() {
return this.isEpicBoard && this.item.hasIssues;
},
showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel);
},
......@@ -115,6 +119,17 @@ export default {
}
return __('Blocked issue');
},
totalEpicsCount() {
return this.item.descendantCounts.openedEpics + this.item.descendantCounts.closedEpics;
},
totalIssuesCount() {
return this.item.descendantCounts.openedIssues + this.item.descendantCounts.closedIssues;
},
totalWeight() {
return (
this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues
);
},
},
methods: {
...mapActions(['performSearch', 'setError']),
......@@ -227,17 +242,71 @@ export default {
{{ itemId }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
<issue-due-date
v-if="item.dueDate"
:date="item.dueDate"
:closed="item.closed || Boolean(item.closedAt)"
/>
<issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
<issue-card-weight
v-if="validIssueWeight(item)"
:weight="item.weight"
@click="filterByWeight(item.weight)"
/>
<span v-if="shouldRenderEpicCountables" data-testid="epic-countables">
<gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
<p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
{{ __('Epics') }} &#8226;
<span class="gl-font-weight-normal"
>{{
sprintf(__('%{openedEpics} open, %{closedEpics} closed'), {
openedEpics: item.descendantCounts.openedEpics,
closedEpics: item.descendantCounts.closedEpics,
})
}}
</span>
</p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Issues') }} &#8226;
<span class="gl-font-weight-normal"
>{{
sprintf(__('%{openedIssues} open, %{closedIssues} closed'), {
openedIssues: item.descendantCounts.openedIssues,
closedIssues: item.descendantCounts.closedIssues,
})
}}
</span>
</p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Weight') }} &#8226;
<span class="gl-font-weight-normal" data-testid="epic-countables-total-weight"
>{{
sprintf(__('%{closedWeight} complete, %{openWeight} incomplete'), {
openWeight: item.descendantWeightSum.openedIssues,
closedWeight: item.descendantWeightSum.closedIssues,
})
}}
</span>
</p>
</gl-tooltip>
<span ref="countBadge" class="issue-count-badge board-card-info">
<span v-if="allowSubEpics" class="gl-mr-3">
<gl-icon name="epic" />
{{ totalEpicsCount }}
</span>
<span class="gl-mr-3" data-testid="epic-countables-counts-issues">
<gl-icon name="issues" />
{{ totalIssuesCount }}
</span>
<span class="gl-mr-3" data-testid="epic-countables-weight-issues">
<gl-icon name="weight" />
{{ totalWeight }}
</span>
</span>
</span>
<span v-if="!isEpicBoard">
<issue-due-date
v-if="item.dueDate"
:date="item.dueDate"
:closed="item.closed || Boolean(item.closedAt)"
/>
<issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
<issue-card-weight
v-if="validIssueWeight(item)"
:weight="item.weight"
@click="filterByWeight(item.weight)"
/>
</span>
</span>
</div>
<div class="board-card-assignee gl-display-flex">
......
......@@ -112,6 +112,12 @@ You can filter by the following:
- Author
- Label
### View count of issues and weight in an epic
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331330) in GitLab 14.1.
Epics on the **Epic Boards** show a summary of their issues and weight. Hovering over the total counts will show the number of open and closed issues, as well as the completed and incomplete weight.
### Move epics and lists
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.
......
......@@ -27,6 +27,17 @@ query ListEpics(
...Label
}
}
hasIssues
descendantCounts {
closedEpics
closedIssues
openedEpics
openedIssues
}
descendantWeightSum {
closedIssues
openedIssues
}
}
}
pageInfo {
......
......@@ -436,6 +436,9 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests"
msgstr[0] ""
msgstr[1] ""
msgid "%{closedWeight} complete, %{openWeight} incomplete"
msgstr ""
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr ""
......
import { GlLabel, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants';
......@@ -35,8 +35,14 @@ describe('Board card component', () => {
let store;
const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
const createStore = () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
const findEpicCountables = () => wrapper.findByTestId('epic-countables');
const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const createStore = ({ isEpicBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
state: {
......@@ -45,16 +51,14 @@ describe('Board card component', () => {
},
getters: {
isGroupBoard: () => true,
isEpicBoard: () => false,
isEpicBoard: () => isEpicBoard,
isProjectBoard: () => false,
},
});
};
const createWrapper = (props = {}) => {
createStore();
wrapper = mount(BoardCardInner, {
wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
list,
......@@ -88,6 +92,7 @@ describe('Board card component', () => {
weight: 1,
};
createStore();
createWrapper({ item: issue, list });
});
......@@ -414,7 +419,90 @@ describe('Board card component', () => {
},
});
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('is an epic board', () => {
const descendantCounts = {
closedEpics: 0,
closedIssues: 0,
openedEpics: 0,
openedIssues: 0,
};
const descendantWeightSum = {
closedIssues: 0,
openedIssues: 0,
};
beforeEach(() => {
createStore({ isEpicBoard: true });
});
it('should render if the item has issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: true,
},
});
expect(findEpicCountables().exists()).toBe(true);
});
it('should not render if the item does not have issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: false,
},
});
expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
});
it('shows render item countBadge and weights correctly', () => {
createWrapper({
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
},
descendantWeightSum: {
...descendantWeightSum,
openedIssues: 2,
},
hasIssues: true,
},
});
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
expect(findEpicCountablesBadgeWeight().text()).toBe('2');
});
it('renders the tooltip with the correct data', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
},
hasIssues: true,
},
});
const tooltip = findEpicCountablesTotalTooltip();
expect(tooltip).toBeDefined();
expect(findEpicCountablesTotalWeight().text()).toBe('10 complete, 5 incomplete');
});
});
});
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