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 0000000000000000000000000000000000000000..53416f0ab4d884e72aa3d7b606171ebde3a9cafb --- /dev/null +++ b/app/assets/javascripts/releases/components/milestone_list.vue @@ -0,0 +1,45 @@ + + diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 88b6b4732b1c1323bcbae643648bd47d4b804a29..2dacd8549ade478e8663bacc9ad46181a2a41740 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); + }, }, }; @@ -73,6 +89,12 @@ export default { {{ release.tag_name }} + +
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 0000000000000000000000000000000000000000..6a7052562560cb1814c1acd74886f2218ccea309 --- /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 0000000000000000000000000000000000000000..f267177ddab37f517a849fef7348f24e286ffaeb --- /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 0000000000000000000000000000000000000000..4be5d500fd96515d5c18f15594c9caec475f285f --- /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 0000000000000000000000000000000000000000..a0885813c7efdf088e76fa1f86bc998d532aea45 --- /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: '

A super nice release!

', + 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 fdf23f3f69dff8b3896145c9bb70438eb13626a2..0000000000000000000000000000000000000000 --- 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: '

changelog

  • line1line 2
', - 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'); - }); - }); -});