Commit 505bcb7a authored by Donald Cook's avatar Donald Cook Committed by Natalia Tepluhina

Static counter for related items

Updated related items tree to use GraphQL endpoint for count
parent ba6941e1
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { mapState, mapActions } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
......@@ -20,14 +20,19 @@ export default {
GlTooltip: GlTooltipDirective,
},
computed: {
...mapGetters(['headerItems']),
...mapState(['parentItem']),
...mapState(['parentItem', 'descendantCounts']),
badgeTooltip() {
return sprintf(s__('Epics|%{epicsCount} epics and %{issuesCount} issues'), {
epicsCount: this.headerItems[0].count,
issuesCount: this.headerItems[1].count,
epicsCount: this.totalEpicsCount,
issuesCount: this.totalIssuesCount,
});
},
totalEpicsCount() {
return this.descendantCounts.openedEpics + this.descendantCounts.closedEpics;
},
totalIssuesCount() {
return this.descendantCounts.openedIssues + this.descendantCounts.closedIssues;
},
},
methods: {
...mapActions(['toggleAddItemForm', 'toggleCreateEpicForm', 'setItemInputValue']),
......@@ -59,29 +64,27 @@ export default {
class="issue-count-badge"
:title="badgeTooltip"
>
<span
v-for="(item, index) in headerItems"
:key="index"
:class="{ 'ml-2': index }"
class="d-inline-flex align-items-center"
>
<icon :size="16" :name="item.iconName" class="text-secondary mr-1" />
{{ item.count }}
<span class="d-inline-flex align-items-center">
<icon :size="16" name="epic" class="text-secondary mr-1" />
{{ totalEpicsCount }}
</span>
<span class="ml-2 d-inline-flex align-items-center">
<icon :size="16" name="issues" class="text-secondary mr-1" />
{{ totalIssuesCount }}
</span>
</div>
</div>
<div class="d-inline-flex js-button-container">
<template v-if="parentItem.userPermissions.adminEpic">
<epic-actions-split-button
:class="headerItems[0].qaClass"
class="qa-add-epics-button"
@showAddEpicForm="showAddEpicForm"
@showCreateEpicForm="showCreateEpicForm"
/>
<slot name="issueActions">
<gl-button
:class="headerItems[1].qaClass"
class="ml-1 js-add-issues-button"
class="ml-1 js-add-issues-button qa-add-issues-button"
size="sm"
@click="showAddIssueForm"
>{{ __('Add an issue') }}</gl-button
......
......@@ -8,6 +8,12 @@ fragment BaseEpic on Epic {
adminEpic
createEpic
}
descendantCounts {
openedEpics
closedEpics
openedIssues
closedIssues
}
}
fragment EpicNode on Epic {
......
......@@ -25,20 +25,23 @@ export const setInitialConfig = ({ commit }, data) => commit(types.SET_INITIAL_C
export const setInitialParentItem = ({ commit }, data) =>
commit(types.SET_INITIAL_PARENT_ITEM, data);
export const setChildrenCount = ({ commit, state }, { children, isRemoved = false }) => {
const [epicsCount, issuesCount] = children.reduce(
(acc, item) => {
if (item.type === ChildType.Epic) {
acc[0] += isRemoved ? -1 : 1;
} else {
acc[1] += isRemoved ? -1 : 1;
}
return acc;
},
[state.epicsCount || 0, state.issuesCount || 0],
);
export const setChildrenCount = ({ commit, state }, data) =>
commit(types.SET_CHILDREN_COUNT, { ...state.descendantCounts, ...data });
export const updateChildrenCount = ({ state, dispatch }, { item, isRemoved = false }) => {
const descendantCounts = {};
if (item.type === ChildType.Epic) {
descendantCounts[`${item.state}Epics`] = isRemoved
? state.descendantCounts[`${item.state}Epics`] - 1
: state.descendantCounts[`${item.state}Epics`] + 1;
} else {
descendantCounts[`${item.state}Issues`] = isRemoved
? state.descendantCounts[`${item.state}Issues`] - 1
: state.descendantCounts[`${item.state}Issues`] + 1;
}
commit(types.SET_CHILDREN_COUNT, { epicsCount, issuesCount });
dispatch('setChildrenCount', descendantCounts);
};
export const expandItem = ({ commit }, data) => commit(types.EXPAND_ITEM, data);
......@@ -55,8 +58,6 @@ export const setItemChildren = (
append,
});
dispatch('setChildrenCount', { children });
if (isSubItem) {
dispatch('expandItem', {
parentItem,
......@@ -117,6 +118,10 @@ export const fetchItems = ({ dispatch }, { parentItem, isSubItem = false }) => {
parentItem,
pageInfo: data.group.epic.issues.pageInfo,
});
if (!isSubItem) {
dispatch('setChildrenCount', data.group.epic.descendantCounts);
}
})
.catch(() => {
dispatch('receiveItemsFailure', {
......@@ -235,7 +240,7 @@ export const removeItem = ({ dispatch }, { parentItem, item }) => {
item,
});
dispatch('setChildrenCount', { children: [item], isRemoved: true });
dispatch('updateChildrenCount', { item, isRemoved: true });
})
.catch(({ status }) => {
dispatch('receiveRemoveItemFailure', {
......@@ -290,7 +295,9 @@ export const receiveAddItemSuccess = ({ dispatch, commit, getters }, { rawItems
items,
});
dispatch('setChildrenCount', { children: items });
items.forEach(item => {
dispatch('updateChildrenCount', { item });
});
dispatch('setItemChildrenFlags', {
children: items,
......@@ -346,7 +353,7 @@ export const receiveCreateItemSuccess = ({ state, commit, dispatch, getters }, {
item,
});
dispatch('setChildrenCount', { children: [item] });
dispatch('updateChildrenCount', { item });
dispatch('setItemChildrenFlags', {
children: [item],
......
......@@ -19,9 +19,8 @@ export default {
state.childrenFlags[state.parentItem.reference] = {};
},
[types.SET_CHILDREN_COUNT](state, { epicsCount, issuesCount }) {
state.epicsCount = epicsCount;
state.issuesCount = issuesCount;
[types.SET_CHILDREN_COUNT](state, data) {
state.descendantCounts = data;
},
[types.SET_ITEM_CHILDREN](state, { parentItem, children, append }) {
......
......@@ -9,6 +9,12 @@ export default () => ({
childrenFlags: {},
epicsCount: 0,
issuesCount: 0,
descendantCounts: {
openedEpics: 0,
closedEpics: 0,
openedIssues: 0,
closedIssues: 0,
},
// Add Item Form Data
issuableType: null,
......
......@@ -28,6 +28,7 @@ const createComponent = ({ slots } = {}) => {
isSubItem: false,
children,
});
store.dispatch('setChildrenCount', mockParentItem.descendantCounts);
return shallowMount(RelatedItemsTreeHeader, {
attachToDocument: true,
......
......@@ -49,14 +49,15 @@ describe('RelatedItemsTree', () => {
describe(types.SET_CHILDREN_COUNT, () => {
it('should set provided `epicsCount` and `issuesCount` to state', () => {
const data = {
epicsCount: 4,
issuesCount: 5,
openedEpics: 4,
closedEpics: 5,
openedIssues: 6,
closedIssues: 7,
};
mutations[types.SET_CHILDREN_COUNT](state, data);
expect(state.epicsCount).toBe(data.epicsCount);
expect(state.issuesCount).toBe(data.issuesCount);
expect(state.descendantCounts).toEqual(data);
});
});
......
......@@ -15,6 +15,12 @@ export const mockParentItem = {
adminEpic: true,
createEpic: true,
},
descendantCounts: {
openedEpics: 1,
closedEpics: 1,
openedIssues: 1,
closedIssues: 1,
},
};
export const mockEpic1 = {
......@@ -177,6 +183,7 @@ export const mockQueryResponse = {
hasNextPage: true,
},
},
descendantCounts: mockParentItem.descendantCounts,
},
},
},
......
......@@ -61,6 +61,36 @@ describe('RelatedItemTree', () => {
});
describe('setChildrenCount', () => {
it('should set initial descendantCounts on state', done => {
testAction(
actions.setChildrenCount,
mockParentItem.descendantCounts,
{},
[{ type: types.SET_CHILDREN_COUNT, payload: mockParentItem.descendantCounts }],
[],
done,
);
});
it('should persist non overwritten descendantCounts state', done => {
const descendantCounts = { openedEpics: 9 };
testAction(
actions.setChildrenCount,
descendantCounts,
{ descendantCounts: mockParentItem.descendantCounts },
[
{
type: types.SET_CHILDREN_COUNT,
payload: { ...mockParentItem.descendantCounts, ...descendantCounts },
},
],
[],
done,
);
});
});
describe('updateChildrenCount', () => {
const mockEpicsWithType = mockEpics.map(item =>
Object.assign({}, item, {
type: ChildType.Epic,
......@@ -73,39 +103,66 @@ describe('RelatedItemTree', () => {
}),
);
const mockChildren = [...mockEpicsWithType, ...mockIssuesWithType];
it('should set `epicsCount` and `issuesCount`, by incrementing it, on state', done => {
it('should update openedEpics, by incrementing it', done => {
testAction(
actions.setChildrenCount,
{ children: mockChildren, isRemoved: false },
{},
actions.updateChildrenCount,
{ item: mockEpicsWithType[0], isRemoved: false },
{ descendantCounts: mockParentItem.descendantCounts },
[],
[
{
type: types.SET_CHILDREN_COUNT,
payload: { epicsCount: mockEpics.length, issuesCount: mockIssues.length },
type: 'setChildrenCount',
payload: { openedEpics: mockParentItem.descendantCounts.openedEpics + 1 },
},
],
done,
);
});
it('should update openedIssues, by incrementing it', done => {
testAction(
actions.updateChildrenCount,
{ item: mockIssuesWithType[0], isRemoved: false },
{ descendantCounts: mockParentItem.descendantCounts },
[],
[
{
type: 'setChildrenCount',
payload: { openedIssues: mockParentItem.descendantCounts.openedIssues + 1 },
},
],
done,
);
});
it('should set `epicsCount` and `issuesCount`, by decrementing it, on state', done => {
it('should update openedEpics, by decrementing it', done => {
testAction(
actions.setChildrenCount,
{ children: mockChildren, isRemoved: true },
{
epicsCount: mockEpics.length,
issuesCount: mockIssues.length,
},
actions.updateChildrenCount,
{ item: mockEpicsWithType[0], isRemoved: true },
{ descendantCounts: mockParentItem.descendantCounts },
[],
[
{
type: types.SET_CHILDREN_COUNT,
payload: { epicsCount: 0, issuesCount: 0 },
type: 'setChildrenCount',
payload: { openedEpics: mockParentItem.descendantCounts.openedEpics - 1 },
},
],
done,
);
});
it('should update openedIssues, by decrementing it', done => {
testAction(
actions.updateChildrenCount,
{ item: mockIssuesWithType[0], isRemoved: true },
{ descendantCounts: mockParentItem.descendantCounts },
[],
[
{
type: 'setChildrenCount',
payload: { openedIssues: mockParentItem.descendantCounts.openedIssues - 1 },
},
],
done,
);
});
......@@ -156,12 +213,7 @@ describe('RelatedItemTree', () => {
payload: mockPayload,
},
],
[
{
type: 'setChildrenCount',
payload: { children: mockPayload.children },
},
],
[],
done,
);
});
......@@ -180,10 +232,6 @@ describe('RelatedItemTree', () => {
},
],
[
{
type: 'setChildrenCount',
payload: { children: mockPayload.children },
},
{
type: 'expandItem',
payload: { parentItem: mockPayload.parentItem },
......@@ -331,6 +379,7 @@ describe('RelatedItemTree', () => {
const children = epicUtils.processQueryResponse(mockQueryResponse.data.group);
const epicPageInfo = mockQueryResponse.data.group.epic.children.pageInfo;
const issuesPageInfo = mockQueryResponse.data.group.epic.issues.pageInfo;
const epicDescendantCounts = mockQueryResponse.data.group.epic.descendantCounts;
testAction(
actions.fetchItems,
......@@ -379,6 +428,12 @@ describe('RelatedItemTree', () => {
pageInfo: issuesPageInfo,
},
},
{
type: 'setChildrenCount',
payload: {
...epicDescendantCounts,
},
},
],
done,
);
......@@ -684,8 +739,8 @@ describe('RelatedItemTree', () => {
payload: { parentItem: data.parentItem, item: data.item },
},
{
type: 'setChildrenCount',
payload: { children: [data.item], isRemoved: true },
type: 'updateChildrenCount',
payload: { item: data.item, isRemoved: true },
},
],
done,
......@@ -826,8 +881,12 @@ describe('RelatedItemTree', () => {
],
[
{
type: 'setChildrenCount',
payload: { children: mockEpicsWithoutPerm },
type: 'updateChildrenCount',
payload: { item: mockEpicsWithoutPerm[0] },
},
{
type: 'updateChildrenCount',
payload: { item: mockEpicsWithoutPerm[1] },
},
{
type: 'setItemChildrenFlags',
......@@ -989,8 +1048,8 @@ describe('RelatedItemTree', () => {
],
[
{
type: 'setChildrenCount',
payload: { children: [createdEpic] },
type: 'updateChildrenCount',
payload: { item: createdEpic },
},
{
type: 'setItemChildrenFlags',
......
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