Commit cbfc556e authored by Marcia Ramos's avatar Marcia Ramos

Merge branch 'nfriend-create-release-through-ui' into 'master'

Add button to create a Release through the UI

Closes #32812

See merge request gitlab-org/gitlab!24516
parents 0b34bae1 18350e7f
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui'; import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui';
import { import {
getParameterByName, getParameterByName,
historyPushState, historyPushState,
buildUrlWithCurrentLocation, buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ReleaseBlock from './release_block.vue'; import ReleaseBlock from './release_block.vue';
...@@ -16,13 +17,14 @@ export default { ...@@ -16,13 +17,14 @@ export default {
GlEmptyState, GlEmptyState,
ReleaseBlock, ReleaseBlock,
TablePagination, TablePagination,
GlLink,
}, },
props: { props: {
projectId: { projectId: {
type: String, type: String,
required: true, required: true,
}, },
documentationLink: { documentationPath: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -30,6 +32,11 @@ export default { ...@@ -30,6 +32,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
newReleasePath: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']), ...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']),
...@@ -39,6 +46,11 @@ export default { ...@@ -39,6 +46,11 @@ export default {
shouldRenderSuccessState() { shouldRenderSuccessState() {
return this.releases.length && !this.isLoading && !this.hasError; return this.releases.length && !this.isLoading && !this.hasError;
}, },
emptyStateText() {
return __(
"Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.",
);
},
}, },
created() { created() {
this.fetchReleases({ this.fetchReleases({
...@@ -56,7 +68,16 @@ export default { ...@@ -56,7 +68,16 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="prepend-top-default"> <div class="flex flex-column mt-2">
<gl-link
v-if="newReleasePath"
:href="newReleasePath"
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
class="btn btn-success align-self-end mb-2 js-new-release-btn"
>
{{ __('New release') }}
</gl-link>
<gl-skeleton-loading v-if="isLoading" class="js-loading" /> <gl-skeleton-loading v-if="isLoading" class="js-loading" />
<gl-empty-state <gl-empty-state
...@@ -64,14 +85,20 @@ export default { ...@@ -64,14 +85,20 @@ export default {
class="js-empty-state" class="js-empty-state"
:title="__('Getting started with releases')" :title="__('Getting started with releases')"
:svg-path="illustrationPath" :svg-path="illustrationPath"
:description=" >
__( <template #description>
'Releases are based on Git tags and mark specific points in a project\'s development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software.', <span id="releases-description">
) {{ emptyStateText }}
" <gl-link
:primary-button-link="documentationLink" :href="documentationPath"
:primary-button-text="__('Open Documentation')" :aria-label="__('Releases documentation')"
/> target="_blank"
>
{{ __('More information') }}
</gl-link>
</span>
</template>
</gl-empty-state>
<div v-else-if="shouldRenderSuccessState" class="js-success-state"> <div v-else-if="shouldRenderSuccessState" class="js-success-state">
<release-block <release-block
......
...@@ -15,11 +15,7 @@ export default () => { ...@@ -15,11 +15,7 @@ export default () => {
}), }),
render: h => render: h =>
h(ReleaseListApp, { h(ReleaseListApp, {
props: { props: el.dataset,
projectId: el.dataset.projectId,
documentationLink: el.dataset.documentationPath,
illustrationPath: el.dataset.illustrationPath,
},
}), }),
}); });
}; };
...@@ -17,7 +17,9 @@ module ReleasesHelper ...@@ -17,7 +17,9 @@ module ReleasesHelper
project_id: @project.id, project_id: @project.id,
illustration_path: illustration, illustration_path: illustration,
documentation_path: help_page documentation_path: help_page
} }.tap do |data|
data[:new_release_path] = new_project_tag_path(@project) if can?(current_user, :create_release, @project)
end
end end
def data_for_edit_release_page def data_for_edit_release_page
......
...@@ -36,11 +36,19 @@ ...@@ -36,11 +36,19 @@
.form-group.row .form-group.row
= label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2' = label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2'
.col-sm-10 .col-sm-10
.form-text.mb-3
- link_start = '<a href="%{url}" rel="noopener noreferrer" target="_blank">'.html_safe
- releases_page_path = project_releases_path(@project)
- releases_page_link_start = link_start % { url: releases_page_path }
- docs_url = help_page_path('user/project/releases/index.md', anchor: 'creating-a-release')
- docs_link_start = link_start % { url: docs_url }
- link_end = '</a>'.html_safe
- replacements = { releases_page_link_start: releases_page_link_start, docs_link_start: docs_link_start, link_end: link_end }
= s_('TagsPage|Optionally, create a public Release of your project, based on this tag. Release notes are displayed on the %{releases_page_link_start}Releases%{link_end} page. %{docs_link_start}More information%{link_end}').html_safe % replacements
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description
= render 'shared/notes/hints' = render 'shared/notes/hints'
.form-text.text-muted
= s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions .form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-success' = button_tag s_('TagsPage|Create tag'), class: 'btn btn-success'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel' = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
......
---
title: Add "New release" button to Releases page
merge_request: 24516
author:
type: added
...@@ -69,6 +69,7 @@ The following table depicts the various user permission levels in a project. ...@@ -69,6 +69,7 @@ The following table depicts the various user permission levels in a project.
| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ | | See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create confidential issue | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | Create confidential issue | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ | | View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
| View [Releases](project/releases/index.md) | ✓ (*6*) | ✓ | ✓ | ✓ | ✓ |
| Assign issues | | ✓ | ✓ | ✓ | ✓ | | Assign issues | | ✓ | ✓ | ✓ | ✓ |
| Label issues | | ✓ | ✓ | ✓ | ✓ | | Label issues | | ✓ | ✓ | ✓ | ✓ |
| Set issue weight | | ✓ | ✓ | ✓ | ✓ | | Set issue weight | | ✓ | ✓ | ✓ | ✓ |
...@@ -83,6 +84,7 @@ The following table depicts the various user permission levels in a project. ...@@ -83,6 +84,7 @@ The following table depicts the various user permission levels in a project.
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| View project statistics | | ✓ | ✓ | ✓ | ✓ | | View project statistics | | ✓ | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ | | View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete [Releases](project/releases/index.md)| | | ✓ | ✓ | ✓ |
| Pull from [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ | | Pull from [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| Publish to [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | | ✓ | ✓ | ✓ | | Publish to [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Upload [Design Management](project/issues/design_management.md) files **(PREMIUM)** | | | ✓ | ✓ | ✓ | | Upload [Design Management](project/issues/design_management.md) files **(PREMIUM)** | | | ✓ | ✓ | ✓ |
...@@ -152,6 +154,7 @@ The following table depicts the various user permission levels in a project. ...@@ -152,6 +154,7 @@ The following table depicts the various user permission levels in a project.
1. If **Public pipelines** is enabled in **Project Settings > CI/CD**. 1. If **Public pipelines** is enabled in **Project Settings > CI/CD**.
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [Protected Branches](./project/protected_branches.md). 1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [Protected Branches](./project/protected_branches.md).
1. If the [branch is protected](./project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings), this depends on the access Developers and Maintainers are given. 1. If the [branch is protected](./project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings), this depends on the access Developers and Maintainers are given.
1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see repository information like tags and commits.
## Project features permissions ## Project features permissions
...@@ -198,17 +201,6 @@ Confidential issues can be accessed by reporters and higher permission levels, ...@@ -198,17 +201,6 @@ Confidential issues can be accessed by reporters and higher permission levels,
as well as by guest users that create a confidential issue. To learn more, as well as by guest users that create a confidential issue. To learn more,
read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues). read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues).
### Releases permissions
[Project Releases](project/releases/index.md) can be read by project
members with Reporter, Developer, Maintainer, and Owner permissions.
Guest users can access Release pages for downloading assets but
are not allowed to download the source code nor see repository
information such as tags and commits.
Releases can be created, updated, or deleted via [Releases APIs](../api/releases/index.md)
by project Developers, Maintainers, and Owners.
## Group members permissions ## Group members permissions
NOTE: **Note:** NOTE: **Note:**
......
...@@ -16,13 +16,6 @@ GitLab's **Releases** are a way to track deliverables in your project. Consider ...@@ -16,13 +16,6 @@ GitLab's **Releases** are a way to track deliverables in your project. Consider
a snapshot in time of the source, build output, artifacts, and other metadata a snapshot in time of the source, build output, artifacts, and other metadata
associated with a released version of your code. associated with a released version of your code.
There are several ways to create a Release:
- In the interface, when you create a new Git tag.
- In the interface, by adding a release note to an existing Git tag.
- Using the [Releases API](../../../api/releases/index.md): we recommend doing this as one of the last
steps in your CI/CD release pipeline.
## Getting started with Releases ## Getting started with Releases
Start by giving a [description](#release-description) to the Release and Start by giving a [description](#release-description) to the Release and
...@@ -117,7 +110,7 @@ it takes you to the list of Releases. ...@@ -117,7 +110,7 @@ it takes you to the list of Releases.
![Number of Releases](img/releases_count_v12_8.png "Incremental counter of Releases") ![Number of Releases](img/releases_count_v12_8.png "Incremental counter of Releases")
For private projects, the number of Releases is displayed to users with Reporter For private projects, the number of Releases is displayed to users with Reporter
[permissions](../../permissions.md#releases-permissions) or higher. For public projects, [permissions](../../permissions.md#project-members-permissions) or higher. For public projects,
it is displayed to every user regardless of their permission level. it is displayed to every user regardless of their permission level.
### Upcoming Releases ### Upcoming Releases
...@@ -130,6 +123,29 @@ Release tag. Once the `released_at` date and time has passed, the badge is autom ...@@ -130,6 +123,29 @@ Release tag. Once the `released_at` date and time has passed, the badge is autom
![An upcoming release](img/upcoming_release_v12_7.png) ![An upcoming release](img/upcoming_release_v12_7.png)
## Creating a Release
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32812) in GitLab
12.9, Releases can be created directly through the GitLab Releases UI.
NOTE: **Note:**
Only users with Developer permissions or higher can create Releases.
Read more about [Release permissions](../../../user/permissions.md#project-members-permissions).
To create a new Release through the GitLab UI:
1. Navigate to **Project overview > Releases** and click the **New release** button.
1. On the **New Tag** page, fill out the tag details.
1. Optionally, in the **Release notes** field, enter the Release's description.
If you leave this field empty, only a tag will be created.
If you populate it, both a tag and a Release will be created.
1. Click **Create tag**.
If you created a release, you can view it at **Project overview > Releases**.
You can also create a Release using the [Releases API](../../../api/releases/index.md#create-a-release):
we recommend doing this as one of the last steps in your CI/CD release pipeline.
## Editing a release ## Editing a release
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26016) in GitLab 12.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26016) in GitLab 12.6.
......
...@@ -13050,6 +13050,9 @@ msgstr "" ...@@ -13050,6 +13050,9 @@ msgstr ""
msgid "New project" msgid "New project"
msgstr "" msgstr ""
msgid "New release"
msgstr ""
msgid "New runners registration token has been generated!" msgid "New runners registration token has been generated!"
msgstr "" msgstr ""
...@@ -13643,9 +13646,6 @@ msgstr "" ...@@ -13643,9 +13646,6 @@ msgstr ""
msgid "Open" msgid "Open"
msgstr "" msgstr ""
msgid "Open Documentation"
msgstr ""
msgid "Open Selection" msgid "Open Selection"
msgstr "" msgstr ""
...@@ -16317,6 +16317,9 @@ msgstr "" ...@@ -16317,6 +16317,9 @@ msgstr ""
msgid "Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}." msgid "Releases are based on Git tags. We recommend naming tags that fit within semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}."
msgstr "" msgstr ""
msgid "Releases documentation"
msgstr ""
msgid "Release|Something went wrong while getting the release details" msgid "Release|Something went wrong while getting the release details"
msgstr "" msgstr ""
...@@ -19317,7 +19320,7 @@ msgstr "" ...@@ -19317,7 +19320,7 @@ msgstr ""
msgid "TagsPage|Optionally, add a message to the tag. Leaving this blank creates a %{link_start}lightweight tag.%{link_end}" msgid "TagsPage|Optionally, add a message to the tag. Leaving this blank creates a %{link_start}lightweight tag.%{link_end}"
msgstr "" msgstr ""
msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page." msgid "TagsPage|Optionally, create a public Release of your project, based on this tag. Release notes are displayed on the %{releases_page_link_start}Releases%{link_end} page. %{docs_link_start}More information%{link_end}"
msgstr "" msgstr ""
msgid "TagsPage|Release notes" msgid "TagsPage|Release notes"
......
...@@ -18,16 +18,31 @@ describe ReleasesHelper do ...@@ -18,16 +18,31 @@ describe ReleasesHelper do
context 'url helpers' do context 'url helpers' do
let(:project) { build(:project, namespace: create(:group)) } let(:project) { build(:project, namespace: create(:group)) }
let(:release) { create(:release, project: project) } let(:release) { create(:release, project: project) }
let(:user) { create(:user) }
let(:can_user_create_release) { false }
let(:common_keys) { [:project_id, :illustration_path, :documentation_path] }
before do before do
helper.instance_variable_set(:@project, project) helper.instance_variable_set(:@project, project)
helper.instance_variable_set(:@release, release) helper.instance_variable_set(:@release, release)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?)
.with(user, :create_release, project)
.and_return(can_user_create_release)
end end
describe '#data_for_releases_page' do describe '#data_for_releases_page' do
it 'has the needed data to display release blocks' do it 'includes the required data for displaying release blocks' do
keys = %i(project_id illustration_path documentation_path) expect(helper.data_for_releases_page.keys).to contain_exactly(*common_keys)
expect(helper.data_for_releases_page.keys).to eq(keys) end
context 'when the user is allowed to create a new release' do
let(:can_user_create_release) { true }
it 'includes new_release_path' do
expect(helper.data_for_releases_page.keys).to contain_exactly(*common_keys, :new_release_path)
expect(helper.data_for_releases_page[:new_release_path]).to eq(new_project_tag_path(project))
end
end end
end end
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
releases, releases,
} from '../mock_data'; } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import waitForPromises from 'spec/helpers/wait_for_promises';
describe('Releases App ', () => { describe('Releases App ', () => {
const Component = Vue.extend(app); const Component = Vue.extend(app);
...@@ -22,7 +23,7 @@ describe('Releases App ', () => { ...@@ -22,7 +23,7 @@ describe('Releases App ', () => {
const props = { const props = {
projectId: 'gitlab-ce', projectId: 'gitlab-ce',
documentationLink: 'help/releases', documentationPath: 'help/releases',
illustrationPath: 'illustration/path', illustrationPath: 'illustration/path',
}; };
...@@ -51,9 +52,9 @@ describe('Releases App ', () => { ...@@ -51,9 +52,9 @@ describe('Releases App ', () => {
expect(vm.$el.querySelector('.js-success-state')).toBeNull(); expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
setTimeout(() => { waitForPromises()
done(); .then(done)
}, 0); .catch(done.fail);
}); });
}); });
...@@ -66,14 +67,16 @@ describe('Releases App ', () => { ...@@ -66,14 +67,16 @@ describe('Releases App ', () => {
}); });
it('renders success state', done => { it('renders success state', done => {
setTimeout(() => { waitForPromises()
expect(vm.$el.querySelector('.js-loading')).toBeNull(); .then(() => {
expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
done();
}, 0); done();
})
.catch(done.fail);
}); });
}); });
...@@ -86,14 +89,16 @@ describe('Releases App ', () => { ...@@ -86,14 +89,16 @@ describe('Releases App ', () => {
}); });
it('renders success state', done => { it('renders success state', done => {
setTimeout(() => { waitForPromises()
expect(vm.$el.querySelector('.js-loading')).toBeNull(); .then(() => {
expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull(); expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();
done();
}, 0); done();
})
.catch(done.fail);
}); });
}); });
...@@ -104,14 +109,76 @@ describe('Releases App ', () => { ...@@ -104,14 +109,76 @@ describe('Releases App ', () => {
}); });
it('renders empty state', done => { it('renders empty state', done => {
setTimeout(() => { waitForPromises()
expect(vm.$el.querySelector('.js-loading')).toBeNull(); .then(() => {
expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull(); expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull(); expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
done();
}, 0); done();
})
.catch(done.fail);
});
});
describe('"New release" button', () => {
const findNewReleaseButton = () => vm.$el.querySelector('.js-new-release-btn');
beforeEach(() => {
spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} }));
});
const factory = additionalProps => {
vm = mountComponentWithStore(Component, {
props: {
...props,
...additionalProps,
},
store,
});
};
describe('when the user is allowed to create a new Release', () => {
const newReleasePath = 'path/to/new/release';
beforeEach(() => {
factory({ newReleasePath });
});
it('renders the "New release" button', done => {
waitForPromises()
.then(() => {
expect(findNewReleaseButton()).not.toBeNull();
done();
})
.catch(done.fail);
});
it('renders the "New release" button with the correct href', done => {
waitForPromises()
.then(() => {
expect(findNewReleaseButton().getAttribute('href')).toBe(newReleasePath);
done();
})
.catch(done.fail);
});
});
describe('when the user is not allowed to create a new Release', () => {
beforeEach(() => factory());
it('does not render the "New release" button', done => {
waitForPromises()
.then(() => {
expect(findNewReleaseButton()).toBeNull();
done();
})
.catch(done.fail);
});
}); });
}); });
}); });
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