Update release blocks to support association of milestones

This commit updates the release blocks that appear on the releases page
to show the list of milestones associated with the release (if any).
上级 19289323
<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: 32765
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% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册