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'; ...@@ -5,8 +5,7 @@ import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import MilestoneList from './milestone_list.vue'; import { __, n__, sprintf } from '../../locale';
import { __, sprintf } from '../../locale';
export default { export default {
name: 'ReleaseBlock', name: 'ReleaseBlock',
...@@ -15,7 +14,6 @@ export default { ...@@ -15,7 +14,6 @@ export default {
GlBadge, GlBadge,
Icon, Icon,
UserAvatarLink, UserAvatarLink,
MilestoneList,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -57,19 +55,11 @@ export default { ...@@ -57,19 +55,11 @@ export default {
hasAuthor() { hasAuthor() {
return !_.isEmpty(this.author); 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() { shouldRenderMilestones() {
// Similar to the `milestones` computed above, return !_.isEmpty(this.release.milestones);
// this check will need to be updated once },
// the API begins sending an array of milestones labelText() {
// instead of just a single object. return n__('Milestone', 'Milestones', this.release.milestones.length);
return Boolean(this.release.milestone);
}, },
}, },
}; };
...@@ -101,11 +91,27 @@ export default { ...@@ -101,11 +91,27 @@ export default {
<span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span> <span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
</div> </div>
<milestone-list <template v-if="shouldRenderMilestones">
v-if="shouldRenderMilestones" <div class="js-milestone-list-label">
class="append-right-4 js-milestone-list" <icon name="flag" class="align-middle" />
:milestones="milestones" <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"> <div class="append-right-4">
&bull; &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." ...@@ -9990,7 +9990,9 @@ msgid "Migration successful."
msgstr "" msgstr ""
msgid "Milestone" msgid "Milestone"
msgstr "" msgid_plural "Milestones"
msgstr[0] ""
msgstr[1] ""
msgid "Milestone lists not available with your current license" msgid "Milestone lists not available with your current license"
msgstr "" 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'; ...@@ -3,6 +3,7 @@ import ReleaseBlock from '~/releases/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore'; import { first } from 'underscore';
import { release } from '../mock_data'; import { release } from '../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
describe('Release block', () => { describe('Release block', () => {
let wrapper; let wrapper;
...@@ -15,7 +16,7 @@ describe('Release block', () => { ...@@ -15,7 +16,7 @@ describe('Release block', () => {
}); });
}; };
const milestoneListExists = () => wrapper.find('.js-milestone-list').exists(); const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -98,20 +99,56 @@ describe('Release block', () => { ...@@ -98,20 +99,56 @@ describe('Release block', () => {
}); });
}); });
it('renders the milestone list if at least one milestone is associated to the release', () => { it('renders the milestone icon', () => {
factory(release); 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', () => { it('does not render the milestone list if no milestones are associated to the release', () => {
const releaseClone = JSON.parse(JSON.stringify(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); factory(releaseClone);
expect(milestoneListExists()).toBe(false); expect(
milestoneListLabel()
.find('.js-label-text')
.text(),
).toEqual('Milestone');
}); });
it('renders upcoming release badge', () => { it('renders upcoming release badge', () => {
......
...@@ -57,7 +57,7 @@ export const release = { ...@@ -57,7 +57,7 @@ export const release = {
committed_date: '2019-08-26T17:47:07.000Z', committed_date: '2019-08-26T17:47:07.000Z',
}, },
upcoming_release: false, upcoming_release: false,
milestone: milestones[0], milestones,
assets: { assets: {
count: 5, count: 5,
sources: [ 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