Commit e84337ed authored by Phil Hughes's avatar Phil Hughes

Merge branch '11691-epic-issue-counts-epic-tree' into 'master'

Make Epics and Issues count dynamic in Epic tree

See merge request gitlab-org/gitlab-ee!14003
parents e36dd54e 1b301cc1
...@@ -46,6 +46,12 @@ export default { ...@@ -46,6 +46,12 @@ export default {
itemReference() { itemReference() {
return this.item.reference; return this.item.reference;
}, },
itemWebPath() {
// Here, GraphQL API (during item fetch) returns `webPath`
// and Rails API (during item add) returns `path`,
// we need to make both accessible.
return this.item.path || this.item.webPath;
},
isOpen() { isOpen() {
return this.item.state === ChildState.Open; return this.item.state === ChildState.Open;
}, },
...@@ -74,7 +80,7 @@ export default { ...@@ -74,7 +80,7 @@ export default {
return this.itemReference.split(this.item.pathIdSeparator).pop(); return this.itemReference.split(this.item.pathIdSeparator).pop();
}, },
computedPath() { computedPath() {
return this.item.webPath.length ? this.item.webPath : null; return this.itemWebPath.length ? this.itemWebPath : null;
}, },
itemActionInProgress() { itemActionInProgress() {
return ( return (
......
...@@ -23,6 +23,22 @@ export const setInitialConfig = ({ commit }, data) => commit(types.SET_INITIAL_C ...@@ -23,6 +23,22 @@ 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 }) => {
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],
);
commit(types.SET_CHILDREN_COUNT, { epicsCount, issuesCount });
};
export const expandItem = ({ commit }, data) => commit(types.EXPAND_ITEM, data); export const expandItem = ({ commit }, data) => commit(types.EXPAND_ITEM, data);
export const collapseItem = ({ commit }, data) => commit(types.COLLAPSE_ITEM, data); export const collapseItem = ({ commit }, data) => commit(types.COLLAPSE_ITEM, data);
...@@ -33,6 +49,8 @@ export const setItemChildren = ({ commit, dispatch }, { parentItem, children, is ...@@ -33,6 +49,8 @@ export const setItemChildren = ({ commit, dispatch }, { parentItem, children, is
isSubItem, isSubItem,
}); });
dispatch('setChildrenCount', { children });
if (isSubItem) { if (isSubItem) {
dispatch('expandItem', { dispatch('expandItem', {
parentItem, parentItem,
...@@ -131,6 +149,8 @@ export const removeItem = ({ dispatch }, { parentItem, item }) => { ...@@ -131,6 +149,8 @@ export const removeItem = ({ dispatch }, { parentItem, item }) => {
parentItem, parentItem,
item, item,
}); });
dispatch('setChildrenCount', { children: [item], isRemoved: true });
}) })
.catch(({ status }) => { .catch(({ status }) => {
dispatch('receiveRemoveItemFailure', { dispatch('receiveRemoveItemFailure', {
...@@ -168,6 +188,8 @@ export const receiveAddItemSuccess = ({ dispatch, commit, getters }, { actionTyp ...@@ -168,6 +188,8 @@ export const receiveAddItemSuccess = ({ dispatch, commit, getters }, { actionTyp
items, items,
}); });
dispatch('setChildrenCount', { children: items });
dispatch('setItemChildrenFlags', { dispatch('setItemChildrenFlags', {
children: items, children: items,
isSubItem: false, isSubItem: false,
...@@ -224,6 +246,8 @@ export const receiveCreateItemSuccess = ( ...@@ -224,6 +246,8 @@ export const receiveCreateItemSuccess = (
item, item,
}); });
dispatch('setChildrenCount', { children: [item] });
dispatch('setItemChildrenFlags', { dispatch('setItemChildrenFlags', {
children: [item], children: [item],
isSubItem: false, isSubItem: false,
......
...@@ -7,34 +7,20 @@ export const directChildren = state => state.children[state.parentItem.reference ...@@ -7,34 +7,20 @@ export const directChildren = state => state.children[state.parentItem.reference
export const anyParentHasChildren = (state, getters) => export const anyParentHasChildren = (state, getters) =>
getters.directChildren.some(item => item.hasChildren || item.hasIssues); getters.directChildren.some(item => item.hasChildren || item.hasIssues);
export const headerItems = (state, getters) => { export const headerItems = state => [
const children = getters.directChildren || []; {
let totalEpics = 0; iconName: 'epic',
let totalIssues = 0; count: state.epicsCount,
qaClass: 'qa-add-epics-button',
children.forEach(item => { type: ChildType.Epic,
if (item.type === ChildType.Epic) { },
totalEpics += 1; {
} else { iconName: 'issues',
totalIssues += 1; count: state.issuesCount,
} qaClass: 'qa-add-issues-button',
}); type: ChildType.Issue,
},
return [ ];
{
iconName: 'epic',
count: totalEpics,
qaClass: 'qa-add-epics-button',
type: ChildType.Epic,
},
{
iconName: 'issues',
count: totalIssues,
qaClass: 'qa-add-issues-button',
type: ChildType.Issue,
},
];
};
export const epicsBeginAtIndex = (state, getters) => export const epicsBeginAtIndex = (state, getters) =>
getters.directChildren.findIndex(item => item.type === ChildType.Epic); getters.directChildren.findIndex(item => item.type === ChildType.Epic);
......
...@@ -2,6 +2,7 @@ export const SET_INITIAL_CONFIG = 'SET_INITIAL_CONFIG'; ...@@ -2,6 +2,7 @@ export const SET_INITIAL_CONFIG = 'SET_INITIAL_CONFIG';
export const SET_INITIAL_PARENT_ITEM = 'SET_INITIAL_PARENT_ITEM'; export const SET_INITIAL_PARENT_ITEM = 'SET_INITIAL_PARENT_ITEM';
export const SET_CHILDREN_COUNT = 'SET_CHILDREN_COUNT';
export const SET_ITEM_CHILDREN = 'SET_ITEM_CHILDREN'; export const SET_ITEM_CHILDREN = 'SET_ITEM_CHILDREN';
export const SET_ITEM_CHILDREN_FLAGS = 'SET_ITEM_CHILDREN_FLAGS'; export const SET_ITEM_CHILDREN_FLAGS = 'SET_ITEM_CHILDREN_FLAGS';
......
...@@ -18,6 +18,11 @@ export default { ...@@ -18,6 +18,11 @@ export default {
state.childrenFlags[state.parentItem.reference] = {}; state.childrenFlags[state.parentItem.reference] = {};
}, },
[types.SET_CHILDREN_COUNT](state, { epicsCount, issuesCount }) {
state.epicsCount = epicsCount;
state.issuesCount = issuesCount;
},
[types.SET_ITEM_CHILDREN](state, { parentItem, children }) { [types.SET_ITEM_CHILDREN](state, { parentItem, children }) {
Vue.set(state.children, parentItem.reference, children); Vue.set(state.children, parentItem.reference, children);
}, },
......
...@@ -6,6 +6,8 @@ export default () => ({ ...@@ -6,6 +6,8 @@ export default () => ({
children: {}, children: {},
childrenFlags: {}, childrenFlags: {},
epicsCount: 0,
issuesCount: 0,
// Add Item Form Data // Add Item Form Data
actionType: '', actionType: '',
......
...@@ -74,7 +74,8 @@ describe('RelatedItemsTree', () => { ...@@ -74,7 +74,8 @@ describe('RelatedItemsTree', () => {
describe('headerItems', () => { describe('headerItems', () => {
it('returns an item within array containing Epic iconName, count, qaClass & type props', () => { it('returns an item within array containing Epic iconName, count, qaClass & type props', () => {
const epicHeaderItem = getters.headerItems(state, mockGetters)[0]; state.epicsCount = 2;
const epicHeaderItem = getters.headerItems(state)[0];
expect(epicHeaderItem).toEqual( expect(epicHeaderItem).toEqual(
expect.objectContaining({ expect.objectContaining({
...@@ -87,7 +88,8 @@ describe('RelatedItemsTree', () => { ...@@ -87,7 +88,8 @@ describe('RelatedItemsTree', () => {
}); });
it('returns an item within array containing Issue iconName, count, qaClass & type props', () => { it('returns an item within array containing Issue iconName, count, qaClass & type props', () => {
const epicHeaderItem = getters.headerItems(state, mockGetters)[1]; state.issuesCount = 2;
const epicHeaderItem = getters.headerItems(state)[1];
expect(epicHeaderItem).toEqual( expect(epicHeaderItem).toEqual(
expect.objectContaining({ expect.objectContaining({
......
...@@ -46,6 +46,20 @@ describe('RelatedItemsTree', () => { ...@@ -46,6 +46,20 @@ describe('RelatedItemsTree', () => {
}); });
}); });
describe(types.SET_CHILDREN_COUNT, () => {
it('should set provided `epicsCount` and `issuesCount` to state', () => {
const data = {
epicsCount: 4,
issuesCount: 5,
};
mutations[types.SET_CHILDREN_COUNT](state, data);
expect(state.epicsCount).toBe(data.epicsCount);
expect(state.issuesCount).toBe(data.issuesCount);
});
});
describe(types.SET_ITEM_CHILDREN, () => { describe(types.SET_ITEM_CHILDREN, () => {
it('should set provided `data.children` to `state.children` with reference key as present in `data.parentItem`', () => { it('should set provided `data.children` to `state.children` with reference key as present in `data.parentItem`', () => {
const data = { const data = {
......
...@@ -67,6 +67,40 @@ describe('RelatedItemsTree', () => { ...@@ -67,6 +67,40 @@ describe('RelatedItemsTree', () => {
}); });
}); });
describe('itemWebPath', () => {
const mockPath = '/foo/bar';
it('returns value of `item.path`', done => {
wrapper.setProps({
item: Object.assign({}, mockItem, {
path: mockPath,
webPath: undefined,
}),
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.itemWebPath).toBe(mockPath);
done();
});
});
it('returns value of `item.webPath` when `item.path` is undefined', done => {
wrapper.setProps({
item: Object.assign({}, mockItem, {
path: undefined,
webPath: mockPath,
}),
});
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.itemWebPath).toBe(mockPath);
done();
});
});
});
describe('isOpen', () => { describe('isOpen', () => {
it('returns true when `item.state` value is `opened`', done => { it('returns true when `item.state` value is `opened`', done => {
wrapper.setProps({ wrapper.setProps({
...@@ -214,11 +248,11 @@ describe('RelatedItemsTree', () => { ...@@ -214,11 +248,11 @@ describe('RelatedItemsTree', () => {
}); });
describe('computedPath', () => { describe('computedPath', () => {
it('returns value of `item.webPath` when it is defined', () => { it('returns value of `itemWebPath` when it is defined', () => {
expect(wrapper.vm.computedPath).toBe(mockItem.webPath); expect(wrapper.vm.computedPath).toBe(mockItem.webPath);
}); });
it('returns `null` when `item.webPath` is empty', done => { it('returns `null` when `itemWebPath` is empty', done => {
wrapper.setProps({ wrapper.setProps({
item: Object.assign({}, mockItem, { item: Object.assign({}, mockItem, {
webPath: '', webPath: '',
......
...@@ -110,6 +110,8 @@ export const mockIssue2 = { ...@@ -110,6 +110,8 @@ export const mockIssue2 = {
export const mockEpics = [mockEpic1, mockEpic2]; export const mockEpics = [mockEpic1, mockEpic2];
export const mockIssues = [mockIssue1, mockIssue2];
export const mockQueryResponse = { export const mockQueryResponse = {
data: { data: {
group: { group: {
......
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
mockParentItem, mockParentItem,
mockQueryResponse, mockQueryResponse,
mockEpics, mockEpics,
mockIssues,
mockEpic1, mockEpic1,
} from '../mock_data'; } from '../mock_data';
...@@ -61,6 +62,57 @@ describe('RelatedItemTree', () => { ...@@ -61,6 +62,57 @@ describe('RelatedItemTree', () => {
}); });
}); });
describe('setChildrenCount', () => {
const mockEpicsWithType = mockEpics.map(item =>
Object.assign({}, item, {
type: ChildType.Epic,
}),
);
const mockIssuesWithType = mockIssues.map(item =>
Object.assign({}, item, {
type: ChildType.Issue,
}),
);
const mockChildren = [...mockEpicsWithType, ...mockIssuesWithType];
it('should set `epicsCount` and `issuesCount`, by incrementing it, on state', done => {
testAction(
actions.setChildrenCount,
{ children: mockChildren, isRemoved: false },
{},
[
{
type: types.SET_CHILDREN_COUNT,
payload: { epicsCount: mockEpics.length, issuesCount: mockIssues.length },
},
],
[],
done,
);
});
it('should set `epicsCount` and `issuesCount`, by decrementing it, on state', done => {
testAction(
actions.setChildrenCount,
{ children: mockChildren, isRemoved: true },
{
epicsCount: mockEpics.length,
issuesCount: mockIssues.length,
},
[
{
type: types.SET_CHILDREN_COUNT,
payload: { epicsCount: 0, issuesCount: 0 },
},
],
[],
done,
);
});
});
describe('expandItem', () => { describe('expandItem', () => {
it('should set `itemExpanded` to true on state.childrenFlags', done => { it('should set `itemExpanded` to true on state.childrenFlags', done => {
testAction( testAction(
...@@ -101,7 +153,12 @@ describe('RelatedItemTree', () => { ...@@ -101,7 +153,12 @@ describe('RelatedItemTree', () => {
payload: mockPayload, payload: mockPayload,
}, },
], ],
[], [
{
type: 'setChildrenCount',
payload: { children: mockPayload.children },
},
],
done, done,
); );
}); });
...@@ -120,6 +177,10 @@ describe('RelatedItemTree', () => { ...@@ -120,6 +177,10 @@ describe('RelatedItemTree', () => {
}, },
], ],
[ [
{
type: 'setChildrenCount',
payload: { children: mockPayload.children },
},
{ {
type: 'expandItem', type: 'expandItem',
payload: { parentItem: mockPayload.parentItem }, payload: { parentItem: mockPayload.parentItem },
...@@ -470,6 +531,10 @@ describe('RelatedItemTree', () => { ...@@ -470,6 +531,10 @@ describe('RelatedItemTree', () => {
type: 'receiveRemoveItemSuccess', type: 'receiveRemoveItemSuccess',
payload: { parentItem: data.parentItem, item: data.item }, payload: { parentItem: data.parentItem, item: data.item },
}, },
{
type: 'setChildrenCount',
payload: { children: [data.item], isRemoved: true },
},
], ],
done, done,
); );
...@@ -606,6 +671,10 @@ describe('RelatedItemTree', () => { ...@@ -606,6 +671,10 @@ describe('RelatedItemTree', () => {
}, },
], ],
[ [
{
type: 'setChildrenCount',
payload: { children: mockEpicsWithoutPerm },
},
{ {
type: 'setItemChildrenFlags', type: 'setItemChildrenFlags',
payload: { children: mockEpicsWithoutPerm, isSubItem: false }, payload: { children: mockEpicsWithoutPerm, isSubItem: false },
...@@ -755,6 +824,10 @@ describe('RelatedItemTree', () => { ...@@ -755,6 +824,10 @@ describe('RelatedItemTree', () => {
}, },
], ],
[ [
{
type: 'setChildrenCount',
payload: { children: [mockItems[0]] },
},
{ {
type: 'setItemChildrenFlags', type: 'setItemChildrenFlags',
payload: { children: [mockItems[0]], isSubItem: false }, payload: { children: [mockItems[0]], isSubItem: false },
......
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