summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-06-28 18:08:20 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-06-28 18:08:20 +0000
commitd12d801795043280c3d726fae0abfec63266d156 (patch)
tree629b2dcbe1b83c5c558ece6d18986cd9f3934dcc
parent5bb54b8711a6fd0993ab014f6749cbb74c7b071b (diff)
downloadgitlab-ce-d12d801795043280c3d726fae0abfec63266d156.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/line_length.yml2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock28
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js3
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue4
-rw-r--r--app/assets/javascripts/users_select/index.js18
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue115
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js4
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql2
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/pages/work_items.scss4
-rw-r--r--app/assets/stylesheets/utilities.scss5
-rw-r--r--app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb3
-rw-r--r--app/graphql/mutations/work_items/update.rb12
-rw-r--r--app/graphql/mutations/work_items/update_widgets.rb1
-rw-r--r--app/graphql/resolvers/todo_resolver.rb66
-rw-r--r--app/graphql/resolvers/todos_resolver.rb69
-rw-r--r--app/graphql/types/alert_management/alert_type.rb2
-rw-r--r--app/graphql/types/query_type.rb4
-rw-r--r--app/graphql/types/user_interface.rb2
-rw-r--r--app/models/work_items/widgets/description.rb4
-rw-r--r--app/services/work_items/update_service.rb15
-rw-r--r--app/services/work_items/widgets/base_service.rb14
-rw-r--r--app/services/work_items/widgets/description_service/update_service.rb15
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml2
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml10
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml2
-rw-r--r--doc/api/graphql/reference/index.md15
-rw-r--r--doc/user/application_security/dast/dast_troubleshooting.md5
-rw-r--r--locale/gitlab.pot5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb14
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb38
-rw-r--r--spec/frontend/fixtures/jobs.rb72
-rw-r--r--spec/frontend/jobs/components/table/cells/job_cell_spec.js19
-rw-r--r--spec/frontend/jobs/mock_data.js199
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap2
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js195
-rw-r--r--spec/frontend/work_items/mock_data.js57
-rw-r--r--spec/graphql/resolvers/todos_resolver_spec.rb (renamed from spec/graphql/resolvers/todo_resolver_spec.rb)2
-rw-r--r--spec/graphql/types/work_items/widgets/description_input_type_spec.rb9
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb35
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb51
-rw-r--r--spec/requests/api/graphql/todo_query_spec.rb50
-rw-r--r--spec/services/work_items/widgets/description_service/update_service_spec.rb35
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb8
49 files changed, 763 insertions, 499 deletions
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 6aed65b586e..9d3f426a228 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -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'
diff --git a/Gemfile b/Gemfile
index ddd482b76cb..f88cc337a13 100644
--- a/Gemfile
+++ b/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
diff --git a/Gemfile.lock b/Gemfile.lock
index c9a06738760..bd08985692f 100644
--- a/Gemfile.lock
+++ b/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)
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index 2a00f9d42a9..cc2608b5c62 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -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();
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index 2f31d8ef3fb..b14e816a674 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -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>
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 0904aae0347..94b4ee77e7e 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -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);
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4d1c171772e..46920969415 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -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>
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
index 09d929faae2..9266b4cdccb 100644
--- a/app/assets/javascripts/work_items/graphql/provider.js
+++ b/app/assets/javascripts/work_items/graphql/provider.js
@@ -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({
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index bfe2f0fe0ce..de4bdad5659 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -23,7 +23,7 @@ extend type WorkItem {
type LocalWorkItemAssigneesInput {
id: WorkItemID!
- assigneeIds: [ID!]
+ assignees: [UserCore!]
}
type LocalWorkItemPayload {
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index be72ec33465..cf4a415446e 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -32,3 +32,4 @@
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';
+@import './pages/work_items';
diff --git a/app/assets/stylesheets/pages/work_items.scss b/app/assets/stylesheets/pages/work_items.scss
new file mode 100644
index 00000000000..b98f55df1ed
--- /dev/null
+++ b/app/assets/stylesheets/pages/work_items.scss
@@ -0,0 +1,4 @@
+.gl-token-selector-token-container {
+ display: flex;
+ align-items: center;
+}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 169d38ab5aa..d7a5e21e303 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -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.
diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
index 6a91a097a17..7af55d42f2a 100644
--- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
+++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
@@ -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
diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb
index c495da00f41..ff4aba4830f 100644
--- a/app/graphql/mutations/work_items/update.rb
+++ b/app/graphql/mutations/work_items/update.rb
@@ -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
diff --git a/app/graphql/mutations/work_items/update_widgets.rb b/app/graphql/mutations/work_items/update_widgets.rb
index d19da0abaac..7037b7e5a2a 100644
--- a/app/graphql/mutations/work_items/update_widgets.rb
+++ b/app/graphql/mutations/work_items/update_widgets.rb
@@ -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." \
diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb
index f0be1b6e9a5..0653cd27b4d 100644
--- a/app/graphql/resolvers/todo_resolver.rb
+++ b/app/graphql/resolvers/todo_resolver.rb
@@ -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
diff --git a/app/graphql/resolvers/todos_resolver.rb b/app/graphql/resolvers/todos_resolver.rb
new file mode 100644
index 00000000000..3e8dddb4859
--- /dev/null
+++ b/app/graphql/resolvers/todos_resolver.rb
@@ -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
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index 43b7bbb419f..a0d19229d3d 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -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,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 46d121f6552..c54dab618d2 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -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,
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index 1c8a1352c72..edbc8aee9c5 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -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,
diff --git a/app/models/work_items/widgets/description.rb b/app/models/work_items/widgets/description.rb
index 35b6d295321..1e84d172bef 100644
--- a/app/models/work_items/widgets/description.rb
+++ b/app/models/work_items/widgets/description.rb
@@ -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
diff --git a/app/services/work_items/update_service.rb b/app/services/work_items/update_service.rb
index 0b420881b4b..7b50040a716 100644
--- a/app/services/work_items/update_service.rb
+++ b/app/services/work_items/update_service.rb
@@ -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
diff --git a/app/services/work_items/widgets/base_service.rb b/app/services/work_items/widgets/base_service.rb
new file mode 100644
index 00000000000..72debc272bd
--- /dev/null
+++ b/app/services/work_items/widgets/base_service.rb
@@ -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
diff --git a/app/services/work_items/widgets/description_service/update_service.rb b/app/services/work_items/widgets/description_service/update_service.rb
new file mode 100644
index 00000000000..e63b6b2ee6c
--- /dev/null
+++ b/app/services/work_items/widgets/description_service/update_service.rb
@@ -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
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 96dcd7e1111..3d4c5561dba 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -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')
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 7cc0ff2c28e..d4476bf838a 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -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')
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
index 13603c8f946..a3addfb9b1a 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
@@ -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 }
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index af91c842838..5ded21eeb60 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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. |
diff --git a/doc/user/application_security/dast/dast_troubleshooting.md b/doc/user/application_security/dast/dast_troubleshooting.md
index 50570b89920..0c7a9806c72 100644
--- a/doc/user/application_security/dast/dast_troubleshooting.md
+++ b/doc/user/application_security/dast/dast_troubleshooting.md
@@ -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.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b97c4e6ec28..ecea30cb802 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
index f4ed9f28dac..25dec82b74c 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb
@@ -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
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 1c707466b51..ae1bce7ea4c 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -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)
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index 2bf8e9ba6a4..c8b22bb3125 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -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")
- end
+ click_button("Compare branches")
- 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
+ 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
- context "when completed the merge request form" do
- before do
- fill_in("Title", with: title)
- click_button("Create merge request")
- end
+ fill_in("Title", with: title)
+ click_button("Create merge request")
- it "creates a merge request" do
- page.within(".merge-request") do
- expect(page).to have_content(title)
- end
- end
+ page.within(".merge-request") do
+ expect(page).to have_content(title)
end
-
- it_behaves_like 'merge request author auto assign'
end
context "XSS branch name exists" do
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 3cc87432655..c76b06bd39e 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -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')
+
+ after do
+ remove_repository(project)
end
- render_views
+ describe Projects::JobsController, type: :controller do
+ let!(:delayed) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'delayed job') }
- before do
- sign_in(user)
- end
+ before do
+ sign_in(user)
+ end
- after do
- remove_repository(project)
+ 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
- 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 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(response).to be_successful
+ expect_graphql_errors_to_be_empty
+ end
end
end
diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
index fc4e5586349..e3bef17b6fa 100644
--- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
@@ -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];
+
+ createComponent(mockJobAsGuest);
- const expectedJobId = `#${getIdFromGraphQLId(mockJobLimitedAccess.id)}`;
+ 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');
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 4676635cce0..57ec1c7ef3f 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -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: {
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
index 62a9ff98243..11841106ed0 100644
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
+++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
@@ -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
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 0552fe5050e..b2678293c05 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -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';
-
-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',
- },
-];
+import { i18n } from '~/work_items/constants';
+import { temporaryConfig, resolvers } from '~/work_items/graphql/provider';
+import { projectMembersResponse, mockAssignees, workItemQueryResponse } from '../mock_data';
-const workItemId = 'gid://gitlab/WorkItem/1';
+Vue.use(VueApollo);
-const mutate = jest.fn();
+const workItemId = 'gid://gitlab/WorkItem/1';
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();
-
- findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
- await nextTick();
-
- expect(mutate).toHaveBeenCalledWith({
- mutation: localUpdateWorkItemMutation,
- variables: {
- input: { id: workItemId, assigneeIds: [mockAssignees[0].id] },
- },
- });
- });
+ expect(findTokenSelector().props('selectedTokens')).toEqual([mockAssignees[0]]);
+ });
+
+ it('does not start user search by default', () => {
+ createComponent();
+
+ 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 }),
+ );
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 91dfc61198c..116bf48901d 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -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',
+ },
+];
diff --git a/spec/graphql/resolvers/todo_resolver_spec.rb b/spec/graphql/resolvers/todos_resolver_spec.rb
index 0760935a2fe..40ca2de0385 100644
--- a/spec/graphql/resolvers/todo_resolver_spec.rb
+++ b/spec/graphql/resolvers/todos_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::TodoResolver do
+RSpec.describe Resolvers::TodosResolver do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/graphql/types/work_items/widgets/description_input_type_spec.rb b/spec/graphql/types/work_items/widgets/description_input_type_spec.rb
new file mode 100644
index 00000000000..81c64bc38ab
--- /dev/null
+++ b/spec/graphql/types/work_items/widgets/description_input_type_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 71b03103115..7a160819a41 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
index 595d8fe97ed..2a5cb937a2f 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb
@@ -9,16 +9,23 @@ 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' }
+ 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
- let(:mutation) { graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s)) }
-
- let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) }
-
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
diff --git a/spec/requests/api/graphql/todo_query_spec.rb b/spec/requests/api/graphql/todo_query_spec.rb
new file mode 100644
index 00000000000..3f743f4402a
--- /dev/null
+++ b/spec/requests/api/graphql/todo_query_spec.rb
@@ -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
diff --git a/spec/services/work_items/widgets/description_service/update_service_spec.rb b/spec/services/work_items/widgets/description_service/update_service_spec.rb
new file mode 100644
index 00000000000..a2eceb97f09
--- /dev/null
+++ b/spec/services/work_items/widgets/description_service/update_service_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
index 4565108b5e4..9d023d9514a 100644
--- a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
@@ -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
diff --git a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb
index a44a699c878..bbde448a1a1 100644
--- a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb
@@ -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
diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb
new file mode 100644
index 00000000000..56c2ca22e15
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb
@@ -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
diff --git a/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb b/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb
deleted file mode 100644
index d4986975f03..00000000000
--- a/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb
+++ /dev/null
@@ -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