Commit 8c5ad4e8 authored by Nathan Friend's avatar Nathan Friend

Update release block to support multiple milestones

This commit updates the release blocks on the Releases page to support
the association of multiple milestones to a release.
parent 40cfd00d
<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,8 +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';
import { __, n__, sprintf } from '../../locale';
export default {
name: 'ReleaseBlock',
......@@ -15,7 +14,6 @@ export default {
GlBadge,
Icon,
UserAvatarLink,
MilestoneList,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -57,19 +55,11 @@ 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);
return !_.isEmpty(this.release.milestones);
},
labelText() {
return n__('Milestone', 'Milestones', this.release.milestones.length);
},
},
};
......@@ -101,11 +91,27 @@ export default {
<span v-else 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"
/>
<template v-if="shouldRenderMilestones">
<div class="js-milestone-list-label">
<icon name="flag" class="align-middle" />
<span class="js-label-text">{{ labelText }}</span>
</div>
<template v-for="(milestone, index) in release.milestones">
<gl-link
:key="milestone.id"
v-gl-tooltip
:title="milestone.description"
:href="milestone.web_url"
class="append-right-4 prepend-left-4 js-milestone-link"
>
{{ milestone.title }}
</gl-link>
<template v-if="index !== release.milestones.length - 1">
&bull;
</template>
</template>
</template>
<div class="append-right-4">
&bull;
......
---
title: Add support for the association of multiple milestones to the Releases page
merge_request: 17091
author:
type: changed
......@@ -9973,7 +9973,9 @@ msgid "Migration successful."
msgstr ""
msgid "Milestone"
msgstr ""
msgid_plural "Milestones"
msgstr[0] ""
msgstr[1] ""
msgid "Milestone lists not available with your current license"
msgstr ""
......
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);
});
});
......@@ -3,6 +3,7 @@ import ReleaseBlock from '~/releases/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore';
import { release } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
describe('Release block', () => {
let wrapper;
......@@ -15,7 +16,7 @@ describe('Release block', () => {
});
};
const milestoneListExists = () => wrapper.find('.js-milestone-list').exists();
const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
afterEach(() => {
wrapper.destroy();
......@@ -98,20 +99,56 @@ describe('Release block', () => {
});
});
it('renders the milestone list if at least one milestone is associated to the release', () => {
factory(release);
it('renders the milestone icon', () => {
expect(
milestoneListLabel()
.find(Icon)
.exists(),
).toBe(true);
});
it('renders the label as "Milestones" if more than one milestone is passed in', () => {
expect(
milestoneListLabel()
.find('.js-label-text')
.text(),
).toEqual('Milestones');
});
it('renders a link to the milestone with a tooltip', () => {
const milestone = first(release.milestones);
const milestoneLink = wrapper.find('.js-milestone-link');
expect(milestoneLink.exists()).toBe(true);
expect(milestoneLink.text()).toBe(milestone.title);
expect(milestoneListExists()).toBe(true);
expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
});
});
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;
delete releaseClone.milestones;
factory(releaseClone);
expect(milestoneListLabel().exists()).toBe(false);
});
it('renders the label as "Milestone" if only a single milestone is passed in', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
releaseClone.milestones = releaseClone.milestones.slice(0, 1);
factory(releaseClone);
expect(milestoneListExists()).toBe(false);
expect(
milestoneListLabel()
.find('.js-label-text')
.text(),
).toEqual('Milestone');
});
it('renders upcoming release badge', () => {
......
......@@ -57,7 +57,7 @@ export const release = {
committed_date: '2019-08-26T17:47:07.000Z',
},
upcoming_release: false,
milestone: milestones[0],
milestones,
assets: {
count: 5,
sources: [
......
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