Commit 203f1f5c authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '29020-update-release-blocks-for-multiple-milestone-support' into 'master'

Update release blocks to support multiple milestones

See merge request gitlab-org/gitlab!17091
parents 1c2a6c5b 8c5ad4e8
<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
......@@ -9990,7 +9990,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