Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5bb54b8711
commit
d12d801795
|
|
@ -4583,7 +4583,7 @@ Layout/LineLength:
|
|||
- 'spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/releases_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/snippets_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/todo_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/todos_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/user_discussions_count_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/users/group_count_resolver_spec.rb'
|
||||
- 'spec/graphql/resolvers/users/groups_resolver_spec.rb'
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -145,9 +145,9 @@ gem 'seed-fu', '~> 2.3.7'
|
|||
gem 'elasticsearch-model', '~> 7.2'
|
||||
gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation'
|
||||
gem 'elasticsearch-api', '7.13.3'
|
||||
gem 'aws-sdk-core', '~> 3'
|
||||
gem 'aws-sdk-core', '~> 3.131.0'
|
||||
gem 'aws-sdk-cloudformation', '~> 1'
|
||||
gem 'aws-sdk-s3', '~> 1'
|
||||
gem 'aws-sdk-s3', '~> 1.114.0'
|
||||
gem 'faraday_middleware-aws-sigv4', '~>0.3.0'
|
||||
gem 'typhoeus', '~> 1.4.0' # Used with Elasticsearch to support http keep-alive connections
|
||||
|
||||
|
|
|
|||
28
Gemfile.lock
28
Gemfile.lock
|
|
@ -105,24 +105,24 @@ GEM
|
|||
execjs (> 0)
|
||||
awesome_print (1.9.2)
|
||||
awrence (1.1.1)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.345.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.600.0)
|
||||
aws-sdk-cloudformation (1.41.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-core (3.104.3)
|
||||
aws-sdk-core (3.131.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.36.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.75.0)
|
||||
aws-sdk-core (~> 3, >= 3.104.1)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
|
|
@ -679,7 +679,7 @@ GEM
|
|||
atlassian-jwt
|
||||
multipart-post
|
||||
oauth (~> 0.5, >= 0.5.0)
|
||||
jmespath (1.4.0)
|
||||
jmespath (1.6.1)
|
||||
js_regex (3.7.0)
|
||||
character_set (~> 1.4)
|
||||
regexp_parser (~> 2.1)
|
||||
|
|
@ -1464,8 +1464,8 @@ DEPENDENCIES
|
|||
autoprefixer-rails (= 10.2.5.1)
|
||||
awesome_print
|
||||
aws-sdk-cloudformation (~> 1)
|
||||
aws-sdk-core (~> 3)
|
||||
aws-sdk-s3 (~> 1)
|
||||
aws-sdk-core (~> 3.131.0)
|
||||
aws-sdk-s3 (~> 1.114.0)
|
||||
babosa (~> 1.0.4)
|
||||
base32 (~> 0.3.0)
|
||||
batch-loader (~> 2.0.1)
|
||||
|
|
|
|||
|
|
@ -68,8 +68,7 @@ export default class IssuableForm {
|
|||
this.gfmAutoComplete = new GfmAutoComplete(
|
||||
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
|
||||
).setup();
|
||||
const autoAssignToMe = form.get(0).id === 'new_merge_request';
|
||||
this.usersSelect = new UsersSelect(undefined, undefined, { autoAssignToMe });
|
||||
this.usersSelect = new UsersSelect();
|
||||
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
|
||||
this.zenMode = new ZenMode();
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,9 @@ export default {
|
|||
<template>
|
||||
<section class="settings no-animate js-self-monitoring-settings">
|
||||
<div class="settings-header">
|
||||
<h4 class="js-section-header">
|
||||
<h4
|
||||
class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only"
|
||||
>
|
||||
{{ s__('SelfMonitoring|Self monitoring') }}
|
||||
</h4>
|
||||
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
const { handleClick, autoAssignToMe } = options;
|
||||
const { handleClick } = options;
|
||||
const userSelect = this;
|
||||
|
||||
$els.each((i, dropdown) => {
|
||||
|
|
@ -172,7 +172,10 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
});
|
||||
};
|
||||
|
||||
const onAssignToMeClick = () => {
|
||||
$assignToMeLink.on('click', (e) => {
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).hide();
|
||||
|
||||
if ($dropdown.data('multiSelect')) {
|
||||
assignYourself();
|
||||
checkMaxSelect();
|
||||
|
|
@ -191,19 +194,8 @@ function UsersSelect(currentUser, els, options = {}) {
|
|||
.text(gon.current_user_fullname)
|
||||
.removeClass('is-default');
|
||||
}
|
||||
};
|
||||
|
||||
$assignToMeLink.on('click', (e) => {
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).hide();
|
||||
onAssignToMeClick();
|
||||
});
|
||||
|
||||
if (autoAssignToMe) {
|
||||
$assignToMeLink.hide();
|
||||
onAssignToMeClick();
|
||||
}
|
||||
|
||||
$block.on('click', '.js-assign-yourself', (e) => {
|
||||
e.preventDefault();
|
||||
return assignTo(userSelect.currentUser.id);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,23 @@
|
|||
<script>
|
||||
import { GlTokenSelector, GlIcon, GlAvatar, GlLink } from '@gitlab/ui';
|
||||
import { GlTokenSelector, GlIcon, GlAvatar, GlLink, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import { n__ } from '~/locale';
|
||||
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
|
||||
import { i18n } from '../constants';
|
||||
|
||||
function isClosingIcon(el) {
|
||||
return el?.classList.contains('gl-token-close');
|
||||
function isTokenSelectorElement(el) {
|
||||
return el?.classList.contains('gl-token-close') || el?.classList.contains('dropdown-item');
|
||||
}
|
||||
|
||||
function addClass(el) {
|
||||
return {
|
||||
...el,
|
||||
class: 'gl-bg-transparent',
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
@ -13,7 +26,10 @@ export default {
|
|||
GlIcon,
|
||||
GlAvatar,
|
||||
GlLink,
|
||||
GlSkeletonLoader,
|
||||
SidebarParticipant,
|
||||
},
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
|
|
@ -27,45 +43,95 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
localAssignees: this.assignees.map((assignee) => ({
|
||||
...assignee,
|
||||
class: 'gl-bg-transparent!',
|
||||
})),
|
||||
searchStarted: false,
|
||||
localAssignees: this.assignees.map(addClass),
|
||||
searchKey: '',
|
||||
searchUsers: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
assigneeIds() {
|
||||
return this.localAssignees.map((assignee) => assignee.id);
|
||||
apollo: {
|
||||
searchUsers: {
|
||||
query() {
|
||||
return userSearchQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
search: this.searchKey,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.searchStarted;
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.users?.nodes.map((node) => addClass({ ...node, ...node.user }));
|
||||
},
|
||||
error() {
|
||||
this.$emit('error', i18n.fetchError);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
assigneeListEmpty() {
|
||||
return this.assignees.length === 0;
|
||||
},
|
||||
containerClass() {
|
||||
return !this.isEditing ? 'gl-shadow-none! gl-bg-transparent!' : '';
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.searchUsers.loading;
|
||||
},
|
||||
assigneeText() {
|
||||
return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
assignees(newVal) {
|
||||
if (!this.isEditing) {
|
||||
this.localAssignees = newVal.map(addClass);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
methods: {
|
||||
getUserId(id) {
|
||||
return getIdFromGraphQLId(id);
|
||||
},
|
||||
setAssignees(e) {
|
||||
if (isClosingIcon(e.relatedTarget) || !this.isEditing) return;
|
||||
if (isTokenSelectorElement(e.relatedTarget) || !this.isEditing) return;
|
||||
this.isEditing = false;
|
||||
this.$apollo.mutate({
|
||||
mutation: localUpdateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
assigneeIds: this.assigneeIds,
|
||||
assignees: this.localAssignees,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
async focusTokenSelector() {
|
||||
handleFocus() {
|
||||
this.isEditing = true;
|
||||
this.searchStarted = true;
|
||||
},
|
||||
async focusTokenSelector() {
|
||||
this.handleFocus();
|
||||
await this.$nextTick();
|
||||
this.$refs.tokenSelector.focusTextInput();
|
||||
},
|
||||
handleMouseOver() {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.searchStarted = true;
|
||||
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
},
|
||||
handleMouseOut() {
|
||||
clearTimeout(this.timeout);
|
||||
},
|
||||
setSearchKey(value) {
|
||||
this.searchKey = value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -73,17 +139,21 @@ export default {
|
|||
<template>
|
||||
<div class="gl-display-flex gl-mb-4 work-item-assignees gl-relative">
|
||||
<span class="gl-font-weight-bold gl-w-15 gl-pt-2" data-testid="assignees-title">{{
|
||||
__('Assignee(s)')
|
||||
assigneeText
|
||||
}}</span>
|
||||
<gl-token-selector
|
||||
ref="tokenSelector"
|
||||
v-model="localAssignees"
|
||||
hide-dropdown-with-no-items
|
||||
:container-class="containerClass"
|
||||
:dropdown-items="searchUsers"
|
||||
:loading="isLoading"
|
||||
class="gl-w-full gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
|
||||
@token-remove="focusTokenSelector"
|
||||
@focus="isEditing = true"
|
||||
@input="focusTokenSelector"
|
||||
@text-input="debouncedSearchKeyUpdate"
|
||||
@focus="handleFocus"
|
||||
@blur="setAssignees"
|
||||
@mouseover.native="handleMouseOver"
|
||||
@mouseout.native="handleMouseOut"
|
||||
>
|
||||
<template #empty-placeholder>
|
||||
<div
|
||||
|
|
@ -106,6 +176,17 @@ export default {
|
|||
<span class="gl-pl-2">{{ token.name }}</span>
|
||||
</gl-link>
|
||||
</template>
|
||||
<template #dropdown-item-content="{ dropdownItem }">
|
||||
<sidebar-participant :user="dropdownItem" />
|
||||
</template>
|
||||
<template #loading-content>
|
||||
<gl-skeleton-loader :height="170">
|
||||
<rect width="380" height="20" x="10" y="15" rx="4" />
|
||||
<rect width="280" height="20" x="10" y="50" rx="4" />
|
||||
<rect width="380" height="20" x="10" y="95" rx="4" />
|
||||
<rect width="280" height="20" x="10" y="130" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</template>
|
||||
</gl-token-selector>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -70,9 +70,7 @@ export const resolvers = {
|
|||
const assigneesWidget = draftData.workItem.mockWidgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_ASSIGNEE,
|
||||
);
|
||||
assigneesWidget.nodes = assigneesWidget.nodes.filter((assignee) =>
|
||||
input.assigneeIds.includes(assignee.id),
|
||||
);
|
||||
assigneesWidget.nodes = [...input.assignees];
|
||||
});
|
||||
|
||||
cache.writeQuery({
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ extend type WorkItem {
|
|||
|
||||
type LocalWorkItemAssigneesInput {
|
||||
id: WorkItemID!
|
||||
assigneeIds: [ID!]
|
||||
assignees: [UserCore!]
|
||||
}
|
||||
|
||||
type LocalWorkItemPayload {
|
||||
|
|
|
|||
|
|
@ -32,3 +32,4 @@
|
|||
@import './pages/storage_quota';
|
||||
@import './pages/tree';
|
||||
@import './pages/users';
|
||||
@import './pages/work_items';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
.gl-token-selector-token-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -253,11 +253,6 @@ $gl-line-height-42: px-to-rem(42px);
|
|||
max-width: 50%;
|
||||
}
|
||||
|
||||
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2894
|
||||
.gl-form-lg {
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
/**
|
||||
Note: ::-webkit-scrollbar is a non-standard rule only
|
||||
supported by webkit browsers.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ module Mutations
|
|||
argument :title, GraphQL::Types::String,
|
||||
required: false,
|
||||
description: copy_field_description(Types::WorkItemType, :title)
|
||||
argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
|
||||
required: false,
|
||||
description: 'Input for description widget.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ module Mutations
|
|||
end
|
||||
|
||||
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
|
||||
widget_params = extract_widget_params(work_item, attributes)
|
||||
|
||||
::WorkItems::UpdateService.new(
|
||||
project: work_item.project,
|
||||
current_user: current_user,
|
||||
params: attributes,
|
||||
widget_params: widget_params,
|
||||
spam_params: spam_params
|
||||
).execute(work_item)
|
||||
|
||||
|
|
@ -45,6 +47,16 @@ module Mutations
|
|||
def find_object(id:)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
|
||||
def extract_widget_params(work_item, attributes)
|
||||
# Get the list of widgets for the work item's type to extract only the supported attributes
|
||||
widget_keys = work_item.work_item_type.widgets.map(&:api_symbol)
|
||||
widget_params = attributes.extract!(*widget_keys)
|
||||
|
||||
# Cannot use prepare to use `.to_h` on each input due to
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87472#note_945199865
|
||||
widget_params.transform_values { |values| values.to_h }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
module Mutations
|
||||
module WorkItems
|
||||
# TODO: Deprecate in favor of using WorkItemUpdate. See https://gitlab.com/gitlab-org/gitlab/-/issues/366300
|
||||
class UpdateWidgets < BaseMutation
|
||||
graphql_name 'WorkItemUpdateWidgets'
|
||||
description "Updates the attributes of a work item's widgets by global ID." \
|
||||
|
|
|
|||
|
|
@ -2,68 +2,16 @@
|
|||
|
||||
module Resolvers
|
||||
class TodoResolver < BaseResolver
|
||||
type Types::TodoType.connection_type, null: true
|
||||
description 'Retrieve a single to-do item'
|
||||
|
||||
alias_method :target, :object
|
||||
type Types::TodoType, null: true
|
||||
|
||||
argument :action, [Types::TodoActionEnum],
|
||||
required: false,
|
||||
description: 'Action to be filtered.'
|
||||
argument :id, Types::GlobalIDType[Todo],
|
||||
required: true,
|
||||
description: 'ID of the to-do item.'
|
||||
|
||||
argument :author_id, [GraphQL::Types::ID],
|
||||
required: false,
|
||||
description: 'ID of an author.'
|
||||
|
||||
argument :project_id, [GraphQL::Types::ID],
|
||||
required: false,
|
||||
description: 'ID of a project.'
|
||||
|
||||
argument :group_id, [GraphQL::Types::ID],
|
||||
required: false,
|
||||
description: 'ID of a group.'
|
||||
|
||||
argument :state, [Types::TodoStateEnum],
|
||||
required: false,
|
||||
description: 'State of the todo.'
|
||||
|
||||
argument :type, [Types::TodoTargetEnum],
|
||||
required: false,
|
||||
description: 'Type of the todo.'
|
||||
|
||||
before_connection_authorization do |nodes, current_user|
|
||||
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(
|
||||
nodes.map(&:project).compact,
|
||||
current_user
|
||||
).execute
|
||||
end
|
||||
|
||||
def resolve(**args)
|
||||
return Todo.none unless current_user.present? && target.present?
|
||||
return Todo.none if target.is_a?(User) && target != current_user
|
||||
|
||||
TodosFinder.new(current_user, todo_finder_params(args)).execute.with_entity_associations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def todo_finder_params(args)
|
||||
{
|
||||
state: args[:state],
|
||||
type: args[:type],
|
||||
group_id: args[:group_id],
|
||||
author_id: args[:author_id],
|
||||
action_id: args[:action],
|
||||
project_id: args[:project_id]
|
||||
}.merge(target_params)
|
||||
end
|
||||
|
||||
def target_params
|
||||
return {} unless TodosFinder::TODO_TYPES.include?(target.class.name)
|
||||
|
||||
{
|
||||
type: target.class.name,
|
||||
target_id: target.id
|
||||
}
|
||||
def resolve(id:)
|
||||
GitlabSchema.find_by_gid(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
class TodosResolver < BaseResolver
|
||||
type Types::TodoType.connection_type, null: true
|
||||
|
||||
alias_method :target, :object
|
||||
|
||||
argument :action, [Types::TodoActionEnum],
|
||||
required: false,
|
||||
description: 'Action to be filtered.'
|
||||
|
||||
argument :author_id, [GraphQL::Types::ID],
|
||||
required: false,
|
||||
description: 'ID of an author.'
|
||||
|
||||
argument :project_id, [GraphQL::Types::ID],
|
||||
required: false,
|
||||
description: 'ID of a project.'
|
||||
|
||||
argument :group_id, [GraphQL::Types::ID],
|
||||
required: false,
|
||||
description: 'ID of a group.'
|
||||
|
||||
argument :state, [Types::TodoStateEnum],
|
||||
required: false,
|
||||
description: 'State of the todo.'
|
||||
|
||||
argument :type, [Types::TodoTargetEnum],
|
||||
required: false,
|
||||
description: 'Type of the todo.'
|
||||
|
||||
before_connection_authorization do |nodes, current_user|
|
||||
Preloaders::UserMaxAccessLevelInProjectsPreloader.new(
|
||||
nodes.map(&:project).compact,
|
||||
current_user
|
||||
).execute
|
||||
end
|
||||
|
||||
def resolve(**args)
|
||||
return Todo.none unless current_user.present? && target.present?
|
||||
return Todo.none if target.is_a?(User) && target != current_user
|
||||
|
||||
TodosFinder.new(current_user, todo_finder_params(args)).execute.with_entity_associations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def todo_finder_params(args)
|
||||
{
|
||||
state: args[:state],
|
||||
type: args[:type],
|
||||
group_id: args[:group_id],
|
||||
author_id: args[:author_id],
|
||||
action_id: args[:action],
|
||||
project_id: args[:project_id]
|
||||
}.merge(target_params)
|
||||
end
|
||||
|
||||
def target_params
|
||||
return {} unless TodosFinder::TODO_TYPES.include?(target.class.name)
|
||||
|
||||
{
|
||||
type: target.class.name,
|
||||
target_id: target.id
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -116,7 +116,7 @@ module Types
|
|||
null: true,
|
||||
description: 'Runbook for the alert as defined in alert details.'
|
||||
|
||||
field :todos, description: 'To-do items of the current user for the alert.', resolver: Resolvers::TodoResolver
|
||||
field :todos, description: 'To-do items of the current user for the alert.', resolver: Resolvers::TodosResolver
|
||||
|
||||
field :details_url,
|
||||
GraphQL::Types::String,
|
||||
|
|
|
|||
|
|
@ -136,6 +136,10 @@ module Types
|
|||
null: true,
|
||||
resolver: Resolvers::BoardListResolver
|
||||
|
||||
field :todo,
|
||||
null: true,
|
||||
resolver: Resolvers::TodoResolver
|
||||
|
||||
field :topics, Types::Projects::TopicType.connection_type,
|
||||
null: true,
|
||||
resolver: Resolvers::TopicsResolver,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ module Types
|
|||
null: true,
|
||||
description: 'Personal namespace of the user.'
|
||||
|
||||
field :todos, resolver: Resolvers::TodoResolver, description: 'To-do items of the user.'
|
||||
field :todos, resolver: Resolvers::TodosResolver, description: 'To-do items of the user.'
|
||||
|
||||
# Merge request field: MRs can be authored, assigned, or assigned-for-review:
|
||||
field :authored_merge_requests,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@ module WorkItems
|
|||
module Widgets
|
||||
class Description < Base
|
||||
delegate :description, to: :work_item
|
||||
|
||||
def update(params:)
|
||||
work_item.description = params[:description] if params&.key?(:description)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ module WorkItems
|
|||
super(project: project, current_user: current_user, params: params, spam_params: nil)
|
||||
|
||||
@widget_params = widget_params
|
||||
@widget_services = {}
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -24,8 +25,20 @@ module WorkItems
|
|||
|
||||
def execute_widgets(work_item:, callback:)
|
||||
work_item.widgets.each do |widget|
|
||||
widget.try(callback, params: @widget_params[widget.class.api_symbol])
|
||||
widget_service(widget).try(callback, params: @widget_params[widget.class.api_symbol])
|
||||
end
|
||||
end
|
||||
|
||||
def widget_service(widget)
|
||||
service_class = begin
|
||||
"WorkItems::Widgets::#{widget.type.capitalize}Service::UpdateService".constantize
|
||||
rescue NameError
|
||||
nil
|
||||
end
|
||||
|
||||
return unless service_class
|
||||
|
||||
@widget_services[widget] ||= service_class.new(widget: widget, current_user: current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItems
|
||||
module Widgets
|
||||
class BaseService < ::BaseService
|
||||
attr_reader :widget, :current_user
|
||||
|
||||
def initialize(widget:, current_user:)
|
||||
@widget = widget
|
||||
@current_user = current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItems
|
||||
module Widgets
|
||||
module DescriptionService
|
||||
class UpdateService < WorkItems::Widgets::BaseService
|
||||
def update(params: {})
|
||||
return unless params.present? && params[:description]
|
||||
|
||||
widget.work_item.description = params[:description]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
= render 'shared/project_creation_levels', f: f, method: :default_project_creation, legend: s_('ProjectCreationLevel|Default project creation protection')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Metrics - Prometheus')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
%section.settings.as-grafana.no-animate#js-grafana-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Metrics - Grafana')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'performance_bar_settings_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Profiling - Performance bar')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'usage_statistics_settings_content' } }
|
||||
.settings-header#usage-statistics
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Usage statistics')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
- if Feature.enabled?(:configure_sentry_in_application_settings)
|
||||
%section.settings.as-sentry.no-animate#js-sentry-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sentry_settings_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Sentry')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
.form-group.row
|
||||
= f.label :name, s_('ProtectedBranch|Branch:'), class: 'col-sm-12'
|
||||
.col-sm-12
|
||||
= render partial: "projects/protected_branches/shared/dropdown", locals: { f: f, toggle_classes: 'gl-w-full! gl-form-lg' }
|
||||
= render partial: "projects/protected_branches/shared/dropdown", locals: { f: f, toggle_classes: 'gl-w-full! gl-form-input-lg' }
|
||||
.form-text.text-muted
|
||||
- wildcards_url = help_page_url('user/project/protected_branches', anchor: 'configure-multiple-protected-branches-by-using-a-wildcard')
|
||||
- wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url }
|
||||
|
|
|
|||
|
|
@ -431,6 +431,18 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="querytimelogsstarttime"></a>`startTime` | [`Time`](#time) | List timelogs within a time range where the logged time is equal to or after startTime. |
|
||||
| <a id="querytimelogsusername"></a>`username` | [`String`](#string) | List timelogs for a user. |
|
||||
|
||||
### `Query.todo`
|
||||
|
||||
Retrieve a single to-do item.
|
||||
|
||||
Returns [`Todo`](#todo).
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="querytodoid"></a>`id` | [`TodoID!`](#todoid) | ID of the to-do item. |
|
||||
|
||||
### `Query.topics`
|
||||
|
||||
Find project topics.
|
||||
|
|
@ -5608,6 +5620,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="mutationworkitemupdatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
|
||||
| <a id="mutationworkitemupdateid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
||||
| <a id="mutationworkitemupdatestateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
|
||||
| <a id="mutationworkitemupdatetitle"></a>`title` | [`String`](#string) | Title of the work item. |
|
||||
|
|
@ -11751,6 +11764,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
|
|||
| <a id="groupdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
|
||||
| <a id="groupdora"></a>`dora` | [`Dora`](#dora) | Group's DORA metrics. |
|
||||
| <a id="groupemailsdisabled"></a>`emailsDisabled` | [`Boolean`](#boolean) | Indicates if a group has email notifications disabled. |
|
||||
| <a id="groupenforcefreeusercap"></a>`enforceFreeUserCap` | [`Boolean`](#boolean) | Indicates whether the group has limited users for a free plan. |
|
||||
| <a id="groupepicboards"></a>`epicBoards` | [`EpicBoardConnection`](#epicboardconnection) | Find epic boards. (see [Connections](#connections)) |
|
||||
| <a id="groupepicsenabled"></a>`epicsEnabled` | [`Boolean`](#boolean) | Indicates if Epics are enabled for namespace. |
|
||||
| <a id="groupexternalauditeventdestinations"></a>`externalAuditEventDestinations` | [`ExternalAuditEventDestinationConnection`](#externalauditeventdestinationconnection) | External locations that receive audit events belonging to the group. (see [Connections](#connections)) |
|
||||
|
|
@ -21925,6 +21939,7 @@ A time-frame defined as a closed inclusive range of two dates.
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="workitemupdatedtaskinputdescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
|
||||
| <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
||||
| <a id="workitemupdatedtaskinputstateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
|
||||
| <a id="workitemupdatedtaskinputtitle"></a>`title` | [`String`](#string) | Title of the work item. |
|
||||
|
|
|
|||
|
|
@ -102,3 +102,8 @@ To avoid this error, make sure you are using the latest stable version of Docker
|
|||
## Lack of IPv6 support
|
||||
|
||||
Due to the underlying [ZAProxy engine not supporting IPv6](https://github.com/zaproxy/zaproxy/issues/3705), DAST is unable to scan or crawl IPv6-based applications.
|
||||
|
||||
## Additional insight into DAST scan activity
|
||||
|
||||
For additional insight into what a DAST scan is doing at a given time, you may find it helpful to review
|
||||
the web server access logs for a DAST target endpoint during or following a scan.
|
||||
|
|
|
|||
|
|
@ -43507,6 +43507,11 @@ msgstr ""
|
|||
msgid "WorkItem|Are you sure you want to delete the work item? This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Assignee"
|
||||
msgid_plural "WorkItem|Assignees"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "WorkItem|Cancel"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ module QA
|
|||
Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request|
|
||||
merge_request.project = project
|
||||
merge_request.title = merge_request_title
|
||||
merge_request.assignee = 'me'
|
||||
merge_request.description = merge_request_description
|
||||
end
|
||||
|
||||
|
|
@ -53,6 +54,7 @@ module QA
|
|||
merge_request.description = merge_request_description
|
||||
merge_request.project = project
|
||||
merge_request.milestone = milestone
|
||||
merge_request.assignee = 'me'
|
||||
merge_request.labels.push(label)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -25,20 +25,6 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when ’Create merge request’ button is clicked' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
click_button('Create merge request')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it_behaves_like 'merge request author auto assign'
|
||||
end
|
||||
|
||||
context 'when interacting with the dropdown' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
|
|
|
|||
|
|
@ -15,39 +15,27 @@ RSpec.describe "User creates a merge request", :js do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when completed the compare branches form' do
|
||||
before do
|
||||
visit(project_new_merge_request_path(project))
|
||||
it "creates a merge request" do
|
||||
visit(project_new_merge_request_path(project))
|
||||
|
||||
find(".js-source-branch").click
|
||||
click_link("fix")
|
||||
find(".js-source-branch").click
|
||||
click_link("fix")
|
||||
|
||||
find(".js-target-branch").click
|
||||
click_link("feature")
|
||||
find(".js-target-branch").click
|
||||
click_link("feature")
|
||||
|
||||
click_button("Compare branches")
|
||||
click_button("Compare branches")
|
||||
|
||||
page.within('.merge-request-form') do
|
||||
expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
|
||||
end
|
||||
|
||||
it "shows merge request form" do
|
||||
page.within('.merge-request-form') do
|
||||
expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
|
||||
end
|
||||
fill_in("Title", with: title)
|
||||
click_button("Create merge request")
|
||||
|
||||
page.within(".merge-request") do
|
||||
expect(page).to have_content(title)
|
||||
end
|
||||
|
||||
context "when completed the merge request form" do
|
||||
before do
|
||||
fill_in("Title", with: title)
|
||||
click_button("Create merge request")
|
||||
end
|
||||
|
||||
it "creates a merge request" do
|
||||
page.within(".merge-request") do
|
||||
expect(page).to have_content(title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'merge request author auto assign'
|
||||
end
|
||||
|
||||
context "XSS branch name exists" do
|
||||
|
|
|
|||
|
|
@ -2,40 +2,68 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
|
||||
RSpec.describe 'Jobs (JavaScript fixtures)' do
|
||||
include ApiHelpers
|
||||
include JavaScriptFixturesHelpers
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
|
||||
let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') }
|
||||
let(:user) { project.first_owner }
|
||||
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
|
||||
let!(:build_with_artifacts) { create(:ci_build, :success, :artifacts, :trace_artifact, pipeline: pipeline, stage: 'test', artifacts_expire_at: Time.now + 18.months) }
|
||||
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline, stage: 'build') }
|
||||
let!(:pending_build) { create(:ci_build, :pending, pipeline: pipeline, stage: 'deploy') }
|
||||
let!(:delayed_job) do
|
||||
create(:ci_build, :scheduled,
|
||||
pipeline: pipeline,
|
||||
name: 'delayed job',
|
||||
stage: 'test')
|
||||
end
|
||||
|
||||
render_views
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
after do
|
||||
remove_repository(project)
|
||||
end
|
||||
|
||||
it 'jobs/delayed.json' do
|
||||
get :show, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: delayed_job.to_param
|
||||
}, format: :json
|
||||
describe Projects::JobsController, type: :controller do
|
||||
let!(:delayed) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'delayed job') }
|
||||
|
||||
expect(response).to be_successful
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'jobs/delayed.json' do
|
||||
get :show, params: {
|
||||
namespace_id: project.namespace.to_param,
|
||||
project_id: project,
|
||||
id: delayed.to_param
|
||||
}, format: :json
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
|
||||
let!(:created_by_tag) { create(:ci_build, :success, name: 'created_by_tag', tag: true, pipeline: pipeline) }
|
||||
let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) }
|
||||
let!(:stuck) { create(:ci_build, :pending, name: 'stuck', pipeline: pipeline) }
|
||||
|
||||
fixtures_path = 'graphql/jobs/'
|
||||
get_jobs_query = 'get_jobs.query.graphql'
|
||||
|
||||
let_it_be(:query) do
|
||||
get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_query}")
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_jobs_query}.json" do
|
||||
post_graphql(query, current_user: user, variables: {
|
||||
fullPath: 'frontend-fixtures/builds-project'
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
|
||||
it "#{fixtures_path}#{get_jobs_query}.as_guest.json" do
|
||||
guest = create(:user)
|
||||
project.add_guest(guest)
|
||||
|
||||
post_graphql(query, current_user: guest, variables: {
|
||||
fullPath: 'frontend-fixtures/builds-project'
|
||||
})
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import JobCell from '~/jobs/components/table/cells/job_cell.vue';
|
||||
import { mockJobsInTable } from '../../../mock_data';
|
||||
import { mockJobsInTable, mockJobsAsGuestInTable } from '../../../mock_data';
|
||||
|
||||
const mockJob = mockJobsInTable[0];
|
||||
const mockJobCreatedByTag = mockJobsInTable[1];
|
||||
const mockJobLimitedAccess = mockJobsInTable[2];
|
||||
const mockStuckJob = mockJobsInTable[3];
|
||||
const getMockJob = (name) => mockJobsInTable.find((job) => job.name === name);
|
||||
|
||||
describe('Job Cell', () => {
|
||||
let wrapper;
|
||||
|
|
@ -23,6 +20,8 @@ describe('Job Cell', () => {
|
|||
|
||||
const findBadgeById = (id) => wrapper.findByTestId(id);
|
||||
|
||||
const mockJob = getMockJob('build');
|
||||
|
||||
const createComponent = (jobData = mockJob) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(JobCell, {
|
||||
|
|
@ -49,9 +48,11 @@ describe('Job Cell', () => {
|
|||
});
|
||||
|
||||
it('display the job id with no link', () => {
|
||||
createComponent(mockJobLimitedAccess);
|
||||
const mockJobAsGuest = mockJobsAsGuestInTable[0];
|
||||
|
||||
const expectedJobId = `#${getIdFromGraphQLId(mockJobLimitedAccess.id)}`;
|
||||
createComponent(mockJobAsGuest);
|
||||
|
||||
const expectedJobId = `#${getIdFromGraphQLId(mockJobAsGuest.id)}`;
|
||||
|
||||
expect(findJobIdNoLink().text()).toBe(expectedJobId);
|
||||
expect(findJobIdNoLink().exists()).toBe(true);
|
||||
|
|
@ -75,7 +76,7 @@ describe('Job Cell', () => {
|
|||
});
|
||||
|
||||
it('displays label icon when job is created by a tag', () => {
|
||||
createComponent(mockJobCreatedByTag);
|
||||
createComponent(getMockJob('created_by_tag'));
|
||||
|
||||
expect(findLabelIcon().exists()).toBe(true);
|
||||
expect(findForkIcon().exists()).toBe(false);
|
||||
|
|
@ -131,7 +132,7 @@ describe('Job Cell', () => {
|
|||
});
|
||||
|
||||
it('stuck icon is shown if job is stuck', () => {
|
||||
createComponent(mockStuckJob);
|
||||
createComponent(getMockJob('stuck'));
|
||||
|
||||
expect(findStuckIcon().exists()).toBe(true);
|
||||
expect(findStuckIcon().attributes('name')).toBe('warning');
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
|
||||
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
|
||||
const threeWeeksAgo = new Date();
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
|
||||
|
||||
// Fixtures generated at spec/frontend/fixtures/jobs.rb
|
||||
export const mockJobsInTable = mockJobs.data.project.jobs.nodes;
|
||||
export const mockJobsAsGuestInTable = mockJobsAsGuest.data.project.jobs.nodes;
|
||||
|
||||
export const stages = [
|
||||
{
|
||||
name: 'build',
|
||||
|
|
@ -1283,199 +1289,6 @@ export const mockPipelineDetached = {
|
|||
},
|
||||
};
|
||||
|
||||
export const mockJobsInTable = [
|
||||
{
|
||||
detailedStatus: {
|
||||
icon: 'status_manual',
|
||||
label: 'manual play action',
|
||||
text: 'manual',
|
||||
tooltip: 'manual action',
|
||||
action: {
|
||||
buttonTitle: 'Trigger this manual action',
|
||||
icon: 'play',
|
||||
method: 'post',
|
||||
path: '/root/ci-project/-/jobs/2004/play',
|
||||
title: 'Play',
|
||||
__typename: 'StatusAction',
|
||||
},
|
||||
detailsPath: '/root/ci-project/-/jobs/2004',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
id: 'gid://gitlab/Ci::Build/2004',
|
||||
refName: 'main',
|
||||
refPath: '/root/ci-project/-/commits/main',
|
||||
tags: [],
|
||||
shortSha: '2d5d8323',
|
||||
commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe',
|
||||
pipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/423',
|
||||
path: '/root/ci-project/-/pipelines/423',
|
||||
user: {
|
||||
webPath: '/root',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
__typename: 'User',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
stage: { name: 'test', __typename: 'CiStage' },
|
||||
name: 'test_manual_job',
|
||||
duration: null,
|
||||
finishedAt: null,
|
||||
coverage: null,
|
||||
createdByTag: false,
|
||||
retryable: false,
|
||||
playable: true,
|
||||
cancelable: false,
|
||||
active: false,
|
||||
stuck: false,
|
||||
userPermissions: { readBuild: true, __typename: 'JobPermissions' },
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
detailedStatus: {
|
||||
icon: 'status_skipped',
|
||||
label: 'skipped',
|
||||
text: 'skipped',
|
||||
tooltip: 'skipped',
|
||||
action: null,
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
id: 'gid://gitlab/Ci::Build/2021',
|
||||
refName: 'main',
|
||||
refPath: '/root/ci-project/-/commits/main',
|
||||
tags: [],
|
||||
shortSha: '2d5d8323',
|
||||
commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe',
|
||||
pipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/425',
|
||||
path: '/root/ci-project/-/pipelines/425',
|
||||
user: {
|
||||
webPath: '/root',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
__typename: 'User',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
stage: { name: 'test', __typename: 'CiStage' },
|
||||
name: 'coverage_job',
|
||||
duration: null,
|
||||
finishedAt: null,
|
||||
coverage: null,
|
||||
createdByTag: true,
|
||||
retryable: false,
|
||||
playable: false,
|
||||
cancelable: false,
|
||||
active: false,
|
||||
stuck: false,
|
||||
userPermissions: { readBuild: true, __typename: 'JobPermissions' },
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
detailedStatus: {
|
||||
icon: 'status_success',
|
||||
label: 'passed',
|
||||
text: 'passed',
|
||||
tooltip: 'passed',
|
||||
action: {
|
||||
buttonTitle: 'Retry this job',
|
||||
icon: 'retry',
|
||||
method: 'post',
|
||||
path: '/root/ci-project/-/jobs/2015/retry',
|
||||
title: 'Retry',
|
||||
__typename: 'StatusAction',
|
||||
},
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
id: 'gid://gitlab/Ci::Build/2015',
|
||||
refName: 'main',
|
||||
refPath: '/root/ci-project/-/commits/main',
|
||||
tags: [],
|
||||
shortSha: '2d5d8323',
|
||||
commitPath: '/root/ci-project/-/commit/2d5d83230bdea0e003d83ef4c16d2bf9a8808ebe',
|
||||
pipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/424',
|
||||
path: '/root/ci-project/-/pipelines/424',
|
||||
user: {
|
||||
webPath: '/root',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
__typename: 'User',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
stage: { name: 'deploy', __typename: 'CiStage' },
|
||||
name: 'artifact_job',
|
||||
duration: 2,
|
||||
finishedAt: '2021-04-01T17:36:18Z',
|
||||
coverage: 82.71,
|
||||
createdByTag: false,
|
||||
retryable: true,
|
||||
playable: false,
|
||||
cancelable: false,
|
||||
active: false,
|
||||
stuck: false,
|
||||
userPermissions: { readBuild: false, __typename: 'JobPermissions' },
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
artifacts: { nodes: [], __typename: 'CiJobArtifactConnection' },
|
||||
allowFailure: false,
|
||||
status: 'PENDING',
|
||||
scheduledAt: null,
|
||||
manualJob: false,
|
||||
triggered: null,
|
||||
createdByTag: false,
|
||||
detailedStatus: {
|
||||
detailsPath: '/root/ci-project/-/jobs/2391',
|
||||
group: 'pending',
|
||||
icon: 'status_pending',
|
||||
label: 'pending',
|
||||
text: 'pending',
|
||||
tooltip: 'pending',
|
||||
action: {
|
||||
buttonTitle: 'Cancel this job',
|
||||
icon: 'cancel',
|
||||
method: 'post',
|
||||
path: '/root/ci-project/-/jobs/2391/cancel',
|
||||
title: 'Cancel',
|
||||
__typename: 'StatusAction',
|
||||
},
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
id: 'gid://gitlab/Ci::Build/2391',
|
||||
refName: 'master',
|
||||
refPath: '/root/ci-project/-/commits/master',
|
||||
tags: [],
|
||||
shortSha: '916330b4',
|
||||
commitPath: '/root/ci-project/-/commit/916330b4fda5dae226524ceb51c756c0ed26679d',
|
||||
pipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/482',
|
||||
path: '/root/ci-project/-/pipelines/482',
|
||||
user: {
|
||||
webPath: '/root',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
stage: { name: 'build', __typename: 'CiStage' },
|
||||
name: 'build_job',
|
||||
duration: null,
|
||||
finishedAt: null,
|
||||
coverage: null,
|
||||
retryable: false,
|
||||
playable: false,
|
||||
cancelable: true,
|
||||
active: true,
|
||||
stuck: true,
|
||||
userPermissions: { readBuild: true, __typename: 'JobPermissions' },
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockJobsQueryResponse = {
|
||||
data: {
|
||||
project: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ exports[`self monitor component When the self monitor project has not been creat
|
|||
class="settings-header"
|
||||
>
|
||||
<h4
|
||||
class="js-section-header"
|
||||
class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only"
|
||||
>
|
||||
|
||||
Self monitoring
|
||||
|
|
|
|||
|
|
@ -1,52 +1,59 @@
|
|||
import { GlLink, GlTokenSelector } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { GlLink, GlTokenSelector, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
import localUpdateWorkItemMutation from '~/work_items/graphql/local_update_work_item.mutation.graphql';
|
||||
import { i18n } from '~/work_items/constants';
|
||||
import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
|
||||
import { projectMembersResponse, mockAssignees, workItemQueryResponse } from '../mock_data';
|
||||
|
||||
const mockAssignees = [
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
},
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/2',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'Marcus Rutherford',
|
||||
username: 'ruthfull',
|
||||
},
|
||||
];
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const workItemId = 'gid://gitlab/WorkItem/1';
|
||||
|
||||
const mutate = jest.fn();
|
||||
|
||||
describe('WorkItemAssignees component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findAssigneeLinks = () => wrapper.findAllComponents(GlLink);
|
||||
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
|
||||
const findEmptyState = () => wrapper.findByTestId('empty-state');
|
||||
|
||||
const createComponent = ({ assignees = mockAssignees } = {}) => {
|
||||
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMembersResponse);
|
||||
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
|
||||
const createComponent = ({
|
||||
assignees = mockAssignees,
|
||||
searchQueryHandler = successSearchQueryHandler,
|
||||
} = {}) => {
|
||||
const apolloProvider = createMockApollo([[userSearchQuery, searchQueryHandler]], resolvers, {
|
||||
typePolicies: temporaryConfig.cacheConfig.typePolicies,
|
||||
});
|
||||
|
||||
apolloProvider.clients.defaultClient.writeQuery({
|
||||
query: workItemQuery,
|
||||
variables: {
|
||||
id: workItemId,
|
||||
},
|
||||
data: workItemQueryResponse.data,
|
||||
});
|
||||
|
||||
wrapper = mountExtended(WorkItemAssignees, {
|
||||
provide: {
|
||||
fullPath: 'test-project-path',
|
||||
},
|
||||
propsData: {
|
||||
assignees,
|
||||
workItemId,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
mutate,
|
||||
},
|
||||
},
|
||||
attachTo: document.body,
|
||||
apolloProvider,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -54,40 +61,114 @@ describe('WorkItemAssignees component', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('should pass the correct data-user-id attribute', () => {
|
||||
it('passes the correct data-user-id attribute', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findAssigneeLinks().at(0).attributes('data-user-id')).toBe('1');
|
||||
});
|
||||
|
||||
describe('when there are assignees', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
it('focuses token selector on token selector input event', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
|
||||
await nextTick();
|
||||
|
||||
it('should focus token selector on token removal', async () => {
|
||||
findTokenSelector().vm.$emit('token-remove', mockAssignees[0].id);
|
||||
await nextTick();
|
||||
expect(findEmptyState().exists()).toBe(false);
|
||||
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
|
||||
});
|
||||
|
||||
expect(findEmptyState().exists()).toBe(false);
|
||||
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
|
||||
});
|
||||
it('calls a mutation on clicking outside the token selector', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
|
||||
findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
|
||||
await waitForPromises();
|
||||
|
||||
it('should call a mutation on clicking outside the token selector', async () => {
|
||||
findTokenSelector().vm.$emit('input', [mockAssignees[0]]);
|
||||
findTokenSelector().vm.$emit('token-remove');
|
||||
await nextTick();
|
||||
expect(mutate).not.toHaveBeenCalled();
|
||||
expect(findTokenSelector().props('selectedTokens')).toEqual([mockAssignees[0]]);
|
||||
});
|
||||
|
||||
findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
|
||||
await nextTick();
|
||||
it('does not start user search by default', () => {
|
||||
createComponent();
|
||||
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
mutation: localUpdateWorkItemMutation,
|
||||
variables: {
|
||||
input: { id: workItemId, assigneeIds: [mockAssignees[0].id] },
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
expect(findTokenSelector().props('dropdownItems')).toEqual([]);
|
||||
});
|
||||
|
||||
it('starts user search on hovering for more than 250ms', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('starts user search on focusing token selector', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not start searching if token-selector was hovered for less than 250ms', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not start searching if cursor was moved out from token selector before 250ms passed', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().trigger('mouseover');
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
findTokenSelector().trigger('mouseout');
|
||||
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
|
||||
await nextTick();
|
||||
|
||||
expect(findTokenSelector().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows skeleton loader on dropdown when loading users', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows correct user list in dropdown when loaded', async () => {
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await nextTick();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(false);
|
||||
expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('emits error event if search users query fails', async () => {
|
||||
createComponent({ searchQueryHandler: errorHandler });
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
|
||||
});
|
||||
|
||||
it('should search for users with correct key after text input', async () => {
|
||||
const searchKey = 'Hello';
|
||||
|
||||
createComponent();
|
||||
findTokenSelector().vm.$emit('focus');
|
||||
findTokenSelector().vm.$emit('text-input', searchKey);
|
||||
await waitForPromises();
|
||||
|
||||
expect(successSearchQueryHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ search: searchKey }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -300,3 +300,60 @@ export const availableWorkItemsResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const projectMembersResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: '1',
|
||||
__typename: 'Project',
|
||||
users: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'user-1',
|
||||
user: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: '/root',
|
||||
status: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'user-2',
|
||||
user: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/5',
|
||||
avatarUrl: '/avatar2',
|
||||
name: 'rookie',
|
||||
username: 'rookie',
|
||||
webUrl: 'rookie',
|
||||
status: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockAssignees = [
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'John Doe',
|
||||
username: 'doe_I',
|
||||
},
|
||||
{
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/2',
|
||||
avatarUrl: '',
|
||||
webUrl: '',
|
||||
name: 'Marcus Rutherford',
|
||||
username: 'ruthfull',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::TodoResolver do
|
||||
RSpec.describe Resolvers::TodosResolver do
|
||||
include GraphqlHelpers
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Types::WorkItems::Widgets::DescriptionInputType do
|
||||
it { expect(described_class.graphql_name).to eq('WorkItemWidgetDescriptionInput') }
|
||||
|
||||
it { expect(described_class.arguments.keys).to match_array(%w[description]) }
|
||||
end
|
||||
|
|
@ -11,8 +11,17 @@ RSpec.describe 'Update a work item' do
|
|||
|
||||
let(:work_item_event) { 'CLOSE' }
|
||||
let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } }
|
||||
let(:fields) do
|
||||
<<~FIELDS
|
||||
workItem {
|
||||
state
|
||||
title
|
||||
}
|
||||
errors
|
||||
FIELDS
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s)) }
|
||||
let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s), fields) }
|
||||
|
||||
let(:mutation_response) { graphql_mutation_response(:work_item_update) }
|
||||
|
||||
|
|
@ -80,5 +89,29 @@ RSpec.describe 'Update a work item' do
|
|||
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with description widget input' do
|
||||
let(:fields) do
|
||||
<<~FIELDS
|
||||
workItem {
|
||||
description
|
||||
widgets {
|
||||
type
|
||||
... on WorkItemWidgetDescription {
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
errors
|
||||
FIELDS
|
||||
end
|
||||
|
||||
it_behaves_like 'update work item description widget' do
|
||||
let(:new_description) { 'updated description' }
|
||||
let(:input) do
|
||||
{ 'descriptionWidget' => { 'description' => new_description } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,15 +9,22 @@ RSpec.describe 'Update work item widgets' do
|
|||
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
|
||||
let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
|
||||
|
||||
let(:input) do
|
||||
{
|
||||
'descriptionWidget' => { 'description' => 'updated description' }
|
||||
}
|
||||
end
|
||||
|
||||
let(:mutation) { graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s)) }
|
||||
|
||||
let(:input) { { 'descriptionWidget' => { 'description' => 'updated description' } } }
|
||||
let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) }
|
||||
let(:mutation) do
|
||||
graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s), <<~FIELDS)
|
||||
errors
|
||||
workItem {
|
||||
description
|
||||
widgets {
|
||||
type
|
||||
... on WorkItemWidgetDescription {
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
FIELDS
|
||||
end
|
||||
|
||||
context 'the user is not allowed to update a work item' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
|
@ -28,32 +35,8 @@ RSpec.describe 'Update work item widgets' do
|
|||
context 'when user has permissions to update a work item', :aggregate_failures do
|
||||
let(:current_user) { developer }
|
||||
|
||||
context 'when the updated work item is not valid' do
|
||||
it 'returns validation errors without the work item' do
|
||||
errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') }
|
||||
|
||||
allow_next_found_instance_of(::WorkItem) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(false)
|
||||
allow(instance).to receive(:errors).and_return(errors)
|
||||
end
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['workItem']).to be_nil
|
||||
expect(mutation_response['errors']).to match_array(['Description error message'])
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates the work item widgets' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
work_item.reload
|
||||
end.to change(work_item, :description).from(nil).to('updated description')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['workItem']).to include(
|
||||
'title' => work_item.title
|
||||
)
|
||||
it_behaves_like 'update work item description widget' do
|
||||
let(:new_description) { 'updated description' }
|
||||
end
|
||||
|
||||
it_behaves_like 'has spam protection' do
|
||||
|
|
@ -69,7 +52,7 @@ RSpec.describe 'Update work item widgets' do
|
|||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
work_item.reload
|
||||
end.to not_change(work_item, :title)
|
||||
end.to not_change(work_item, :description)
|
||||
|
||||
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Todo Query' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { nil }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
|
||||
let_it_be(:todo_owner) { create(:user) }
|
||||
|
||||
let_it_be(:todo) { create(:todo, user: todo_owner, target: project) }
|
||||
|
||||
before do
|
||||
project.add_developer(todo_owner)
|
||||
end
|
||||
|
||||
let(:fields) do
|
||||
<<~GRAPHQL
|
||||
id
|
||||
GRAPHQL
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(:todo, { id: todo.to_global_id.to_s }, fields)
|
||||
end
|
||||
|
||||
subject do
|
||||
result = GitlabSchema.execute(query, context: { current_user: current_user }).to_h
|
||||
graphql_dig_at(result, :data, :todo)
|
||||
end
|
||||
|
||||
context 'when requesting user is todo owner' do
|
||||
let(:current_user) { todo_owner }
|
||||
|
||||
it { is_expected.to include('id' => todo.to_global_id.to_s) }
|
||||
end
|
||||
|
||||
context 'when requesting user is not todo owner' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be_with_reload(:work_item) { create(:work_item, project: project, description: 'old description') }
|
||||
|
||||
let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Description) } }
|
||||
|
||||
describe '#update' do
|
||||
subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang
|
||||
|
||||
context 'when description param is present' do
|
||||
let(:params) { { description: 'updated description' } }
|
||||
|
||||
it 'correctly sets work item description value' do
|
||||
subject
|
||||
|
||||
expect(work_item.description).to eq('updated description')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when description param is not present' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'does not change work item description value' do
|
||||
subject
|
||||
|
||||
expect(work_item.description).to eq('old description')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button
|
|||
it "#{action} a MR with multiple assignees", :js do
|
||||
find('.js-assignee-search').click
|
||||
page.within '.dropdown-menu-user' do
|
||||
click_link user.name unless action == 'creates'
|
||||
click_link user.name
|
||||
click_link user2.name
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees widget merge request' do |action, save
|
|||
it "#{action} a MR with multiple assignees", :js do
|
||||
find('.js-assignee-search').click
|
||||
page.within '.dropdown-menu-user' do
|
||||
click_link user.name unless action == 'creates'
|
||||
click_link user.name
|
||||
click_link user2.name
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'update work item description widget' do
|
||||
it 'updates the description widget' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
work_item.reload
|
||||
end.to change(work_item, :description).from(nil).to(new_description)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response['workItem']['widgets']).to include(
|
||||
{
|
||||
'description' => new_description,
|
||||
'type' => 'DESCRIPTION'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the updated work item is not valid' do
|
||||
it 'returns validation errors without the work item' do
|
||||
errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') }
|
||||
|
||||
allow_next_found_instance_of(::WorkItem) do |instance|
|
||||
allow(instance).to receive(:valid?).and_return(false)
|
||||
allow(instance).to receive(:errors).and_return(errors)
|
||||
end
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['workItem']).to be_nil
|
||||
expect(mutation_response['errors']).to match_array(['Description error message'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'merge request author auto assign' do
|
||||
it 'populates merge request author as assignee' do
|
||||
expect(find('.js-assignee-search')).to have_content(user.name)
|
||||
expect(page).not_to have_content 'Assign yourself'
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue