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>
import { mapState, mapActions } from 'vuex';
import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui';
import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui';
import {
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ReleaseBlock from './release_block.vue';
......@@ -16,13 +17,14 @@ export default {
GlEmptyState,
ReleaseBlock,
TablePagination,
GlLink,
},
props: {
projectId: {
type: String,
required: true,
},
documentationLink: {
documentationPath: {
type: String,
required: true,
},
......@@ -30,6 +32,11 @@ export default {
type: String,
required: true,
},
newReleasePath: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']),
......@@ -39,6 +46,11 @@ export default {
shouldRenderSuccessState() {
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() {
this.fetchReleases({
......@@ -56,7 +68,16 @@ export default {
};
</script>
<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-empty-state
......@@ -64,14 +85,20 @@ export default {
class="js-empty-state"
:title="__('Getting started with releases')"
:svg-path="illustrationPath"
: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.',
)
"
:primary-button-link="documentationLink"
:primary-button-text="__('Open Documentation')"
/>
>
<template #description>
<span id="releases-description">
{{ emptyStateText }}
<gl-link
:href="documentationPath"
:aria-label="__('Releases documentation')"
target="_blank"
>
{{ __('More information') }}
</gl-link>
</span>
</template>
</gl-empty-state>
<div v-else-if="shouldRenderSuccessState" class="js-success-state">
<release-block
......
......@@ -15,11 +15,7 @@ export default () => {
}),
render: h =>
h(ReleaseListApp, {
props: {
projectId: el.dataset.projectId,
documentationLink: el.dataset.documentationPath,
illustrationPath: el.dataset.illustrationPath,
},
props: el.dataset,
}),
});
};
......@@ -17,7 +17,9 @@ module ReleasesHelper
project_id: @project.id,
illustration_path: illustration,
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
def data_for_edit_release_page
......
......@@ -36,11 +36,19 @@
.form-group.row
= label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2'
.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 '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'
.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
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-success'
= 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.
| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create confidential issue | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
| View [Releases](project/releases/index.md) | ✓ (*6*) | ✓ | ✓ | ✓ | ✓ |
| Assign issues | | ✓ | ✓ | ✓ | ✓ |
| Label issues | | ✓ | ✓ | ✓ | ✓ |
| Set issue weight | | ✓ | ✓ | ✓ | ✓ |
......@@ -83,6 +84,7 @@ The following table depicts the various user permission levels in a project.
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| View project statistics | | ✓ | ✓ | ✓ | ✓ |
| 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)** | | ✓ | ✓ | ✓ | ✓ |
| 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)** | | | ✓ | ✓ | ✓ |
......@@ -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. 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. 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
......@@ -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,
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
NOTE: **Note:**
......
......@@ -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
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
Start by giving a [description](#release-description) to the Release and
......@@ -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")
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.
### Upcoming Releases
......@@ -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)
## 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
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26016) in GitLab 12.6.
......
......@@ -13050,6 +13050,9 @@ msgstr ""
msgid "New project"
msgstr ""
msgid "New release"
msgstr ""
msgid "New runners registration token has been generated!"
msgstr ""
......@@ -13643,9 +13646,6 @@ msgstr ""
msgid "Open"
msgstr ""
msgid "Open Documentation"
msgstr ""
msgid "Open Selection"
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}."
msgstr ""
msgid "Releases documentation"
msgstr ""
msgid "Release|Something went wrong while getting the release details"
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}"
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 ""
msgid "TagsPage|Release notes"
......
......@@ -18,16 +18,31 @@ describe ReleasesHelper do
context 'url helpers' do
let(:project) { build(:project, namespace: create(:group)) }
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
helper.instance_variable_set(:@project, project)
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
describe '#data_for_releases_page' do
it 'has the needed data to display release blocks' do
keys = %i(project_id illustration_path documentation_path)
expect(helper.data_for_releases_page.keys).to eq(keys)
it 'includes the required data for displaying release blocks' do
expect(helper.data_for_releases_page.keys).to contain_exactly(*common_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
......
......@@ -13,6 +13,7 @@ import {
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import waitForPromises from 'spec/helpers/wait_for_promises';
describe('Releases App ', () => {
const Component = Vue.extend(app);
......@@ -22,7 +23,7 @@ describe('Releases App ', () => {
const props = {
projectId: 'gitlab-ce',
documentationLink: 'help/releases',
documentationPath: 'help/releases',
illustrationPath: 'illustration/path',
};
......@@ -51,9 +52,9 @@ describe('Releases App ', () => {
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
setTimeout(() => {
done();
}, 0);
waitForPromises()
.then(done)
.catch(done.fail);
});
});
......@@ -66,14 +67,16 @@ describe('Releases App ', () => {
});
it('renders success state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
done();
}, 0);
waitForPromises()
.then(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
done();
})
.catch(done.fail);
});
});
......@@ -86,14 +89,16 @@ describe('Releases App ', () => {
});
it('renders success state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();
done();
}, 0);
waitForPromises()
.then(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();
done();
})
.catch(done.fail);
});
});
......@@ -104,14 +109,76 @@ describe('Releases App ', () => {
});
it('renders empty state', done => {
setTimeout(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
done();
}, 0);
waitForPromises()
.then(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
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