Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-27 15:09:42 +00:00
parent a4068557f4
commit 2977cf67ec
44 changed files with 712 additions and 383 deletions

View File

@ -1507,7 +1507,6 @@ RSpec/ContextWording:
- 'spec/features/snippets/explore_spec.rb'
- 'spec/features/tags/developer_creates_tag_spec.rb'
- 'spec/features/tags/developer_deletes_tag_spec.rb'
- 'spec/features/tags/developer_updates_tag_spec.rb'
- 'spec/features/tags/maintainer_deletes_protected_tag_spec.rb'
- 'spec/features/uploads/user_uploads_file_to_note_spec.rb'
- 'spec/features/user_can_display_performance_bar_spec.rb'

View File

@ -1,6 +0,0 @@
import $ from 'jquery';
import GLForm from '~/gl_form';
import ZenMode from '~/zen_mode';
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.release-form')); // eslint-disable-line no-new

View File

@ -332,11 +332,12 @@ export default {
:button-title="__('Add a table')"
icon="table"
/>
<toolbar-button
<gl-button
v-if="!restrictedToolBarItems.includes('attach-file')"
v-gl-tooltip
:title="__('Attach a file or image')"
data-testid="button-attach-file"
:prepend="true"
:button-title="__('Attach a file or image')"
category="tertiary"
icon="paperclip"
@click="handleAttachFile"
/>

View File

@ -1,39 +0,0 @@
# frozen_string_literal: true
# TODO: remove this file together with FF https://gitlab.com/gitlab-org/gitlab/-/issues/366244
# also delete view/routes
class Projects::Tags::ReleasesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authorize_push_code!
before_action :tag
before_action :release
feature_category :release_evidence
urgency :low
def edit
end
def update
release.update(release_params) if release.persisted? || release_params[:description].present?
redirect_to project_tag_path(@project, tag.name)
end
private
def tag
@tag ||= @repository.find_tag(params[:tag_id])
end
def release
@release ||= Releases::CreateService.new(project, current_user, tag: @tag.name)
.find_or_build_release
end
def release_params
params.require(:release).permit(:description)
end
end

View File

@ -15,6 +15,9 @@ module Mutations
argument :title, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::WorkItemType, :title)
argument :confidential, GraphQL::Types::Boolean,
required: false,
description: 'Sets the work item confidentiality.'
argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
required: false,
description: 'Input for description widget.'

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Mutations
module Timelogs
class Base < Mutations::BaseMutation
field :timelog,
Types::TimelogType,
null: true,
description: 'Timelog.'
private
def response(result)
{ timelog: result.payload[:timelog], errors: result.errors }
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module Mutations
module Timelogs
class Create < Base
graphql_name 'TimelogCreate'
argument :time_spent,
GraphQL::Types::String,
required: true,
description: 'Amount of time spent.'
argument :spent_at,
Types::DateType,
required: true,
description: 'When the time was spent.'
argument :summary,
GraphQL::Types::String,
required: true,
description: 'Summary of time spent.'
argument :issuable_id,
::Types::GlobalIDType[::Issuable],
required: true,
description: 'Global ID of the issuable (Issue, WorkItem or MergeRequest).'
authorize :create_timelog
def resolve(issuable_id:, time_spent:, spent_at:, summary:, **args)
issuable = authorized_find!(id: issuable_id)
parsed_time_spent = Gitlab::TimeTrackingFormatter.parse(time_spent)
result = ::Timelogs::CreateService.new(
issuable, parsed_time_spent, spent_at, summary, current_user
).execute
response(result)
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: [::Issue, ::WorkItem, ::MergeRequest]).sync
end
end
end
end

View File

@ -2,14 +2,9 @@
module Mutations
module Timelogs
class Delete < Mutations::BaseMutation
class Delete < Base
graphql_name 'TimelogDelete'
field :timelog,
Types::TimelogType,
null: true,
description: 'Deleted timelog.'
argument :id,
::Types::GlobalIDType[::Timelog],
required: true,
@ -22,11 +17,13 @@ module Mutations
result = ::Timelogs::DeleteService.new(timelog, current_user).execute
# Return the result payload, not the loaded timelog, so that it returns null in case of unauthorized access
{ timelog: result.payload, errors: result.errors }
response(result)
end
private
def find_object(id:)
GitlabSchema.find_by_gid(id)
GitlabSchema.object_from_id(id, expected_type: ::Timelog).sync
end
end
end

View File

@ -13,6 +13,9 @@ module Mutations
authorize :create_work_item
argument :confidential, GraphQL::Types::Boolean,
required: false,
description: 'Sets the work item confidentiality.'
argument :description, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::WorkItemType, :description)

View File

@ -94,6 +94,7 @@ module Types
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
mount_mutation Mutations::Timelogs::Create
mount_mutation Mutations::Timelogs::Delete
mount_mutation Mutations::Todos::Create
mount_mutation Mutations::Todos::MarkDone

View File

@ -44,6 +44,10 @@ class IssuablePolicy < BasePolicy
rule { can?(:read_issue) & can?(:developer_access) }.policy do
enable :admin_incident_management_timeline_event
end
rule { can?(:reporter_access) }.policy do
enable :create_timelog
end
end
IssuablePolicy.prepend_mod_with('IssuablePolicy')

View File

@ -19,10 +19,6 @@ module Releases
create_release(tag, evidence_pipeline)
end
def find_or_build_release
release || build_release(existing_tag)
end
private
def ensure_tag

View File

@ -111,6 +111,24 @@ module SystemNoteService
::SystemNotes::TimeTrackingService.new(noteable: noteable, project: project, author: author).change_time_spent
end
# Called when a timelog is added to an issuable
#
# issuable - Issuable object (Issue, WorkItem or MergeRequest)
# project - Project owning the issuable
# author - User performing the change
# timelog - Created timelog
#
# Example Note text:
#
# "subtracted 1h 15m of time spent"
#
# "added 2h 30m of time spent"
#
# Returns the created Note object
def created_timelog(issuable, project, author, timelog)
::SystemNotes::TimeTrackingService.new(noteable: issuable, project: project, author: author).created_timelog(timelog)
end
# Called when a timelog is removed from a Noteable
#
# noteable - Noteable object

View File

@ -76,6 +76,32 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
# Called when a timelog is added to an issuable
#
# timelog - Added timelog
#
# Example Note text:
#
# "subtracted 1h 15m of time spent"
#
# "added 2h 30m of time spent"
#
# Returns the created Note object
def created_timelog(timelog)
time_spent = timelog.time_spent
spent_at = timelog.spent_at&.to_date
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
action = time_spent > 0 ? 'added' : 'subtracted'
text_parts = ["#{action} #{parsed_time} of time spent"]
text_parts << "at #{spent_at}" if spent_at && spent_at != DateTime.current.to_date
body = text_parts.join(' ')
issue_activity_counter.track_issue_time_spent_changed_action(author: author) if noteable.is_a?(Issue)
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
def remove_timelog(timelog)
time_spent = timelog.time_spent
spent_at = timelog.spent_at&.to_date

View File

@ -5,11 +5,26 @@ module Timelogs
include BaseServiceUtility
include Gitlab::Utils::StrongMemoize
attr_accessor :timelog, :current_user
attr_accessor :current_user
def initialize(timelog, user)
@timelog = timelog
def initialize(user)
@current_user = user
end
def success(timelog)
ServiceResponse.success(payload: {
timelog: timelog
})
end
def error(message, http_status = nil)
ServiceResponse.error(message: message, http_status: http_status)
end
def error_in_save(timelog)
return error(_("Failed to save timelog")) if timelog.errors.empty?
error(timelog.errors.full_messages.to_sentence)
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Timelogs
class CreateService < Timelogs::BaseService
attr_accessor :issuable, :time_spent, :spent_at, :summary
def initialize(issuable, time_spent, spent_at, summary, user)
super(user)
@issuable = issuable
@time_spent = time_spent
@spent_at = spent_at
@summary = summary
end
def execute
unless can?(current_user, :create_timelog, issuable)
return error(
_("%{issuable_class_name} doesn't exist or you don't have permission to add timelog to it.") % {
issuable_class_name: issuable.nil? ? 'Issuable' : issuable.base_class_name
}, 404)
end
issue = issuable if issuable.is_a?(Issue)
merge_request = issuable if issuable.is_a?(MergeRequest)
timelog = Timelog.new(
time_spent: time_spent,
spent_at: spent_at,
summary: summary,
user: current_user,
issue: issue,
merge_request: merge_request,
note: nil
)
if !timelog.save
error_in_save(timelog)
else
SystemNoteService.created_timelog(issuable, issuable.project, current_user, timelog)
success(timelog)
end
end
end
end

View File

@ -2,11 +2,17 @@
module Timelogs
class DeleteService < Timelogs::BaseService
attr_accessor :timelog
def initialize(timelog, user)
super(user)
@timelog = timelog
end
def execute
unless can?(current_user, :admin_timelog, timelog)
return ServiceResponse.error(
message: "Timelog doesn't exist or you don't have permission to delete it",
http_status: 404)
return error(_("Timelog doesn't exist or you don't have permission to delete it"), 404)
end
if timelog.destroy
@ -17,9 +23,9 @@ module Timelogs
SystemNoteService.remove_timelog(issuable, issuable.project, current_user, timelog)
end
ServiceResponse.success(payload: timelog)
success(timelog)
else
ServiceResponse.error(message: 'Failed to remove timelog', http_status: 400)
error(_('Failed to remove timelog'), 400)
end
end
end

View File

@ -1,11 +1,7 @@
- if Feature.enabled?(:edit_tag_release_notes_via_release_page, project)
- release_btn_text = s_('TagsPage|Create release')
- release_btn_path = new_project_release_path(project, tag_name: tag.name)
- if release
- release_btn_text = s_('TagsPage|Edit release')
- release_btn_path = edit_project_release_path(project, release)
= link_to release_btn_path, class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: release_btn_text, data: { container: "body" } do
= sprite_icon('pencil', css_class: 'gl-icon')
- else
= link_to edit_project_tag_release_path(project, tag.name), class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
= sprite_icon('pencil', css_class: 'gl-icon')
- release_btn_text = s_('TagsPage|Create release')
- release_btn_path = new_project_release_path(project, tag_name: tag.name)
- if release
- release_btn_text = s_('TagsPage|Edit release')
- release_btn_path = edit_project_release_path(project, release)
= link_to release_btn_path, class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: release_btn_text, data: { container: "body" } do
= sprite_icon('pencil', css_class: 'gl-icon')

View File

@ -1,19 +0,0 @@
- add_to_breadcrumbs _("Tags"), project_tags_path(@project)
- breadcrumb_title @tag.name
- page_title _("Edit"), @tag.name, _("Tags")
.sub-header-block.no-bottom-space
.oneline
.title
Release notes for tag
%strong= @tag.name
= form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name),
html: { class: 'common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'shared/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
.error-alert
.gl-mt-5.gl-display-flex
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3'
= link_to _('Cancel'), project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel"

View File

@ -28,7 +28,7 @@
title: _("Add a collapsible section") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
= markdown_toolbar_button({ icon: "paperclip",
data: { "md-tag" => "", "md-prepend" => true, "testid" => "button-attach-file" },
data: { "testid" => "button-attach-file" },
css_class: 'js-attach-file-button markdown-selector',
title: _("Attach a file or image") })
- if show_fullscreen_button

View File

@ -1,8 +0,0 @@
---
name: edit_tag_release_notes_via_release_page
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88832
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366244
milestone: '15.2'
type: development
group: group::release
default_enabled: false

View File

@ -51,10 +51,7 @@ scope format: false do
end
delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy] do
resource :release, controller: 'tags/releases', only: [:edit, :update]
end
resources :tags, only: [:index, :show, :new, :create, :destroy]
resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::PathRegex.git_reference_regex }
resources :protected_tags, only: [:index, :show, :create, :update, :destroy]
end

View File

@ -3,7 +3,7 @@
announcement_date: "2022-01-22"
removal_milestone: "15.0"
removal_date: "2022-05-22"
breaking_change: false
breaking_change: true
body: |
Currently, test coverage visualizations in GitLab only support Cobertura reports. Starting 15.0, the
`artifacts:reports:cobertura` keyword will be replaced by

View File

@ -3,7 +3,7 @@
announcement_date: "2022-02-22"
removal_milestone: "15.0"
removal_date: "2022-05-22"
breaking_change: false
breaking_change: true
body: |
As of GitLab 15.0, the [`artifacts:reports:cobertura`](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscobertura-removed)
keyword has been [replaced](https://gitlab.com/gitlab-org/gitlab/-/issues/344533) by

View File

@ -11,7 +11,9 @@ This table lists only GitLab versions where a significant change happened in the
package regarding PostgreSQL versions, not all.
Usually, PostgreSQL versions change with major or minor GitLab releases. However, patch versions
of Omnibus GitLab sometimes update the patch level of PostgreSQL.
of Omnibus GitLab sometimes update the patch level of PostgreSQL. We've established a
[yearly cadence for PostgreSQL upgrades](https://about.gitlab.com/handbook/engineering/development/enablement/data_stores/database/postgresql-upgrade-cadence.html)
and trigger automatic database upgrades in the release before the new version is required.
For example:

View File

@ -4815,6 +4815,28 @@ Input type: `TimelineEventUpdateInput`
| <a id="mutationtimelineeventupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventupdatetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.timelogCreate`
Input type: `TimelogCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelogcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelogcreateissuableid"></a>`issuableId` | [`IssuableID!`](#issuableid) | Global ID of the issuable (Issue, WorkItem or MergeRequest). |
| <a id="mutationtimelogcreatespentat"></a>`spentAt` | [`Date!`](#date) | When the time was spent. |
| <a id="mutationtimelogcreatesummary"></a>`summary` | [`String!`](#string) | Summary of time spent. |
| <a id="mutationtimelogcreatetimespent"></a>`timeSpent` | [`String!`](#string) | Amount of time spent. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelogcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelogcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelogcreatetimelog"></a>`timelog` | [`Timelog`](#timelog) | Timelog. |
### `Mutation.timelogDelete`
Input type: `TimelogDeleteInput`
@ -4832,7 +4854,7 @@ Input type: `TimelogDeleteInput`
| ---- | ---- | ----------- |
| <a id="mutationtimelogdeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelogdeleteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelogdeletetimelog"></a>`timelog` | [`Timelog`](#timelog) | Deleted timelog. |
| <a id="mutationtimelogdeletetimelog"></a>`timelog` | [`Timelog`](#timelog) | Timelog. |
### `Mutation.todoCreate`
@ -5587,6 +5609,7 @@ Input type: `WorkItemCreateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemcreateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="mutationworkitemcreatedescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="mutationworkitemcreatehierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | Input for hierarchy widget. |
| <a id="mutationworkitemcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project the work item is associated with. |
@ -5695,6 +5718,7 @@ Input type: `WorkItemUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemupdateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="mutationworkitemupdatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
| <a id="mutationworkitemupdatehierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. |
| <a id="mutationworkitemupdateid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
@ -22394,6 +22418,7 @@ A time-frame defined as a closed inclusive range of two dates.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemupdatedtaskinputconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="workitemupdatedtaskinputdescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
| <a id="workitemupdatedtaskinputhierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. |
| <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |

View File

@ -1496,12 +1496,16 @@ Tracing in GitLab is an integration with Jaeger, an open-source end-to-end distr
</div>
<div class="deprecation removal-150">
<div class="deprecation removal-150 breaking-change">
### `artifacts:reports:cobertura` keyword
Planned removal: GitLab <span class="removal-milestone">15.0</span> (2022-05-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
Review the details carefully before upgrading.
Currently, test coverage visualizations in GitLab only support Cobertura reports. Starting 15.0, the
`artifacts:reports:cobertura` keyword will be replaced by
[`artifacts:reports:coverage_report`](https://gitlab.com/gitlab-org/gitlab/-/issues/344533). Cobertura will be the

View File

@ -621,6 +621,10 @@ The `Managed-Cluster-Applications.gitlab-ci.yml` CI/CD template is being removed
### `artifacts:reports:cobertura` keyword
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes).
Review the details carefully before upgrading.
As of GitLab 15.0, the [`artifacts:reports:cobertura`](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscobertura-removed)
keyword has been [replaced](https://gitlab.com/gitlab-org/gitlab/-/issues/344533) by
[`artifacts:reports:coverage_report`](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscoverage_report).

View File

@ -694,6 +694,9 @@ msgstr ""
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
msgid "%{issuable_class_name} doesn't exist or you don't have permission to add timelog to it."
msgstr ""
msgid "%{issuable}(s) already assigned"
msgstr ""
@ -15946,6 +15949,9 @@ msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
msgid "Failed to remove timelog"
msgstr ""
msgid "Failed to remove user identity."
msgstr ""
@ -15970,6 +15976,9 @@ msgstr ""
msgid "Failed to save preferences."
msgstr ""
msgid "Failed to save timelog"
msgstr ""
msgid "Failed to set due date because the date format is invalid."
msgstr ""
@ -38174,9 +38183,6 @@ msgstr ""
msgid "TagsPage|Edit release"
msgstr ""
msgid "TagsPage|Edit release notes"
msgstr ""
msgid "TagsPage|Existing branch name, tag, or commit SHA"
msgstr ""
@ -40403,6 +40409,9 @@ msgstr ""
msgid "Timeline|Turn recent updates view on"
msgstr ""
msgid "Timelog doesn't exist or you don't have permission to delete it"
msgstr ""
msgid "Timeout"
msgstr ""

View File

@ -1,103 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Tags::ReleasesController do
let!(:project) { create(:project, :repository) }
let!(:user) { create(:user) }
let!(:release) { create(:release, project: project, tag: "v1.1.0") }
let!(:tag) { release.tag }
before do
project.add_developer(user)
sign_in(user)
end
describe 'GET #edit' do
it 'initializes a new release' do
tag_id = release.tag
project.releases.destroy_all # rubocop: disable Cop/DestroyAll
response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).not_to be_persisted
expect(response).to have_gitlab_http_status(:ok)
end
it 'retrieves an existing release' do
response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).to be_persisted
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'PUT #update' do
it 'updates release note description' do
response = update_release(release.tag, "description updated")
release = project.releases.find_by(tag: tag)
expect(release.description).to eq("description updated")
expect(response).to have_gitlab_http_status(:found)
end
it 'creates a release if one does not exist' do
tag_without_release = create_new_tag
expect do
update_release(tag_without_release.name, "a new release")
end.to change { project.releases.count }.by(1)
expect(response).to have_gitlab_http_status(:found)
end
it 'sets the release name, sha, and author for a new release' do
tag_without_release = create_new_tag
response = update_release(tag_without_release.name, "a new release")
release = project.releases.find_by(tag: tag_without_release.name)
expect(release.name).to eq(tag_without_release.name)
expect(release.sha).to eq(tag_without_release.target_commit.sha)
expect(release.author.id).to eq(user.id)
expect(response).to have_gitlab_http_status(:found)
end
it 'does not delete release when description is empty' do
expect do
update_release(tag, "")
end.not_to change { project.releases.count }
expect(release.reload.description).to eq("")
expect(response).to have_gitlab_http_status(:found)
end
it 'does nothing when description is empty and the tag does not have a release' do
tag_without_release = create_new_tag
expect do
update_release(tag_without_release.name, "")
end.not_to change { project.releases.count }
expect(response).to have_gitlab_http_status(:found)
end
end
def create_new_tag
project.repository.add_tag(user, 'mytag', 'master')
end
def update_release(tag_id, description)
put :update, params: {
namespace_id: project.namespace.to_param,
project_id: project,
tag_id: tag_id,
release: { description: description }
}
end
end

View File

@ -15,6 +15,13 @@ RSpec.describe 'Project > Tags', :js do
end
shared_examples "can create and update release" do
it 'shows tag information' do
visit page_url
expect(page).to have_content 'v1.1.0'
expect(page).to have_content 'Version 1.1.0'
end
it 'can create new release' do
visit page_url
page.find("a[href=\"#{new_project_release_path(project, tag_name: 'v1.1.0')}\"]").click
@ -52,71 +59,4 @@ RSpec.describe 'Project > Tags', :js do
include_examples "can create and update release"
end
# TODO: remove most of these together with FF https://gitlab.com/gitlab-org/gitlab/-/issues/366244
describe 'when opening project tags' do
before do
stub_feature_flags(edit_tag_release_notes_via_release_page: false)
visit project_tags_path(project)
end
context 'page with tags list' do
it 'shows tag name' do
expect(page).to have_content 'v1.1.0'
expect(page).to have_content 'Version 1.1.0'
end
it 'shows tag edit button' do
page.within '.tags > .content-list' do
edit_btn = page.find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']")
expect(edit_btn['href']).to end_with("/#{project.full_path}/-/tags/v1.1.0/release/edit")
end
end
end
context 'edit tag release notes' do
before do
page.find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']").click
end
it 'shows tag name header' do
page.within('.content') do
expect(page.find('.sub-header-block')).to have_content 'Release notes for tag v1.1.0'
end
end
it 'shows release notes form' do
page.within('.content') do
expect(page).to have_selector('form.release-form')
end
end
it 'toolbar buttons on release notes form are functional' do
page.within('.content form.release-form') do
note_textarea = page.find('.js-gfm-input')
# Click on Bold button
page.find('.md-header-toolbar button:first-child').click
expect(note_textarea.value).to eq('****')
end
end
it 'release notes form shows "Attach a file or image" button', :js do
page.within('.content form.release-form') do
expect(page).to have_selector('[data-testid="button-attach-file"]')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
it 'shows "Attaching a file" message on uploading 1 file', :js, :capybara_ignore_server_errors do
slow_requests do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -')
end
end
end
end
end

View File

@ -1,56 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
# TODO: remove this file together with FF https://gitlab.com/gitlab-org/gitlab/-/issues/366244
RSpec.describe 'Developer updates tag' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
before do
project.add_developer(user)
sign_in(user)
stub_feature_flags(edit_tag_release_notes_via_release_page: false)
visit project_tags_path(project)
end
context 'from the tags list page' do
it 'updates the release notes' do
find("li > .row-fixed-content.controls a.btn-edit[href='/#{project.full_path}/-/tags/v1.1.0/release/edit']").click
fill_in 'release_description', with: 'Awesome release notes'
click_button 'Save changes'
expect(page).to have_current_path(
project_tag_path(project, 'v1.1.0'), ignore_query: true)
expect(page).to have_content 'v1.1.0'
expect(page).to have_content 'Awesome release notes'
end
it 'description has emoji autocomplete', :js do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
find('#release_description').native.send_keys('')
fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
end
context 'from a specific tag page' do
it 'updates the release notes' do
click_on 'v1.1.0'
click_link 'Edit release notes'
fill_in 'release_description', with: 'Awesome release notes'
click_button 'Save changes'
expect(page).to have_current_path(
project_tag_path(project, 'v1.1.0'), ignore_query: true)
expect(page).to have_content 'v1.1.0'
expect(page).to have_content 'Awesome release notes'
end
end
end

View File

@ -56,7 +56,6 @@ describe('Markdown field header component', () => {
'Add a task list',
'Add a collapsible section',
'Add a table',
'Attach a file or image',
'Go full screen',
];
const elements = findToolbarButtons();
@ -66,6 +65,13 @@ describe('Markdown field header component', () => {
});
});
it('renders "Attach a file or image" button using gl-button', () => {
const button = wrapper.findByTestId('button-attach-file');
expect(button.element.tagName).toBe('GL-BUTTON-STUB');
expect(button.attributes('title')).toBe('Attach a file or image');
});
describe('when the user is on a non-Mac', () => {
beforeEach(() => {
delete window.gl.client.isMac;

View File

@ -113,5 +113,45 @@ RSpec.describe IssuablePolicy, models: true do
end
end
end
context 'when user is anonymous' do
it 'does not allow timelogs creation' do
expect(permissions(nil, issue)).to be_disallowed(:create_timelog)
end
end
context 'when user is not a member of the project' do
it 'does not allow timelogs creation' do
expect(policies).to be_disallowed(:create_timelog)
end
end
context 'when user is not a member of the project but the author of the issuable' do
let(:issue) { create(:issue, project: project, author: user) }
it 'does not allow timelogs creation' do
expect(policies).to be_disallowed(:create_timelog)
end
end
context 'when user is a guest member of the project' do
it 'does not allow timelogs creation' do
expect(permissions(guest, issue)).to be_disallowed(:create_timelog)
end
end
context 'when user is a guest member of the project and the author of the issuable' do
let(:issue) { create(:issue, project: project, author: guest) }
it 'does not allow timelogs creation' do
expect(permissions(guest, issue)).to be_disallowed(:create_timelog)
end
end
context 'when user is at least reporter of the project' do
it 'allows timelogs creation' do
expect(permissions(reporter, issue)).to be_allowed(:create_timelog)
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create a timelog' do
include GraphqlHelpers
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:time_spent) { '1h' }
let(:current_user) { nil }
let(:users_container) { project }
let(:mutation) do
graphql_mutation(:timelogCreate, {
'time_spent' => time_spent,
'spent_at' => '2022-07-08',
'summary' => 'Test summary',
'issuable_id' => issuable.to_global_id.to_s
})
end
let(:mutation_response) { graphql_mutation_response(:timelog_create) }
context 'when issuable is an Issue' do
let_it_be(:issuable) { create(:issue, project: project) }
it_behaves_like 'issuable supports timelog creation mutation'
end
context 'when issuable is a MergeRequest' do
let_it_be(:issuable) { create(:merge_request, source_project: project) }
it_behaves_like 'issuable supports timelog creation mutation'
end
context 'when issuable is a WorkItem' do
let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem') }
it_behaves_like 'issuable supports timelog creation mutation'
end
context 'when issuable is an Incident' do
let_it_be(:issuable) { create(:incident, project: project) }
it_behaves_like 'issuable supports timelog creation mutation'
end
end

View File

@ -12,6 +12,7 @@ RSpec.describe 'Create a work item' do
{
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
}
end
@ -38,6 +39,7 @@ RSpec.describe 'Create a work item' do
expect(response).to have_gitlab_http_status(:success)
expect(created_work_item.issue_type).to eq('task')
expect(created_work_item).to be_confidential
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(mutation_response['workItem']).to include(
input.except('workItemTypeId').merge(

View File

@ -34,6 +34,10 @@ RSpec.describe 'Update a work item' do
context 'when user has permissions to update a work item' do
let(:current_user) { developer }
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::Update }
end
context 'when the work item is open' do
it 'closes and updates the work item' do
expect do
@ -71,36 +75,48 @@ RSpec.describe 'Update a work item' do
end
end
context 'when unsupported widget input is sent' do
let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
let(:input) do
{
'hierarchyWidget' => {}
context 'when updating confidentiality' do
let(:fields) do
<<~FIELDS
workItem {
confidential
}
errors
FIELDS
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
end
shared_examples 'toggling confidentiality' do
it 'successfully updates work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to change(work_item, :confidential).from(values[:old]).to(values[:new])
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::Update }
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['workItem']).to include(
'confidential' => values[:new]
)
end
end
it 'does not update the work item and returns and error' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to not_change(work_item, :title)
context 'when setting as confidential' do
let(:input) { { 'confidential' => true } }
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
it_behaves_like 'toggling confidentiality' do
let(:values) { { old: false, new: true }}
end
end
context 'when setting as non-confidential' do
let(:input) { { 'confidential' => false } }
before do
work_item.update!(confidential: true)
end
it_behaves_like 'toggling confidentiality' do
let(:values) { { old: true, new: false }}
end
end
end
@ -322,5 +338,34 @@ RSpec.describe 'Update a work item' do
end
end
end
context 'when unsupported widget input is sent' do
let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
let(:input) do
{
'hierarchyWidget' => {}
}
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"]
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'does not update the work item and returns and error' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to not_change(work_item, :title)
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
end
end
end
end

View File

@ -111,14 +111,6 @@ RSpec.describe Releases::CreateService do
expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
end
end
end
describe '#find_or_build_release' do
it 'does not save the built release' do
service.find_or_build_release
expect(project.releases.count).to eq(0)
end
context 'when existing milestone is passed in' do
let(:title) { 'v1.0' }

View File

@ -432,6 +432,19 @@ RSpec.describe SystemNoteService do
end
end
describe '.created_timelog' do
let(:issue) { create(:issue, project: project) }
let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)}
it 'calls TimeTrackingService' do
expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service|
expect(service).to receive(:created_timelog)
end
described_class.created_timelog(noteable, project, author, timelog)
end
end
describe '.remove_timelog' do
let(:issue) { create(:issue, project: project) }
let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)}

View File

@ -106,6 +106,30 @@ RSpec.describe ::SystemNotes::TimeTrackingService do
end
end
describe '#create_timelog' do
subject { described_class.new(noteable: noteable, project: project, author: author).created_timelog(timelog) }
context 'when the timelog has a positive time spent value' do
let_it_be(:noteable, reload: true) { create(:issue, project: project) }
let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z')}
it 'sets the note text' do
expect(subject.note).to eq "added 30m of time spent at 2022-03-30"
end
end
context 'when the timelog has a negative time spent value' do
let_it_be(:noteable, reload: true) { create(:issue, project: project) }
let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z')}
it 'sets the note text' do
expect(subject.note).to eq "subtracted 30m of time spent at 2022-03-30"
end
end
end
describe '#remove_timelog' do
subject { described_class.new(noteable: noteable, project: project, author: author).remove_timelog(timelog) }

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Timelogs::CreateService do
let_it_be(:author) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:time_spent) { 3600 }
let_it_be(:spent_at) { "2022-07-08" }
let_it_be(:summary) { "Test summary" }
let(:issuable) { nil }
let(:users_container) { project }
let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) }
describe '#execute' do
subject { service.execute }
context 'when issuable is an Issue' do
let_it_be(:issuable) { create(:issue, project: project) }
let_it_be(:note_noteable) { create(:issue, project: project) }
it_behaves_like 'issuable supports timelog creation service'
end
context 'when issuable is a MergeRequest' do
let_it_be(:issuable) { create(:merge_request, source_project: project, source_branch: 'branch-1') }
let_it_be(:note_noteable) { create(:merge_request, source_project: project, source_branch: 'branch-2') }
it_behaves_like 'issuable supports timelog creation service'
end
context 'when issuable is a WorkItem' do
let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem-1') }
let_it_be(:note_noteable) { create(:work_item, project: project, title: 'WorkItem-2') }
it_behaves_like 'issuable supports timelog creation service'
end
context 'when issuable is an Incident' do
let_it_be(:issuable) { create(:incident, project: project) }
let_it_be(:note_noteable) { create(:incident, project: project) }
it_behaves_like 'issuable supports timelog creation service'
end
end
end

View File

@ -21,8 +21,8 @@ RSpec.describe Timelogs::DeleteService do
end
it 'returns the removed timelog' do
expect(subject).to be_success
expect(subject.payload).to eq(timelog)
is_expected.to be_success
expect(subject.payload[:timelog]).to eq(timelog)
end
end
@ -31,7 +31,7 @@ RSpec.describe Timelogs::DeleteService do
let!(:timelog) { nil }
it 'returns an error' do
expect(subject).to be_error
is_expected.to be_error
expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it')
expect(subject.http_status).to eq(404)
end
@ -41,7 +41,7 @@ RSpec.describe Timelogs::DeleteService do
let(:user) { create(:user) }
it 'returns an error' do
expect(subject).to be_error
is_expected.to be_error
expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it')
expect(subject.http_status).to eq(404)
end
@ -56,7 +56,7 @@ RSpec.describe Timelogs::DeleteService do
end
it 'returns an error' do
expect(subject).to be_error
is_expected.to be_error
expect(subject.message).to eq('Failed to remove timelog')
expect(subject.http_status).to eq(400)
end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable supports timelog creation mutation' do
context 'when the user is anonymous' do
before do
post_graphql_mutation(mutation, current_user: current_user)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the user is a guest member of the namespace' do
let(:current_user) { create(:user) }
before do
users_container.add_guest(current_user)
post_graphql_mutation(mutation, current_user: current_user)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to create a timelog' do
let(:current_user) { author }
before do
users_container.add_reporter(current_user)
end
context 'with valid data' do
it 'creates the timelog' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(Timelog, :count).by(1)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty
expect(mutation_response['timelog']).to include(
'timeSpent' => 3600,
'spentAt' => '2022-07-08T00:00:00Z',
'summary' => 'Test summary'
)
end
end
context 'with invalid time_spent' do
let(:time_spent) { '3h e' }
it 'returns an error' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(Timelog, :count).by(0)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to match_array(['Time spent can\'t be blank'])
expect(mutation_response['timelog']).to be_nil
end
end
end
end
RSpec.shared_examples 'issuable does not support timelog creation mutation' do
context 'when the user is anonymous' do
before do
post_graphql_mutation(mutation, current_user: current_user)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the user is a guest member of the namespace' do
let(:current_user) { create(:user) }
before do
users_container.add_guest(current_user)
post_graphql_mutation(mutation, current_user: current_user)
end
it_behaves_like 'a mutation that returns top-level errors' do
let(:match_errors) { contain_exactly(include('is not a valid ID for')) }
end
end
context 'when user has permissions to create a timelog' do
let(:current_user) { author }
before do
users_container.add_reporter(current_user)
end
it_behaves_like 'a mutation that returns top-level errors' do
let(:match_errors) { contain_exactly(include('is not a valid ID for')) }
end
end
end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable supports timelog creation service' do
shared_examples 'success_response' do
it 'sucessfully saves the timelog' do
is_expected.to be_success
timelog = subject.payload[:timelog]
expect(timelog).to be_persisted
expect(timelog.time_spent).to eq(time_spent)
expect(timelog.spent_at).to eq('Fri, 08 Jul 2022 00:00:00.000000000 UTC +00:00')
expect(timelog.summary).to eq(summary)
expect(timelog.issuable).to eq(issuable)
end
end
context 'when the user does not have permission' do
let(:user) { create(:user) }
it 'returns an error' do
is_expected.to be_error
expect(subject.message).to eq(
"#{issuable.base_class_name} doesn't exist or you don't have permission to add timelog to it.")
expect(subject.http_status).to eq(404)
end
end
context 'when the user has permissions' do
let(:user) { author }
before do
users_container.add_reporter(user)
end
context 'when the timelog save fails' do
before do
allow_next_instance_of(Timelog) do |timelog|
allow(timelog).to receive(:save).and_return(false)
end
end
it 'returns an error' do
is_expected.to be_error
expect(subject.message).to eq('Failed to save timelog')
end
end
context 'when the creation completes sucessfully' do
it_behaves_like 'success_response'
end
end
end
RSpec.shared_examples 'issuable does not support timelog creation service' do
shared_examples 'error_response' do
it 'returns an error' do
is_expected.to be_error
issuable_type = if issuable.nil?
'Issuable'
else
issuable.base_class_name
end
expect(subject.message).to eq(
"#{issuable_type} doesn't exist or you don't have permission to add timelog to it."
)
expect(subject.http_status).to eq(404)
end
end
context 'when the user does not have permission' do
let(:user) { create(:user) }
it_behaves_like 'error_response'
end
context 'when the user has permissions' do
let(:user) { author }
before do
users_container.add_reporter(user)
end
it_behaves_like 'error_response'
end
end