Commit 750e3571 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'nfriend-add-edit-button-to-release-blocks' into 'master'

Add "edit" button to release blocks on Releases page

See merge request gitlab-org/gitlab!18411
parents a8327f49 aa8c980c
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import _ from 'underscore';
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import { GlTooltipDirective, GlLink, GlBadge, GlButton } 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';
......@@ -9,19 +9,21 @@ import { __, n__, sprintf } from '~/locale';
import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility';
import { scrollToElement } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ReleaseBlock',
components: {
GlLink,
GlBadge,
GlButton,
Icon,
UserAvatarLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
mixins: [timeagoMixin, glFeatureFlagsMixin()],
props: {
release: {
type: Object,
......@@ -72,6 +74,11 @@ export default {
labelText() {
return n__('Milestone', 'Milestones', this.release.milestones.length);
},
shouldShowEditButton() {
return Boolean(
this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit,
);
},
},
mounted() {
const hash = getLocationHash();
......@@ -89,12 +96,23 @@ export default {
<template>
<div :id="id" :class="{ 'bg-line-target-blue': isHighlighted }" class="card release-block">
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
<div class="d-flex align-items-start">
<h2 class="card-title mt-0 mr-auto">
{{ release.name }}
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
<gl-link
v-if="shouldShowEditButton"
v-gl-tooltip
class="btn btn-default js-edit-button ml-2"
:title="__('Edit this release')"
:href="release._links.edit"
>
<icon name="pencil" />
</gl-link>
</div>
<div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="append-right-8">
......
......@@ -4,6 +4,9 @@ class Projects::ReleasesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_read_release!
before_action do
push_frontend_feature_flag(:release_edit_page, project)
end
def index
end
......
---
title: Add edit button to release blocks on Releases page
merge_request: 18411
author:
type: added
......@@ -5805,6 +5805,9 @@ msgstr ""
msgid "Edit stage"
msgstr ""
msgid "Edit this release"
msgstr ""
msgid "Edit wiki page"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Release block with default props matches the snapshot 1`] = `
<div
class="card release-block"
id="v0.3"
>
<div
class="card-body"
>
<div
class="d-flex align-items-start"
>
<h2
class="card-title mt-0 mr-auto"
>
New release
<!---->
</h2>
<a
class="btn btn-default js-edit-button ml-2"
data-original-title="Edit this release"
href="http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit"
title=""
>
<svg
aria-hidden="true"
class="s16 ic-pencil"
>
<use
xlink:href="#pencil"
/>
</svg>
</a>
</div>
<div
class="card-subtitle d-flex flex-wrap text-secondary"
>
<div
class="append-right-8"
>
<svg
aria-hidden="true"
class="align-middle s16 ic-commit"
>
<use
xlink:href="#commit"
/>
</svg>
<span
data-original-title="Initial commit"
title=""
>
c22b0728
</span>
</div>
<div
class="append-right-8"
>
<svg
aria-hidden="true"
class="align-middle s16 ic-tag"
>
<use
xlink:href="#tag"
/>
</svg>
<span
data-original-title="Tag"
title=""
>
v0.3
</span>
</div>
<div
class="js-milestone-list-label"
>
<svg
aria-hidden="true"
class="align-middle s16 ic-flag"
>
<use
xlink:href="#flag"
/>
</svg>
<span
class="js-label-text"
>
Milestones
</span>
</div>
<a
class="append-right-4 prepend-left-4 js-milestone-link"
data-original-title="The 13.6 milestone!"
href="http://0.0.0.0:3001/root/release-test/-/milestones/2"
title=""
>
13.6
</a>
<a
class="append-right-4 prepend-left-4 js-milestone-link"
data-original-title="The 13.5 milestone!"
href="http://0.0.0.0:3001/root/release-test/-/milestones/1"
title=""
>
13.5
</a>
<!---->
<div
class="append-right-4"
>
<span
data-original-title="Aug 26, 2019 5:54pm GMT+0000"
title=""
>
released 1 month ago
</span>
</div>
<div
class="d-flex"
>
by
<a
class="user-avatar-link prepend-left-4"
href=""
>
<span>
<img
alt="root's avatar"
class="avatar s20 "
data-original-title=""
data-src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
height="20"
src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
title=""
width="20"
/>
<div
aria-hidden="true"
class="js-user-avatar-image-toolip d-none"
style="display: none;"
>
<div>
root
</div>
</div>
</span>
<!---->
</a>
</div>
</div>
<div
class="card-text prepend-top-default"
>
<b>
Assets
<span
class="js-assets-count badge badge-pill"
>
5
</span>
</b>
<ul
class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list"
>
<li
class="append-bottom-8"
>
<a
class=""
data-original-title="Download asset"
href="https://google.com"
title=""
>
<svg
aria-hidden="true"
class="align-middle append-right-4 align-text-bottom s16 ic-package"
>
<use
xlink:href="#package"
/>
</svg>
my link
<span>
(external source)
</span>
</a>
</li>
<li
class="append-bottom-8"
>
<a
class=""
data-original-title="Download asset"
href="https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50"
title=""
>
<svg
aria-hidden="true"
class="align-middle append-right-4 align-text-bottom s16 ic-package"
>
<use
xlink:href="#package"
/>
</svg>
my second link
<!---->
</a>
</li>
</ul>
<div
class="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="btn btn-link"
data-toggle="dropdown"
type="button"
>
<svg
aria-hidden="true"
class="align-top append-right-4 s16 ic-doc-code"
>
<use
xlink:href="#doc-code"
/>
</svg>
Source code
<svg
aria-hidden="true"
class="s16 ic-arrow-down"
>
<use
xlink:href="#arrow-down"
/>
</svg>
</button>
<div
class="js-sources-dropdown dropdown-menu"
>
<li>
<a
class=""
href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip"
>
Download zip
</a>
</li>
<li>
<a
class=""
href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz"
>
Download tar.gz
</a>
</li>
<li>
<a
class=""
href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2"
>
Download tar.bz2
</a>
</li>
<li>
<a
class=""
href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar"
>
Download tar
</a>
</li>
</div>
</div>
</div>
<div
class="card-text prepend-top-default"
>
<div>
<p
data-sourcepos="1:1-1:21"
dir="auto"
>
A super nice release!
</p>
</div>
</div>
</div>
</div>
`;
......@@ -19,46 +19,53 @@ jest.mock('~/lib/utils/common_utils', () => ({
describe('Release block', () => {
let wrapper;
let releaseClone;
const factory = releaseProp => {
const factory = (releaseProp, releaseEditPageFeatureFlag = true) => {
wrapper = mount(ReleaseBlock, {
propsData: {
release: releaseProp,
},
provide: {
glFeatures: {
releaseEditPage: releaseEditPageFeatureFlag,
},
},
sync: false,
});
return wrapper.vm.$nextTick();
};
const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
const editButton = () => wrapper.find('.js-edit-button');
beforeEach(() => {
releaseClone = JSON.parse(JSON.stringify(release));
});
afterEach(() => {
wrapper.destroy();
});
describe('with default props', () => {
beforeEach(() => {
factory(release);
beforeEach(() => factory(release));
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
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);
wrapper.setProps({ release: { ...release, commit_path: '/commit/example' } });
expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
it('renders an edit button that links to the "Edit release" page', () => {
expect(editButton().exists()).toBe(true);
expect(editButton().attributes('href')).toBe(release._links.edit);
});
it('renders tag name', () => {
expect(wrapper.text()).toContain(release.tag_name);
wrapper.setProps({ release: { ...release, tag_path: '/tag/example' } });
expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
it('renders release name', () => {
expect(wrapper.text()).toContain(release.name);
});
it('renders release date', () => {
......@@ -141,44 +148,73 @@ describe('Release block', () => {
});
});
it('renders commit sha', () => {
releaseClone.commit_path = '/commit/example';
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain(release.commit.short_id);
expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
});
});
it('renders tag name', () => {
releaseClone.tag_path = '/tag/example';
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain(release.tag_name);
expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
});
});
it("does not render an edit button if release._links.edit isn't a string", () => {
delete releaseClone._links;
return factory(releaseClone).then(() => {
expect(editButton().exists()).toBe(false);
});
});
it('does not render an edit button if the releaseEditPage feature flag is disabled', () =>
factory(releaseClone, false).then(() => {
expect(editButton().exists()).toBe(false);
}));
it('does not render the milestone list if no milestones are associated to the release', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
delete releaseClone.milestones;
factory(releaseClone);
expect(milestoneListLabel().exists()).toBe(false);
return factory(releaseClone).then(() => {
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(
milestoneListLabel()
.find('.js-label-text')
.text(),
).toEqual('Milestone');
return factory(releaseClone).then(() => {
expect(
milestoneListLabel()
.find('.js-label-text')
.text(),
).toEqual('Milestone');
});
});
it('renders upcoming release badge', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
releaseClone.upcoming_release = true;
factory(releaseClone);
expect(wrapper.text()).toContain('Upcoming Release');
return factory(releaseClone).then(() => {
expect(wrapper.text()).toContain('Upcoming Release');
});
});
it('slugifies the tag_name before setting it as the elements ID', () => {
const releaseClone = JSON.parse(JSON.stringify(release));
releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>';
factory(releaseClone);
expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
return factory(releaseClone).then(() => {
expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script-');
});
});
describe('anchor scrolling', () => {
......@@ -190,40 +226,39 @@ describe('Release block', () => {
it('does not attempt to scroll the page if no anchor tag is included in the URL', () => {
mockLocationHash = '';
factory(release);
expect(scrollToElement).not.toHaveBeenCalled();
return factory(release).then(() => {
expect(scrollToElement).not.toHaveBeenCalled();
});
});
it("does not attempt to scroll the page if the anchor tag doesn't match the release's tag name", () => {
mockLocationHash = 'v0.4';
factory(release);
expect(scrollToElement).not.toHaveBeenCalled();
return factory(release).then(() => {
expect(scrollToElement).not.toHaveBeenCalled();
});
});
it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => {
mockLocationHash = release.tag_name;
factory(release);
return factory(release).then(() => {
expect(scrollToElement).toHaveBeenCalledTimes(1);
expect(scrollToElement).toHaveBeenCalledTimes(1);
expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
expect(scrollToElement).toHaveBeenCalledWith(wrapper.element);
});
});
it('renders with a light blue background if it is the target of the anchor', () => {
mockLocationHash = release.tag_name;
factory(release);
return wrapper.vm.$nextTick().then(() => {
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(true);
});
});
it('does not render with a light blue background if it is not the target of the anchor', () => {
mockLocationHash = '';
factory(release);
return wrapper.vm.$nextTick().then(() => {
return factory(release).then(() => {
expect(hasTargetBlueBackground()).toBe(false);
});
});
......
......@@ -94,4 +94,7 @@ export const release = {
},
],
},
_links: {
edit: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
},
};
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