Commit d2f49772 authored by Mike Greiling's avatar Mike Greiling

Merge branch 'nfriend-add-milestones-to-release-blocks-3' into 'master'

Add milestones to release blocks

See merge request gitlab-org/gitlab!16562
parents 6d4caf19 1bf6fdc4
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
export default {
name: 'MilestoneList',
components: {
GlLink,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
milestones: {
type: Array,
required: true,
},
},
computed: {
labelText() {
return this.milestones.length === 1 ? s__('Milestone') : s__('Milestones');
},
},
};
</script>
<template>
<div>
<icon name="flag" class="align-middle" /> <span class="js-label-text">{{ labelText }}</span>
<template v-for="(milestone, index) in milestones">
<gl-link
:key="milestone.id"
v-gl-tooltip
:title="milestone.description"
:href="milestone.web_url"
>
{{ milestone.title }}
</gl-link>
<template v-if="index !== milestones.length - 1">
&bull;
</template>
</template>
</div>
</template>
......@@ -5,6 +5,7 @@ import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import MilestoneList from './milestone_list.vue';
import { __, sprintf } from '../../locale';
export default {
......@@ -14,6 +15,7 @@ export default {
GlBadge,
Icon,
UserAvatarLink,
MilestoneList,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -49,6 +51,20 @@ export default {
hasAuthor() {
return !_.isEmpty(this.author);
},
milestones() {
// At the moment, a release can only be associated to
// one milestone. This will be expanded to be many-to-many
// in the near future, so we pass the milestone as an
// array here in anticipation of this change.
return [this.release.milestone];
},
shouldRenderMilestones() {
// Similar to the `milestones` computed above,
// this check will need to be updated once
// the API begins sending an array of milestones
// instead of just a single object.
return Boolean(this.release.milestone);
},
},
};
</script>
......@@ -73,6 +89,12 @@ export default {
<span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
</div>
<milestone-list
v-if="shouldRenderMilestones"
class="append-right-4 js-milestone-list"
:milestones="milestones"
/>
<div class="append-right-4">
&bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
......
---
title: Update release blocks to support association of milestones
merge_request: 16562
author:
type: added
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import MilestoneList from '~/releases/components/milestone_list.vue';
import Icon from '~/vue_shared/components/icon.vue';
import _ from 'underscore';
import { milestones } from '../mock_data';
describe('Milestone list', () => {
let wrapper;
const factory = milestonesProp => {
wrapper = shallowMount(MilestoneList, {
propsData: {
milestones: milestonesProp,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders the milestone icon', () => {
factory(milestones);
expect(wrapper.find(Icon).exists()).toBe(true);
});
it('renders the label as "Milestone" if only a single milestone is passed in', () => {
factory(milestones.slice(0, 1));
expect(wrapper.find('.js-label-text').text()).toEqual('Milestone');
});
it('renders the label as "Milestones" if more than one milestone is passed in', () => {
factory(milestones);
expect(wrapper.find('.js-label-text').text()).toEqual('Milestones');
});
it('renders a link to the milestone with a tooltip', () => {
const milestone = _.first(milestones);
factory([milestone]);
const milestoneLink = wrapper.find(GlLink);
expect(milestoneLink.exists()).toBe(true);
expect(milestoneLink.text()).toBe(milestone.title);
expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
});
});
import { mount } from '@vue/test-utils';
import ReleaseBlock from '~/releases/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore';
import { release } from '../mock_data';
describe('Release block', () => {
let wrapper;
const factory = releaseProp => {
wrapper = mount(ReleaseBlock, {
propsData: {
release: releaseProp,
},
sync: false,
});
};
const milestoneListExists = () => wrapper.find('.js-milestone-list').exists();
afterEach(() => {
wrapper.destroy();
});
describe('with default props', () => {
beforeEach(() => {
factory(release);
});
it("renders the block with an id equal to the release's tag name", () => {
expect(wrapper.attributes().id).toBe('v0.3');
});
it('renders release name', () => {
expect(wrapper.text()).toContain(release.name);
});
it('renders commit sha', () => {
expect(wrapper.text()).toContain(release.commit.short_id);
});
it('renders tag name', () => {
expect(wrapper.text()).toContain(release.tag_name);
});
it('renders release date', () => {
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormated(release.released_at));
});
it('renders number of assets provided', () => {
expect(wrapper.find('.js-assets-count').text()).toContain(release.assets.count);
});
it('renders dropdown with the sources', () => {
expect(wrapper.findAll('.js-sources-dropdown li').length).toEqual(
release.assets.sources.length,
);
expect(wrapper.find('.js-sources-dropdown li a').attributes().href).toEqual(
first(release.assets.sources).url,
);
expect(wrapper.find('.js-sources-dropdown li a').text()).toContain(
first(release.assets.sources).format,
);
});
it('renders list with the links provided', () => {
expect(wrapper.findAll('.js-assets-list li').length).toEqual(release.assets.links.length);
expect(wrapper.find('.js-assets-list li a').attributes().href).toEqual(
first(release.assets.links).url,
);
expect(wrapper.find('.js-assets-list li a').text()).toContain(
first(release.assets.links).name,
);
});
it('renders author avatar', () => {
expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
});
describe('external label', () => {
it('renders external label when link is external', () => {
expect(wrapper.find('.js-assets-list li a').text()).toContain('external source');
});
it('does not render external label when link is not external', () => {
expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain(
'external source',
);
});
});
it('renders the milestone list if at least one milestone is associated to the release', () => {
factory(release);
expect(milestoneListExists()).toBe(true);
});
});
it('does not render the milestone list if no milestones are associated to the release', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
delete releaseClone.milestone;
factory(releaseClone);
expect(milestoneListExists()).toBe(false);
});
it('renders upcoming release badge', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
releaseClone.upcoming_release = true;
factory(releaseClone);
expect(wrapper.text()).toContain('Upcoming Release');
});
});
export const milestones = [
{
id: 50,
iid: 2,
project_id: 18,
title: '13.6',
description: 'The 13.6 milestone!',
state: 'active',
created_at: '2019-08-27T17:22:38.280Z',
updated_at: '2019-08-27T17:22:38.280Z',
due_date: '2019-09-19',
start_date: '2019-08-31',
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/2',
},
{
id: 49,
iid: 1,
project_id: 18,
title: '13.5',
description: 'The 13.5 milestone!',
state: 'active',
created_at: '2019-08-26T17:55:48.643Z',
updated_at: '2019-08-26T17:55:48.643Z',
due_date: '2019-10-11',
start_date: '2019-08-19',
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/1',
},
];
export const release = {
name: 'New release',
tag_name: 'v0.3',
description: 'A super nice release!',
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z',
released_at: '2019-08-26T17:54:04.807Z',
author: {
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'http://0.0.0.0:3001/root',
},
commit: {
id: 'c22b0728d1b465f82898c884d32b01aa642f96c1',
short_id: 'c22b0728',
created_at: '2019-08-26T17:47:07.000Z',
parent_ids: [],
title: 'Initial commit',
message: 'Initial commit',
author_name: 'Administrator',
author_email: 'admin@example.com',
authored_date: '2019-08-26T17:47:07.000Z',
committer_name: 'Administrator',
committer_email: 'admin@example.com',
committed_date: '2019-08-26T17:47:07.000Z',
},
upcoming_release: false,
milestone: milestones[0],
assets: {
count: 5,
sources: [
{
format: 'zip',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip',
},
{
format: 'tar.gz',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz',
},
{
format: 'tar.bz2',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2',
},
{
format: 'tar',
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar',
},
],
links: [
{
id: 1,
name: 'my link',
url: 'https://google.com',
external: true,
},
{
id: 2,
name: 'my second link',
url:
'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
};
import Vue from 'vue';
import component from '~/releases/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Release block', () => {
const Component = Vue.extend(component);
const release = {
name: 'Bionic Beaver',
tag_name: '18.04',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
released_at: '2012-05-28T05:00:00-07:00',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author_name: 'John Smith',
author_email: 'john@example.com',
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets: {
count: 6,
sources: [
{
format: 'zip',
url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
},
{
format: 'tar.gz',
url:
'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
},
{
format: 'tar.bz2',
url:
'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
},
{
format: 'tar',
url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
},
],
links: [
{
name: 'release-18.04.dmg',
url: 'https://my-external-hosting.example.com/scrambled-url/',
external: true,
},
{
name: 'binary-linux-amd64',
url:
'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
};
let vm;
const factory = props => mountComponent(Component, { release: props });
beforeEach(() => {
vm = factory(release);
});
afterEach(() => {
vm.$destroy();
});
it("renders the block with an id equal to the release's tag name", () => {
expect(vm.$el.id).toBe('18.04');
});
it('renders release name', () => {
expect(vm.$el.textContent).toContain(release.name);
});
it('renders commit sha', () => {
expect(vm.$el.textContent).toContain(release.commit.short_id);
});
it('renders tag name', () => {
expect(vm.$el.textContent).toContain(release.tag_name);
});
it('renders release date', () => {
expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at));
});
it('renders number of assets provided', () => {
expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count);
});
it('renders dropdown with the sources', () => {
expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual(
release.assets.sources.length,
);
expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual(
release.assets.sources[0].url,
);
expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain(
release.assets.sources[0].format,
);
});
it('renders list with the links provided', () => {
expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual(
release.assets.links.length,
);
expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual(
release.assets.links[0].url,
);
expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain(
release.assets.links[0].name,
);
});
it('renders author avatar', () => {
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
});
describe('external label', () => {
it('renders external label when link is external', () => {
expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source');
});
it('does not render external label when link is not external', () => {
expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain(
'external source',
);
});
});
describe('with upcoming_release flag', () => {
beforeEach(() => {
vm = factory(Object.assign({}, release, { upcoming_release: true }));
});
it('renders upcoming release badge', () => {
expect(vm.$el.textContent).toContain('Upcoming Release');
});
});
});
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