Commit 2dd6e24c authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'iteration-lists-frontend' into 'master'

Iteration lists frontend

See merge request gitlab-org/gitlab!52178
parents 406642f7 d1d2eb67
......@@ -86,16 +86,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return (
this.listType === ListType.milestone &&
this.list.milestone &&
(!this.list.collapsed || !this.isSwimlanesHeader)
);
return this.listType === ListType.milestone && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
return (
this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader)
);
return this.listType === ListType.assignee && this.showListDetails;
},
showIterationListDetails() {
return this.listType === ListType.iteration && this.showListDetails;
},
showListDetails() {
return !this.list.collapsed || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesCount;
......@@ -218,6 +218,17 @@ export default {
<gl-icon name="timer" />
</span>
<span
v-if="showIterationListDetails"
aria-hidden="true"
:class="{
'gl-mt-3 gl-rotate-90': list.collapsed,
'gl-mr-2': !list.collapsed,
}"
>
<gl-icon name="iteration" />
</span>
<a
v-if="showAssigneeListDetails"
:href="list.assignee.webUrl"
......
......@@ -78,14 +78,16 @@ export default {
return !this.disabled && this.listType !== ListType.closed;
},
showMilestoneListDetails() {
return (
this.list.type === 'milestone' &&
this.list.milestone &&
(this.list.isExpanded || !this.isSwimlanesHeader)
);
return this.list.type === 'milestone' && this.list.milestone && this.showListDetails;
},
showAssigneeListDetails() {
return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
return this.list.type === 'assignee' && this.showListDetails;
},
showIterationListDetails() {
return this.listType === ListType.iteration && this.showListDetails;
},
showListDetails() {
return this.list.isExpanded || !this.isSwimlanesHeader;
},
issuesCount() {
return this.list.issuesSize;
......@@ -203,6 +205,17 @@ export default {
<gl-icon name="timer" />
</span>
<span
v-if="showIterationListDetails"
aria-hidden="true"
:class="{
'gl-mt-3 gl-rotate-90': !list.isExpanded,
'gl-mr-2': list.isExpanded,
}"
>
<gl-icon name="iteration" />
</span>
<a
v-if="showAssigneeListDetails"
:href="list.assignee.path"
......
......@@ -5,17 +5,13 @@ import { __ } from '~/locale';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/sidebar/event_hub';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { LIST } from '~/boards/constants';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
assignee: 'assignee',
milestone: 'milestone',
label: 'label',
labelListText: __('Label'),
components: {
GlButton,
GlDrawer,
......@@ -33,6 +29,11 @@ export default {
default: false,
},
},
data() {
return {
ListType,
};
},
computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
......@@ -56,7 +57,7 @@ export default {
return this.activeList.type || this.activeList.listType || null;
},
listTypeTitle() {
return this.$options.labelListText;
return ListTypeTitles[ListType.label];
},
showSidebar() {
return this.sidebarType === LIST;
......@@ -98,7 +99,7 @@ export default {
>
<template #header>{{ $options.listSettingsText }}</template>
<template v-if="isSidebarOpen">
<div v-if="boardListType === $options.label">
<div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-label
:title="activeListLabel.title"
......
import { __ } from '~/locale';
export const BoardType = {
project: 'project',
group: 'group',
......@@ -6,11 +8,19 @@ export const BoardType = {
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
iteration: 'iteration',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
export const ListTypeTitles = {
assignee: __('Assignee'),
milestone: __('Milestone'),
iteration: __('Iteration'),
label: __('Label'),
};
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
......
export default class ListIteration {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
this.description = obj.description;
}
}
......@@ -5,6 +5,7 @@ import boardsStore from '../stores/boards_store';
import ListLabel from './label';
import ListAssignee from './assignee';
import ListMilestone from './milestone';
import ListIteration from './iteration';
import 'ee_else_ce/boards/models/issue';
const TYPES = {
......@@ -57,6 +58,9 @@ class List {
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.title = this.milestone.title;
} else if (IS_EE && obj.iteration) {
this.iteration = new ListIteration(obj.iteration);
this.title = this.iteration.title;
}
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
......
......@@ -49,7 +49,8 @@ Example response:
"created_at": "2020-01-27T05:07:12.573Z",
"updated_at": "2020-01-27T05:07:12.573Z",
"due_date": "2020-02-01",
"start_date": "2020-02-14"
"start_date": "2020-02-14",
"web_url": "http://gitlab.example.com/groups/my-group/-/iterations/13"
}
]
```
......@@ -51,7 +51,8 @@ Example response:
"created_at": "2020-01-27T05:07:12.573Z",
"updated_at": "2020-01-27T05:07:12.573Z",
"due_date": "2020-02-01",
"start_date": "2020-02-14"
"start_date": "2020-02-14",
"web_url": "http://gitlab.example.com/groups/my-group/-/iterations/13"
}
]
```
<script>
import { GlAvatarLink, GlAvatarLabeled, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import { ListType, ListTypeTitles } from '~/boards/constants';
export default {
milestone: 'milestone',
assignee: 'assignee',
labelMilestoneText: __('Milestone'),
labelAssigneeText: __('Assignee'),
components: {
GlLink,
GlAvatarLink,
......@@ -22,25 +18,17 @@ export default {
required: true,
},
},
computed: {
activeListAssignee() {
return this.activeList.assignee;
data() {
return {
ListType,
};
},
activeListMilestone() {
return this.activeList.milestone;
computed: {
activeListObject() {
return this.activeList[this.boardListType];
},
listTypeTitle() {
switch (this.boardListType) {
case this.$options.milestone: {
return this.$options.labelMilestoneText;
}
case this.$options.assignee: {
return this.$options.labelAssigneeText;
}
default: {
return '';
}
}
listTypeHeader() {
return ListTypeTitles[this.boardListType] || '';
},
},
};
......@@ -48,24 +36,21 @@ export default {
<template>
<div>
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
<gl-link
v-if="boardListType === $options.milestone"
class="js-milestone"
:href="activeListMilestone.webUrl"
>{{ activeListMilestone.title }}</gl-link
>
<label class="js-list-label gl-display-block">{{ listTypeHeader }}</label>
<gl-avatar-link
v-else-if="boardListType === $options.assignee"
v-if="boardListType === ListType.assignee"
class="js-assignee"
:href="activeListAssignee.webUrl"
:href="activeListObject.webUrl"
>
<gl-avatar-labeled
:size="32"
:label="activeListAssignee.name"
:sub-label="`@${activeListAssignee.username}`"
:src="activeListAssignee.avatar"
:label="activeListObject.name"
:sub-label="`@${activeListObject.username}`"
:src="activeListObject.avatar"
/>
</gl-avatar-link>
<gl-link v-else class="js-list-title" :href="activeListObject.webUrl">
{{ activeListObject.title }}
</gl-link>
</div>
</template>
---
title: Add web_url to iterations API
merge_request: 52178
author:
type: added
......@@ -10,6 +10,10 @@ module API
expose :state_enum, as: :state
expose :created_at, :updated_at
expose :start_date, :due_date
expose :web_url do |iteration, _options|
Gitlab::UrlBuilder.build(iteration)
end
end
end
end
......@@ -54,7 +54,8 @@
"id",
"title",
"description",
"state"
"state",
"web_url"
],
"properties": {
"id": {
......@@ -71,6 +72,9 @@
},
"state": {
"type": "integer"
},
"web_url": {
"type": "string"
}
}
}
......
......@@ -81,7 +81,7 @@ describe('Board List Header Component', () => {
const findSettingsButton = () => wrapper.find({ ref: 'settingsBtn' });
describe('Settings Button', () => {
const hasSettings = [ListType.assignee, ListType.milestone, ListType.label];
const hasSettings = [ListType.assignee, ListType.milestone, ListType.iteration, ListType.label];
const hasNoSettings = [ListType.backlog, ListType.closed];
it.each(hasSettings)('does render for List Type `%s`', (listType) => {
......
......@@ -72,7 +72,7 @@ describe('Board List Header Component', () => {
const findSettingsButton = () => wrapper.find({ ref: 'settingsBtn' });
describe('Settings Button', () => {
const hasSettings = [ListType.assignee, ListType.milestone, ListType.label];
const hasSettings = [ListType.assignee, ListType.milestone, ListType.iteration, ListType.label];
const hasNoSettings = [ListType.backlog, ListType.closed];
it.each(hasSettings)('does render for List Type `%s`', (listType) => {
......
......@@ -9,6 +9,10 @@ describe('BoardSettingsListType', () => {
webUrl: 'https://gitlab.com/h5bp/html5-boilerplate/-/milestones/1',
title: 'Backlog',
},
iteration: {
webUrl: 'https://gitlab.com/h5bp/-/iterations/1',
title: 'Sprint 1',
},
assignee: { webUrl: 'https://gitlab.com/root', name: 'root', username: 'root' },
};
const createComponent = (props) => {
......@@ -25,7 +29,7 @@ describe('BoardSettingsListType', () => {
it('renders the correct milestone text', () => {
createComponent({ activeId: 1, boardListType: 'milestone' });
expect(wrapper.find('.js-milestone').text()).toBe('Backlog');
expect(wrapper.find('.js-list-title').text()).toBe('Backlog');
});
it('renders the correct list type text', () => {
......@@ -35,6 +39,20 @@ describe('BoardSettingsListType', () => {
});
});
describe('when list type is "iteration"', () => {
it('renders the correct milestone text', () => {
createComponent({ activeId: 1, boardListType: 'iteration' });
expect(wrapper.find('.js-list-title').text()).toBe('Sprint 1');
});
it('renders the correct list type text', () => {
createComponent({ activeId: 1, boardListType: 'iteration' });
expect(wrapper.find('.js-list-label').text()).toBe('Iteration');
});
});
describe('when list type is "assignee"', () => {
afterEach(() => {
wrapper.destroy();
......
......@@ -4,6 +4,7 @@ import Issue from 'ee/boards/models/issue';
import List from 'ee/boards/models/list';
import { listObj } from 'jest/boards/mock_data';
import CeList from '~/boards/models/list';
import { ListType } from '~/boards/constants';
describe('List model', () => {
let list;
......@@ -15,7 +16,16 @@ describe('List model', () => {
// We need to mock axios since `new List` below makes a network request
axiosMock.onGet().replyOnce(200);
});
afterEach(() => {
list = null;
issue = null;
axiosMock.restore();
});
describe('label lists', () => {
beforeEach(() => {
list = new List(listObj);
issue = new Issue({
title: 'Testing',
......@@ -27,12 +37,6 @@ describe('List model', () => {
});
});
afterEach(() => {
list = null;
issue = null;
axiosMock.restore();
});
it('inits totalWeight', () => {
expect(list.totalWeight).toBe(0);
});
......@@ -96,4 +100,22 @@ describe('List model', () => {
expect(ceRemoveIssue).toHaveBeenCalledWith(issue);
});
});
});
describe('iteration lists', () => {
const iteration = {
id: 1000,
title: 'Sprint 1',
webUrl: 'https://gitlab.com/h5bp/-/iterations/1',
};
beforeEach(() => {
list = new List({ list_type: ListType.iteration, iteration });
});
it('sets the iteration and title', () => {
expect(list.iteration.id).toBe(iteration.id);
expect(list.title).toBe(iteration.title);
});
});
});
......@@ -74,7 +74,13 @@ describe('Board List Header Component', () => {
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
const hasAddButton = [
ListType.backlog,
ListType.label,
ListType.milestone,
ListType.iteration,
ListType.assignee,
];
it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
createComponent({ listType });
......
......@@ -78,7 +78,13 @@ describe('Board List Header Component', () => {
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
const hasAddButton = [
ListType.backlog,
ListType.label,
ListType.milestone,
ListType.iteration,
ListType.assignee,
];
it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
createComponent({ listType });
......@@ -167,7 +173,7 @@ describe('Board List Header Component', () => {
describe('user can drag', () => {
const cannotDragList = [ListType.backlog, ListType.closed];
const canDragList = [ListType.label, ListType.milestone, ListType.assignee];
const canDragList = [ListType.label, ListType.milestone, ListType.iteration, ListType.assignee];
it.each(cannotDragList)(
'does not have user-can-drag-class so user cannot drag list',
......
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