Commit 9733f3cc authored by Nathan Friend's avatar Nathan Friend

Add footer to release blocks

This commit adds a footer to each release block on the Releases page.
parent 485c10ab
...@@ -10,6 +10,7 @@ import { slugify } from '~/lib/utils/text_utility'; ...@@ -10,6 +10,7 @@ import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility'; import { getLocationHash } from '~/lib/utils/url_utility';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReleaseBlockFooter from './release_block_footer.vue';
export default { export default {
name: 'ReleaseBlock', name: 'ReleaseBlock',
...@@ -19,6 +20,7 @@ export default { ...@@ -19,6 +20,7 @@ export default {
GlButton, GlButton,
Icon, Icon,
UserAvatarLink, UserAvatarLink,
ReleaseBlockFooter,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -79,6 +81,9 @@ export default { ...@@ -79,6 +81,9 @@ export default {
this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url, this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url,
); );
}, },
shouldShowFooter() {
return this.glFeatures.releaseIssueSummary;
},
}, },
mounted() { mounted() {
const hash = getLocationHash(); const hash = getLocationHash();
...@@ -164,7 +169,7 @@ export default { ...@@ -164,7 +169,7 @@ export default {
by by
<user-avatar-link <user-avatar-link
class="prepend-left-4" class="prepend-left-4"
:link-href="author.path" :link-href="author.web_url"
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="userImageAltDescription" :img-alt="userImageAltDescription"
:tooltip-text="author.username" :tooltip-text="author.username"
...@@ -216,5 +221,16 @@ export default { ...@@ -216,5 +221,16 @@ export default {
<div v-html="release.description_html"></div> <div v-html="release.description_html"></div>
</div> </div>
</div> </div>
<release-block-footer
v-if="shouldShowFooter"
class="card-footer"
:commit="release.commit"
:commit-path="release.commit_path"
:tag-name="release.tag_name"
:tag-path="release.tag_path"
:author="release.author"
:released-at="release.released_at"
/>
</div> </div>
</template> </template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { __, sprintf } from '~/locale';
export default {
name: 'ReleaseBlockFooter',
components: {
Icon,
GlLink,
UserAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
commit: {
type: Object,
required: false,
default: null,
},
commitPath: {
type: String,
required: false,
default: '',
},
tagName: {
type: String,
required: false,
default: '',
},
tagPath: {
type: String,
required: false,
default: '',
},
author: {
type: Object,
required: false,
default: null,
},
releasedAt: {
type: String,
required: false,
default: '',
},
},
computed: {
releasedAtTimeAgo() {
return this.timeFormated(this.releasedAt);
},
userImageAltDescription() {
return this.author && this.author.username
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
},
};
</script>
<template>
<div>
<div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info">
<icon ref="commitIcon" name="commit" class="mr-1" />
<div v-gl-tooltip.bottom :title="commit.title">
<gl-link v-if="commitPath" :href="commitPath">
{{ commit.short_id }}
</gl-link>
<span v-else>{{ commit.short_id }}</span>
</div>
</div>
<div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info">
<icon name="tag" class="mr-1" />
<div v-gl-tooltip.bottom :title="__('Tag')">
<gl-link v-if="tagPath" :href="tagPath">
{{ tagName }}
</gl-link>
<span v-else>{{ tagName }}</span>
</div>
</div>
<div
v-if="releasedAt || author"
class="float-left d-flex align-items-center js-author-date-info"
>
<span class="text-secondary">{{ __('Created') }}&nbsp;</span>
<template v-if="releasedAt">
<span
v-gl-tooltip.bottom
:title="tooltipTitle(releasedAt)"
class="text-secondary flex-shrink-0"
>
{{ releasedAtTimeAgo }}&nbsp;
</span>
</template>
<div v-if="author" class="d-flex">
<span class="text-secondary">{{ __('by') }}&nbsp;</span>
<user-avatar-link
:link-href="author.web_url"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
tooltip-placement="bottom"
/>
</div>
</div>
</div>
</template>
...@@ -7,6 +7,7 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :authorize_read_release! before_action :authorize_read_release!
before_action do before_action do
push_frontend_feature_flag(:release_edit_page, project) push_frontend_feature_flag(:release_edit_page, project)
push_frontend_feature_flag(:release_issue_summary, project)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
......
---
title: Move release meta-data into footer on Releases page
merge_request: 19451
author:
type: changed
import { mount } from '@vue/test-utils';
import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLink } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { release } from '../../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
jest.mock('~/vue_shared/mixins/timeago', () => ({
methods: {
timeFormated() {
return '7 fortnightes ago';
},
tooltipTitle() {
return 'February 30, 2401';
},
},
}));
describe('Release block footer', () => {
let wrapper;
let releaseClone;
const factory = (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
...convertObjectPropsToCamelCase(releaseClone),
...props,
},
sync: false,
});
return wrapper.vm.$nextTick();
};
beforeEach(() => {
releaseClone = JSON.parse(JSON.stringify(release));
});
afterEach(() => {
wrapper.destroy();
});
const commitInfoSection = () => wrapper.find('.js-commit-info');
const commitInfoSectionLink = () => commitInfoSection().find(GlLink);
const tagInfoSection = () => wrapper.find('.js-tag-info');
const tagInfoSectionLink = () => tagInfoSection().find(GlLink);
const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
describe('with all props provided', () => {
beforeEach(() => factory());
it('renders the commit icon', () => {
const commitIcon = commitInfoSection().find(Icon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('commit');
});
it('renders the commit SHA with a link', () => {
const commitLink = commitInfoSectionLink();
expect(commitLink.exists()).toBe(true);
expect(commitLink.text()).toBe(releaseClone.commit.short_id);
expect(commitLink.attributes('href')).toBe(releaseClone.commit_path);
});
it('renders the tag icon', () => {
const commitIcon = tagInfoSection().find(Icon);
expect(commitIcon.exists()).toBe(true);
expect(commitIcon.props('name')).toBe('tag');
});
it('renders the tag name with a link', () => {
const commitLink = tagInfoSection().find(GlLink);
expect(commitLink.exists()).toBe(true);
expect(commitLink.text()).toBe(releaseClone.tag_name);
expect(commitLink.attributes('href')).toBe(releaseClone.tag_path);
});
it('renders the author and creation time info', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
`Created 7 fortnightes ago by ${releaseClone.author.username}`,
);
});
it("renders the author's avatar image", () => {
const avatarImg = authorDateInfoSection().find('img');
expect(avatarImg.exists()).toBe(true);
expect(avatarImg.attributes('src')).toBe(releaseClone.author.avatar_url);
});
it("renders a link to the author's profile", () => {
const authorLink = authorDateInfoSection().find(GlLink);
expect(authorLink.exists()).toBe(true);
expect(authorLink.attributes('href')).toBe(releaseClone.author.web_url);
});
});
describe('without any commit info', () => {
beforeEach(() => factory({ commit: undefined }));
it('does not render any commit info', () => {
expect(commitInfoSection().exists()).toBe(false);
});
});
describe('without a commit URL', () => {
beforeEach(() => factory({ commitPath: undefined }));
it('renders the commit SHA as plain text (instead of a link)', () => {
expect(commitInfoSectionLink().exists()).toBe(false);
expect(commitInfoSection().text()).toBe(releaseClone.commit.short_id);
});
});
describe('without a tag name', () => {
beforeEach(() => factory({ tagName: undefined }));
it('does not render any tag info', () => {
expect(tagInfoSection().exists()).toBe(false);
});
});
describe('without a tag URL', () => {
beforeEach(() => factory({ tagPath: undefined }));
it('renders the tag name as plain text (instead of a link)', () => {
expect(tagInfoSectionLink().exists()).toBe(false);
expect(tagInfoSection().text()).toBe(releaseClone.tag_name);
});
});
describe('without any author info', () => {
beforeEach(() => factory({ author: undefined }));
it('renders the release date without the author name', () => {
expect(trimText(authorDateInfoSection().text())).toBe('Created 7 fortnightes ago');
});
});
describe('without a released at date', () => {
beforeEach(() => factory({ releasedAt: undefined }));
it('renders the author name without the release date', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
`Created by ${releaseClone.author.username}`,
);
});
});
describe('without a release date or author info', () => {
beforeEach(() => factory({ author: undefined, releasedAt: undefined }));
it('does not render any author or release date info', () => {
expect(authorDateInfoSection().exists()).toBe(false);
});
});
});
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import ReleaseBlock from '~/releases/list/components/release_block.vue'; import ReleaseBlock from '~/releases/list/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.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';
...@@ -21,14 +22,16 @@ describe('Release block', () => { ...@@ -21,14 +22,16 @@ describe('Release block', () => {
let wrapper; let wrapper;
let releaseClone; let releaseClone;
const factory = (releaseProp, releaseEditPageFeatureFlag = true) => { const factory = (releaseProp, featureFlags = {}) => {
wrapper = mount(ReleaseBlock, { wrapper = mount(ReleaseBlock, {
propsData: { propsData: {
release: releaseProp, release: releaseProp,
}, },
provide: { provide: {
glFeatures: { glFeatures: {
releaseEditPage: releaseEditPageFeatureFlag, releaseEditPage: true,
releaseIssueSummary: true,
...featureFlags,
}, },
}, },
sync: false, sync: false,
...@@ -142,6 +145,10 @@ describe('Release block', () => { ...@@ -142,6 +145,10 @@ describe('Release block', () => {
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description); expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
}); });
it('renders the footer', () => {
expect(wrapper.find(ReleaseBlockFooter).exists()).toBe(true);
});
}); });
it('renders commit sha', () => { it('renders commit sha', () => {
...@@ -173,7 +180,7 @@ describe('Release block', () => { ...@@ -173,7 +180,7 @@ describe('Release block', () => {
}); });
it('does not render an edit button if the releaseEditPage feature flag is disabled', () => it('does not render an edit button if the releaseEditPage feature flag is disabled', () =>
factory(releaseClone, false).then(() => { factory(releaseClone, { releaseEditPage: false }).then(() => {
expect(editButton().exists()).toBe(false); expect(editButton().exists()).toBe(false);
})); }));
......
...@@ -30,6 +30,7 @@ export const milestones = [ ...@@ -30,6 +30,7 @@ export const milestones = [
export const release = { export const release = {
name: 'New release', name: 'New release',
tag_name: 'v0.3', tag_name: 'v0.3',
tag_path: '/root/release-test/-/tags/v0.3',
description: 'A super nice release!', description: 'A super nice release!',
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>', description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z', created_at: '2019-08-26T17:54:04.952Z',
...@@ -56,6 +57,7 @@ export const release = { ...@@ -56,6 +57,7 @@ export const release = {
committer_email: 'admin@example.com', committer_email: 'admin@example.com',
committed_date: '2019-08-26T17:47:07.000Z', committed_date: '2019-08-26T17:47:07.000Z',
}, },
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false, upcoming_release: false,
milestones, milestones,
assets: { assets: {
......
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