summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Friend <nathan@gitlab.com>2019-09-12 09:01:30 -0300
committerNathan Friend <nathan@gitlab.com>2019-09-12 09:03:06 -0300
commit0a8a9e46afcbe3821ae26731f80aaeff91b4be7a (patch)
tree144da22d2f15fed69481e4995a2ed7e78235cefe
parent1928932388f063b064dde9d235b6474121a726c0 (diff)
downloadgitlab-ce-nfriend-add-milestones-to-release-blocks-2.tar.gz
Update release blocks to support association of milestonesnfriend-add-milestones-to-release-blocks-2
This commit updates the release blocks that appear on the releases page to show the list of milestones associated with the release (if any).
-rw-r--r--app/assets/javascripts/releases/components/milestone_list.vue45
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue22
-rw-r--r--changelogs/unreleased/nfriend-add-milestones-to-release-blocks-2.yml5
-rw-r--r--spec/frontend/releases/components/milestone_list_spec.js56
-rw-r--r--spec/frontend/releases/components/release_block_spec.js120
-rw-r--r--spec/frontend/releases/mock_data.js97
-rw-r--r--spec/javascripts/releases/components/release_block_spec.js168
7 files changed, 345 insertions, 168 deletions
diff --git a/app/assets/javascripts/releases/components/milestone_list.vue b/app/assets/javascripts/releases/components/milestone_list.vue
new file mode 100644
index 00000000000..53416f0ab4d
--- /dev/null
+++ b/app/assets/javascripts/releases/components/milestone_list.vue
@@ -0,0 +1,45 @@
+<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>
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index 88b6b4732b1..2dacd8549ad 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -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)">
diff --git a/changelogs/unreleased/nfriend-add-milestones-to-release-blocks-2.yml b/changelogs/unreleased/nfriend-add-milestones-to-release-blocks-2.yml
new file mode 100644
index 00000000000..6a705256256
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-milestones-to-release-blocks-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update release blocks to support association of milestones
+merge_request: 32765
+author:
+type: added
diff --git a/spec/frontend/releases/components/milestone_list_spec.js b/spec/frontend/releases/components/milestone_list_spec.js
new file mode 100644
index 00000000000..f267177ddab
--- /dev/null
+++ b/spec/frontend/releases/components/milestone_list_spec.js
@@ -0,0 +1,56 @@
+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);
+ });
+});
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
new file mode 100644
index 00000000000..4be5d500fd9
--- /dev/null
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -0,0 +1,120 @@
+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');
+ });
+});
diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js
new file mode 100644
index 00000000000..a0885813c7e
--- /dev/null
+++ b/spec/frontend/releases/mock_data.js
@@ -0,0 +1,97 @@
+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,
+ },
+ ],
+ },
+};
diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js
deleted file mode 100644
index fdf23f3f69d..00000000000
--- a/spec/javascripts/releases/components/release_block_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-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');
- });
- });
-});