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