Commit 17d502d4 authored by Florie Guibert's avatar Florie Guibert

View closed issues in epic

- Reorder epics and issues in tree view, opened first, closed last
- Add grey background on closed issues and epics in tree view
- These changes apply to nested groups and epics and issues
parent a94463a8
---
title: View closed issues in epic
merge_request: 19741
author:
type: added
......@@ -110,7 +110,11 @@ export default {
<div class="card card-slim sortable-row flex-grow-1">
<div
class="item-body card-body d-flex align-items-center p-2 pl-xl-3"
:class="{ 'p-xl-1': userSignedIn, 'item-logged-out pt-xl-2 pb-xl-2': !userSignedIn }"
:class="{
'p-xl-1': userSignedIn,
'item-logged-out pt-xl-2 pb-xl-2': !userSignedIn,
'item-closed': isClosed,
}"
>
<div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
<div class="item-title d-flex align-items-center mb-1 mb-xl-0">
......
......@@ -9,14 +9,27 @@ export const gqClient = createGqClient();
* Returns a numeric representation of item
* order in an array.
*
* This method is to be used as comparision
* This method is to be used as comparison
* function for Array.sort
*
* @param {cbject} childA
* @param {object} childB
* @param {Object} childA
* @param {Object} childB
*/
export const sortChildren = (childA, childB) => childA.relativePosition - childB.relativePosition;
/**
* Returns a numeric representation of item, by state,
* opened items first, closed items last
* Used to sort epics and issues
*
* This method is to be used as comparison
* function for Array.sort
*
* @param {Array} items
*/
const stateOrder = ['opened', 'closed'];
export const sortByState = (a, b) => stateOrder.indexOf(a.state) - stateOrder.indexOf(b.state);
/**
* Returns formatted child item to include additional
* flags and properties to use while rendering tree.
......@@ -76,4 +89,7 @@ export const extractChildIssues = issues =>
* @param {Object} responseRoot
*/
export const processQueryResponse = ({ epic }) =>
[].concat(extractChildEpics(epic.children), extractChildIssues(epic.issues)).sort(sortChildren);
[]
.concat(extractChildEpics(epic.children), extractChildIssues(epic.issues))
.sort(sortChildren)
.sort(sortByState);
......@@ -24,6 +24,10 @@
cursor: default;
min-height: $grid-size * 5;
}
&.item-closed {
background-color: $gray-50;
}
}
.btn-tree-item-chevron {
......
......@@ -295,6 +295,10 @@ describe('RelatedItemsTree', () => {
expect(wrapper.find('.item-body').classes()).not.toContain('item-logged-out');
});
it('renders item body element without class `item-closed` when item state is opened', () => {
expect(wrapper.find('.item-body').classes()).not.toContain('item-closed');
});
it('renders item state icon for large screens', () => {
const statusIcon = wrapper.findAll(Icon).at(0);
......
......@@ -4,7 +4,7 @@ import { PathIdSeparator } from 'ee/related_issues/constants';
import { ChildType } from 'ee/related_items_tree/constants';
import {
mockQueryResponse,
mockQueryResponse2,
mockEpic1,
mockIssue1,
} from '../../../javascripts/related_items_tree/mock_data';
......@@ -44,6 +44,57 @@ describe('RelatedItemsTree', () => {
});
});
describe('sortByState', () => {
const items = [
{
state: 'closed',
},
{
state: 'opened',
},
{
state: 'closed',
},
];
const paramA = {};
const paramB = {};
it('returns non-zero positive integer when paramA.state is closed and paramB.state is opened', () => {
paramA.state = 'closed';
paramB.state = 'opened';
expect(epicUtils.sortByState(paramA, paramB) > -1).toBe(true);
});
it('returns non-zero negative integer when paramA.state is opened and paramB.state is closed', () => {
paramA.state = 'opened';
paramB.state = 'closed';
expect(epicUtils.sortByState(paramA, paramB) < 0).toBe(true);
});
it('returns zero when paramA.state is same as paramB.state', () => {
paramA.state = 'opened';
paramB.state = 'opened';
expect(epicUtils.sortByState(paramA, paramB)).toBe(0);
});
it('reorders items by state, opened first, closed last', () => {
expect(items.sort(epicUtils.sortByState)).toEqual([
{
state: 'opened',
},
{
state: 'closed',
},
{
state: 'closed',
},
]);
});
});
describe('formatChildItem', () => {
it('returns new object from provided item object with pathIdSeparator assigned', () => {
const item = {
......@@ -61,11 +112,11 @@ describe('RelatedItemsTree', () => {
describe('extractChildEpics', () => {
it('returns updated epics array with `type` and `pathIdSeparator` assigned and `edges->node` nesting removed', () => {
const formattedChildren = epicUtils.extractChildEpics(
mockQueryResponse.data.group.epic.children,
mockQueryResponse2.data.group.epic.children,
);
expect(formattedChildren.length).toBe(
mockQueryResponse.data.group.epic.children.edges.length,
mockQueryResponse2.data.group.epic.children.edges.length,
);
expect(formattedChildren[0]).toHaveProperty('type', ChildType.Epic);
expect(formattedChildren[0]).toHaveProperty('pathIdSeparator', PathIdSeparator.Epic);
......@@ -88,11 +139,11 @@ describe('RelatedItemsTree', () => {
describe('extractChildIssues', () => {
it('returns updated issues array with `type` and `pathIdSeparator` assigned and `edges->node` nesting removed', () => {
const formattedChildren = epicUtils.extractChildIssues(
mockQueryResponse.data.group.epic.issues,
mockQueryResponse2.data.group.epic.issues,
);
expect(formattedChildren.length).toBe(
mockQueryResponse.data.group.epic.issues.edges.length,
mockQueryResponse2.data.group.epic.issues.edges.length,
);
expect(formattedChildren[0]).toHaveProperty('type', ChildType.Issue);
expect(formattedChildren[0]).toHaveProperty('pathIdSeparator', PathIdSeparator.Issue);
......@@ -100,14 +151,20 @@ describe('RelatedItemsTree', () => {
});
describe('processQueryResponse', () => {
it('returns array of issues and epics from query response with issues being on top of the list', () => {
const formattedChildren = epicUtils.processQueryResponse(mockQueryResponse.data.group);
it('returns array of issues and epics from query response with open epics and issues being on top of the list', () => {
const formattedChildren = epicUtils.processQueryResponse(mockQueryResponse2.data.group);
expect(formattedChildren.length).toBe(4); // 2 Epics and 2 Issues
expect(formattedChildren.length).toBe(5); // 2 Epics and 3 Issues
expect(formattedChildren[0]).toHaveProperty('type', ChildType.Epic);
expect(formattedChildren[1]).toHaveProperty('type', ChildType.Epic);
expect(formattedChildren[0]).toHaveProperty('state', 'opened');
expect(formattedChildren[1]).toHaveProperty('type', ChildType.Issue);
expect(formattedChildren[1]).toHaveProperty('state', 'opened');
expect(formattedChildren[2]).toHaveProperty('type', ChildType.Issue);
expect(formattedChildren[3]).toHaveProperty('type', ChildType.Issue);
expect(formattedChildren[2]).toHaveProperty('state', 'opened');
expect(formattedChildren[3]).toHaveProperty('type', ChildType.Epic);
expect(formattedChildren[3]).toHaveProperty('state', 'closed');
expect(formattedChildren[4]).toHaveProperty('type', ChildType.Issue);
expect(formattedChildren[4]).toHaveProperty('state', 'closed');
});
});
});
......
......@@ -13,7 +13,7 @@ import {
mockInitialConfig,
mockParentItem,
mockEpic1,
mockIssue1,
mockIssue2,
} from '../mock_data';
const { epic } = mockQueryResponse.data.group;
......@@ -138,7 +138,7 @@ describe('RelatedItemsTree', () => {
});
it('returns value of `epicIssueId` prop when item is an Issue', () => {
expect(wrapper.vm.getItemId(wrapper.vm.children[2])).toBe(mockIssue1.epicIssueId);
expect(wrapper.vm.getItemId(wrapper.vm.children[2])).toBe(mockIssue2.epicIssueId);
});
});
......@@ -166,7 +166,7 @@ describe('RelatedItemsTree', () => {
}),
).toEqual(
jasmine.objectContaining({
id: mockIssue1.epicIssueId,
id: mockIssue2.epicIssueId,
}),
);
});
......@@ -192,7 +192,7 @@ describe('RelatedItemsTree', () => {
}),
).toEqual(
jasmine.objectContaining({
adjacentReferenceId: mockIssue1.epicIssueId,
adjacentReferenceId: mockIssue2.epicIssueId,
}),
);
});
......
......@@ -111,6 +111,25 @@ export const mockIssue2 = {
milestone: null,
};
export const mockIssue3 = {
iid: '42',
epicIssueId: 'gid://gitlab/EpicIssue/5',
title: 'View closed issues in epic',
closedAt: null,
state: 'closed',
createdAt: '2019-02-18T14:13:05Z',
confidential: false,
dueDate: null,
weight: null,
webPath: '/gitlab-org/gitlab-shell/issues/42',
reference: 'gitlab-org/gitlab-shell#42',
relationPath: '/groups/gitlab-org/-/epics/1/issues/27',
assignees: {
edges: [],
},
milestone: null,
};
export const mockEpics = [mockEpic1, mockEpic2];
export const mockIssues = [mockIssue1, mockIssue2];
......@@ -163,6 +182,108 @@ export const mockQueryResponse = {
},
};
export const mockQueryResponse2 = {
data: {
group: {
id: 1,
path: 'gitlab-org',
fullPath: 'gitlab-org',
epic: {
id: 1,
iid: 1,
title: 'Foo bar',
webPath: '/groups/gitlab-org/-/epics/1',
userPermissions: {
adminEpic: true,
createEpic: true,
},
children: {
edges: [
{
node: mockEpic1,
},
{
node: mockEpic2,
},
],
pageInfo: {
endCursor: 'abc',
hasNextPage: true,
},
},
issues: {
edges: [
{
node: mockIssue3,
},
{
node: mockIssue1,
},
{
node: mockIssue2,
},
],
pageInfo: {
endCursor: 'def',
hasNextPage: true,
},
},
},
},
},
};
export const mockQueryResponse2 = {
data: {
group: {
id: 1,
path: 'gitlab-org',
fullPath: 'gitlab-org',
epic: {
id: 1,
iid: 1,
title: 'Foo bar',
webPath: '/groups/gitlab-org/-/epics/1',
userPermissions: {
adminEpic: true,
createEpic: true,
},
children: {
edges: [
{
node: mockEpic1,
},
{
node: mockEpic2,
},
],
pageInfo: {
endCursor: 'abc',
hasNextPage: true,
},
},
issues: {
edges: [
{
node: mockIssue3,
},
{
node: mockIssue1,
},
{
node: mockIssue2,
},
],
pageInfo: {
endCursor: 'def',
hasNextPage: true,
},
},
},
},
},
};
export const mockReorderMutationResponse = {
epicTreeReorder: {
clientMutationId: null,
......
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