Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a4068557f4
commit
2977cf67ec
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue