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> <script>
import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { GlLabel, GlTooltip, GlTooltipDirective, GlIcon, GlLoadingIcon } 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';
...@@ -16,6 +16,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue'; ...@@ -16,6 +16,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default { export default {
components: { components: {
GlTooltip,
GlLabel, GlLabel,
GlLoadingIcon, GlLoadingIcon,
GlIcon, GlIcon,
...@@ -55,7 +56,7 @@ export default { ...@@ -55,7 +56,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['isShowingLabels', 'issuableType']), ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard']), ...mapGetters(['isEpicBoard']),
cappedAssignees() { cappedAssignees() {
// e.g. maxRender is 4, // e.g. maxRender is 4,
...@@ -99,6 +100,9 @@ export default { ...@@ -99,6 +100,9 @@ export default {
} }
return false; return false;
}, },
shouldRenderEpicCountables() {
return this.isEpicBoard && this.item.hasIssues;
},
showLabelFooter() { showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel); return this.isShowingLabels && this.item.labels.find(this.showLabel);
}, },
...@@ -115,6 +119,17 @@ export default { ...@@ -115,6 +119,17 @@ export default {
} }
return __('Blocked issue'); 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: { methods: {
...mapActions(['performSearch', 'setError']), ...mapActions(['performSearch', 'setError']),
...@@ -227,17 +242,71 @@ export default { ...@@ -227,17 +242,71 @@ export default {
{{ itemId }} {{ itemId }}
</span> </span>
<span class="board-info-items gl-mt-3 gl-display-inline-block"> <span class="board-info-items gl-mt-3 gl-display-inline-block">
<issue-due-date <span v-if="shouldRenderEpicCountables" data-testid="epic-countables">
v-if="item.dueDate" <gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
:date="item.dueDate" <p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
:closed="item.closed || Boolean(item.closedAt)" {{ __('Epics') }} &#8226;
/> <span class="gl-font-weight-normal"
<issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" /> >{{
<issue-card-weight sprintf(__('%{openedEpics} open, %{closedEpics} closed'), {
v-if="validIssueWeight(item)" openedEpics: item.descendantCounts.openedEpics,
:weight="item.weight" closedEpics: item.descendantCounts.closedEpics,
@click="filterByWeight(item.weight)" })
/> }}
</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> </span>
</div> </div>
<div class="board-card-assignee gl-display-flex"> <div class="board-card-assignee gl-display-flex">
......
...@@ -112,6 +112,12 @@ You can filter by the following: ...@@ -112,6 +112,12 @@ You can filter by the following:
- Author - Author
- Label - 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 ### Move epics and lists
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0. > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.
......
...@@ -27,6 +27,17 @@ query ListEpics( ...@@ -27,6 +27,17 @@ query ListEpics(
...Label ...Label
} }
} }
hasIssues
descendantCounts {
closedEpics
closedIssues
openedEpics
openedIssues
}
descendantWeightSum {
closedIssues
openedIssues
}
} }
} }
pageInfo { pageInfo {
......
...@@ -436,6 +436,9 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests" ...@@ -436,6 +436,9 @@ 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 ""
......
import { GlLabel, GlLoadingIcon } from '@gitlab/ui'; import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash'; import { range } from 'lodash';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
...@@ -35,8 +35,14 @@ describe('Board card component', () => { ...@@ -35,8 +35,14 @@ describe('Board card component', () => {
let store; let store;
const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon); const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createStore = () => { 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({ store = new Vuex.Store({
...defaultStore, ...defaultStore,
state: { state: {
...@@ -45,16 +51,14 @@ describe('Board card component', () => { ...@@ -45,16 +51,14 @@ describe('Board card component', () => {
}, },
getters: { getters: {
isGroupBoard: () => true, isGroupBoard: () => true,
isEpicBoard: () => false, isEpicBoard: () => isEpicBoard,
isProjectBoard: () => false, isProjectBoard: () => false,
}, },
}); });
}; };
const createWrapper = (props = {}) => { const createWrapper = (props = {}) => {
createStore(); wrapper = mountExtended(BoardCardInner, {
wrapper = mount(BoardCardInner, {
store, store,
propsData: { propsData: {
list, list,
...@@ -88,6 +92,7 @@ describe('Board card component', () => { ...@@ -88,6 +92,7 @@ describe('Board card component', () => {
weight: 1, weight: 1,
}; };
createStore();
createWrapper({ item: issue, list }); createWrapper({ item: issue, list });
}); });
...@@ -414,7 +419,90 @@ describe('Board card component', () => { ...@@ -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