Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-19 12:30:18 +00:00
parent a33559afd2
commit 06319330df
91 changed files with 1212 additions and 132 deletions

View File

@ -28,8 +28,8 @@ pages:
- cp .public/assets/application-*.css public/application.css || true
- mv $KNAPSACK_RSPEC_SUITE_REPORT_PATH public/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || true
- mv $FLAKY_RSPEC_SUITE_REPORT_PATH public/$FLAKY_RSPEC_SUITE_REPORT_PATH || true
- mv $RSPEC_PACKED_TESTS_MAPPING_PATH.gz public/ || true
- mv $RSPEC_PACKED_TESTS_MAPPING_ALT_PATH.gz public/ || true
- mv $RSPEC_PACKED_TESTS_MAPPING_PATH.gz public/$RSPEC_PACKED_TESTS_MAPPING_PATH || true
- mv $RSPEC_PACKED_TESTS_MAPPING_ALT_PATH.gz public/$RSPEC_PACKED_TESTS_MAPPING_ALT_PATH || true
- mv $FRONTEND_FIXTURES_MAPPING_PATH public/$FRONTEND_FIXTURES_MAPPING_PATH || true
- *compress-public
artifacts:

View File

@ -1 +1 @@
2f8c46ee159b96b2c50b43e11654a3864654b05c
32916bff1cffbe5287550db0b960f6b4e47addfe

View File

@ -203,6 +203,7 @@
"WorkItemWidgetAssignees",
"WorkItemWidgetAwardEmoji",
"WorkItemWidgetColor",
"WorkItemWidgetCrmContacts",
"WorkItemWidgetCurrentUserTodos",
"WorkItemWidgetDescription",
"WorkItemWidgetDesigns",

View File

@ -1,12 +1,12 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState } from 'vuex';
import { __ } from '~/locale';
import { getParameterByName, setUrlParams, queryToObject, visitUrl } from '~/lib/utils/url_utility';
import {
SORT_QUERY_PARAM_NAME,
ACTIVE_TAB_QUERY_PARAM_NAME,
AVAILABLE_FILTERED_SEARCH_TOKENS,
FILTERED_SEARCH_MAX_ROLE,
} from 'ee_else_ce/members/constants';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@ -21,6 +21,7 @@ export default {
sourceId: {},
canManageMembers: {},
canFilterByEnterprise: { default: false },
availableRoles: {},
},
data() {
return {
@ -35,6 +36,11 @@ export default {
}),
tokens() {
return this.$options.availableTokens.filter((token) => {
if (token.type === FILTERED_SEARCH_MAX_ROLE.type) {
const maxRoleToken = token;
maxRoleToken.options = this.availableRoles;
}
if (
Object.prototype.hasOwnProperty.call(token, 'requiredPermissions') &&
!this[token.requiredPermissions]
@ -94,14 +100,6 @@ export default {
};
}
} else {
// Remove this block after this issue is closed: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2159
if (value.data === __('Service account')) {
return {
...accumulator,
[type]: 'service_account',
};
}
return {
...accumulator,
[type]: value.data,
@ -133,6 +131,7 @@ export default {
<template>
<filtered-search-bar
:namespace="sourceId.toString()"
terms-as-tokens
:tokens="tokens"
:recent-searches-storage-key="filteredSearchBar.recentSearchesStorageKey"
:search-input-placeholder="filteredSearchBar.placeholder"

View File

@ -153,10 +153,20 @@ export const FILTERED_SEARCH_TOKEN_GROUPS_WITH_INHERITED_PERMISSIONS = {
type: 'groups_with_inherited_permissions',
};
export const FILTERED_SEARCH_MAX_ROLE = {
type: 'max_role',
icon: 'shield',
title: __('Role'),
token: GlFilteredSearchToken,
unique: true,
operators: OPERATORS_IS,
};
export const AVAILABLE_FILTERED_SEARCH_TOKENS = [
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
FILTERED_SEARCH_TOKEN_GROUPS_WITH_INHERITED_PERMISSIONS,
FILTERED_SEARCH_MAX_ROLE,
];
export const AVATAR_SIZE = 48;

View File

@ -30,6 +30,7 @@ export const initMembersApp = (el, options) => {
manageMemberRolesPath,
canApproveAccessRequests,
namespaceUserLimit,
availableRoles,
...vuexStoreAttributes
} = parseDataAttributes(el);
@ -67,6 +68,7 @@ export const initMembersApp = (el, options) => {
manageMemberRolesPath,
canApproveAccessRequests,
namespaceUserLimit,
availableRoles,
group: {
name: groupName,
path: groupPath,

View File

@ -21,7 +21,7 @@ const APP_OPTIONS = {
requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['two_factor', 'with_inherited_permissions', 'enterprise', 'user_type'],
tokens: ['two_factor', 'with_inherited_permissions', 'enterprise', 'user_type', 'max_role'],
searchParam: 'search',
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'group_members',

View File

@ -28,7 +28,7 @@ initMembersApp(document.querySelector('.js-project-members-list-app'), {
requestFormatter: projectMemberRequestFormatter,
filteredSearchBar: {
show: true,
tokens: ['with_inherited_permissions'],
tokens: ['with_inherited_permissions', 'max_role'],
searchParam: 'search',
placeholder: s__('Members|Filter members'),
recentSearchesStorageKey: 'project_members',

View File

@ -173,7 +173,7 @@ export const addHierarchyChild = ({ cache, id, workItem }) => {
(child) => child.id === workItem?.id,
);
if (!existingChild) {
findHierarchyWidgetChildren(draftState?.workItem).push(workItem);
findHierarchyWidgetChildren(draftState?.workItem).unshift(workItem);
}
}),
});

View File

@ -49,7 +49,7 @@
.settings-section,
.settings-section-no-bottom ~ .settings-section {
padding-top: 0;
padding-top: 0 !important;
}
// Fix for sticky header when there is no search bar.
@ -66,7 +66,7 @@
}
.settings-section-no-bottom::after {
padding-bottom: 0;
padding-bottom: 0 !important;
@include media-breakpoint-up(sm) {
padding-bottom: $gl-spacing-scale-5;

View File

@ -83,7 +83,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def filter_params
params.permit(:two_factor, :search, :user_type).merge(sort: @sort)
params.permit(:two_factor, :search, :user_type, :max_role).merge(sort: @sort)
end
def membershipable_members

View File

@ -47,7 +47,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def filter_params
params.permit(:search).merge(sort: @sort)
params.permit(:search, :max_role).merge(sort: @sort)
end
def membershipable_members

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Members
module RoleParser
def get_access_level(role_string)
extract_number(role_string, :static)
end
def get_member_role_id(role_string)
extract_number(role_string, :custom)
end
private
def extract_number(role_string, type)
role_string.try(:match, /^#{type}-(\d+)$/).to_a.second&.to_i
end
end
end

View File

@ -14,6 +14,7 @@ class GroupMembersFinder < UnionFinder
}.freeze
include CreatedAtFilter
include Members::RoleParser
# Params can be any of the following:
# two_factor: string. 'enabled' or 'disabled' are returning different set of data, other values are not effective.
@ -73,6 +74,7 @@ class GroupMembersFinder < UnionFinder
members = members.by_access_level(params[:access_levels]) if params[:access_levels].present?
members = filter_by_user_type(members)
members = filter_by_max_role(members)
members = apply_additional_filters(members)
members = by_created_at(members)
@ -122,6 +124,13 @@ class GroupMembersFinder < UnionFinder
members.filter_by_user_type(params[:user_type])
end
def filter_by_max_role(members)
max_role = get_access_level(params[:max_role])
return members unless max_role&.in?(group.access_level_roles.values)
members.all_by_access_level(max_role).with_static_role
end
def apply_additional_filters(members)
# overridden in EE to include additional filtering conditions.
members

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class MembersFinder
include Members::RoleParser
RELATIONS = %i[direct inherited descendants invited_groups shared_into_ancestors].freeze
DEFAULT_RELATIONS = %i[direct inherited].freeze
@ -53,7 +55,14 @@ class MembersFinder
members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
members = members.owners_and_maintainers if params[:owners_and_maintainers].present?
members
filter_by_max_role(members)
end
def filter_by_max_role(members)
max_role = get_access_level(params[:max_role])
return members unless max_role&.in?(Gitlab::Access.all_values)
members.all_by_access_level(max_role).with_static_role
end
def group_union_members(include_relations)
@ -132,3 +141,5 @@ class MembersFinder
end.join(',')
end
end
MembersFinder.prepend_mod

View File

@ -17,6 +17,10 @@ module Mutations
MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR = 'Please provide either projectPath or namespacePath argument, but not both.'
DISABLED_FF_ERROR = 'create_group_level_work_items feature flag is disabled. Only project paths allowed.'
argument :crm_contacts_widget,
::Types::WorkItems::Widgets::CrmContactsCreateInputType,
required: false,
description: 'Input for CRM contacts widget.'
argument :description,
GraphQL::Types::String,
required: false,

View File

@ -16,6 +16,10 @@ module Mutations
::Types::WorkItems::Widgets::AwardEmojiUpdateInputType,
required: false,
description: 'Input for emoji reactions widget.'
argument :crm_contacts_widget,
::Types::WorkItems::Widgets::CrmContactsUpdateInputType,
required: false,
description: 'Input for CRM contacts widget.'
argument :current_user_todos_widget,
::Types::WorkItems::Widgets::CurrentUserTodosInputType,
required: false,

View File

@ -26,7 +26,8 @@ module Types
::Types::WorkItems::Widgets::ParticipantsType,
::Types::WorkItems::Widgets::TimeTracking::TimeTrackingType,
::Types::WorkItems::Widgets::DesignsType,
::Types::WorkItems::Widgets::DevelopmentType
::Types::WorkItems::Widgets::DevelopmentType,
::Types::WorkItems::Widgets::CrmContactsType
].freeze
def self.ce_orphan_types
@ -70,6 +71,8 @@ module Types
::Types::WorkItems::Widgets::DesignsType
when ::WorkItems::Widgets::Development
::Types::WorkItems::Widgets::DevelopmentType
when ::WorkItems::Widgets::CrmContacts
::Types::WorkItems::Widgets::CrmContactsType
else
raise "Unknown GraphQL type for widget #{object}"
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
class CrmContactsCreateInputType < BaseInputObject
graphql_name 'WorkItemWidgetCrmContactsCreateInput'
argument :contact_ids,
[::Types::GlobalIDType[::CustomerRelations::Contact]],
required: true,
description: 'CRM contact IDs to set.',
prepare: ->(ids, _ctx) { ids.map { |gid| gid.model_id.to_i } }
end
end
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
# Disabling widget level authorization as it might be too granular
# and we already authorize the parent work item
# rubocop:disable Graphql/AuthorizeTypes -- reason above
class CrmContactsType < BaseObject
graphql_name 'WorkItemWidgetCrmContacts'
description 'Represents CRM contacts widget'
implements Types::WorkItems::WidgetInterface
field :contacts,
Types::CustomerRelations::ContactType.connection_type,
null: true,
description: 'Collection of CRM contacts associated with the work item.',
method: :customer_relations_contacts
end
# rubocop:enable Graphql/AuthorizeTypes
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
class CrmContactsUpdateInputType < BaseInputObject
graphql_name 'WorkItemWidgetCrmContactsUpdateInput'
argument :contact_ids,
[::Types::GlobalIDType[::CustomerRelations::Contact]],
required: true,
description: 'CRM contact IDs to set. Replaces existing contacts by default.',
prepare: ->(ids, _ctx) { ids.map { |gid| gid.model_id.to_i } }
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
default_value: Types::MutationOperationModeEnum.default_mode,
description: 'Set the operation mode.'
end
end
end
end

View File

@ -28,7 +28,8 @@ module Groups::GroupMembersHelper
group_name: group.name,
group_path: group.full_path,
can_approve_access_requests: true, # true for CE, overridden in EE
placeholder: placeholder_users
placeholder: placeholder_users,
available_roles: available_group_roles(group)
}
end
# rubocop:enable Metrics/ParameterLists
@ -83,6 +84,13 @@ module Groups::GroupMembersHelper
member_path: group_group_link_path(group, ':id')
}
end
# Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb`
def available_group_roles(group)
group.access_level_roles.sort_by { |_, access_level| access_level }.map do |name, access_level|
{ title: name, value: "static-#{access_level}" }
end
end
end
Groups::GroupMembersHelper.prepend_mod_with('Groups::GroupMembersHelper')

View File

@ -30,7 +30,8 @@ module Projects::ProjectMembersHelper
can_manage_access_requests: Ability.allowed?(current_user, :admin_member_access_request, project),
group_name: project.group&.name,
group_path: project.group&.full_path,
can_approve_access_requests: true # true for CE, overridden in EE
can_approve_access_requests: true, # true for CE, overridden in EE
available_roles: available_project_roles(project)
}
end
@ -92,6 +93,13 @@ module Projects::ProjectMembersHelper
member_path: project_group_link_path(project, ':id')
}
end
# Overridden in `ee/app/helpers/ee/projects/project_members_helper.rb`
def available_project_roles(_)
Gitlab::Access.options_with_owner.map do |name, access_level|
{ title: name, value: "static-#{access_level}" }
end
end
end
Projects::ProjectMembersHelper.prepend_mod_with('Projects::ProjectMembersHelper')

View File

@ -300,6 +300,8 @@ class Member < ApplicationRecord
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
scope :with_static_role, -> { where(member_role_id: nil) }
before_validation :set_member_namespace_id, on: :create
before_validation :generate_invite_token, on: :create, if: ->(member) { member.invite_email.present? && !member.invite_accepted_at? }

View File

@ -5,7 +5,6 @@ class UserDetail < MainClusterwide::ApplicationRecord
extend ::Gitlab::Utils::Override
ignore_column :requires_credit_card_verification, remove_with: '16.1', remove_after: '2023-06-22'
ignore_column :onboarding_step_url, remove_with: '17.1', remove_after: '2024-05-16'
REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze

View File

@ -42,7 +42,8 @@ module WorkItems
participants: 20,
time_tracking: 21,
designs: 22,
development: 23
development: 23,
crm_contacts: 24
}
attribute :widget_options, :ind_jsonb

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module WorkItems
module Widgets
class CrmContacts < Base
delegate :customer_relations_contacts, to: :work_item
def self.quick_action_commands
[:add_contacts, :remove_contacts]
end
def self.quick_action_params
[:contact_emails]
end
end
end
end

View File

@ -17,6 +17,7 @@ module Issuable
def before_update; end
def after_update_commit; end
def after_save_commit; end
def after_save; end
private

View File

@ -408,11 +408,15 @@ class IssuableBaseService < ::BaseContainerService
def transaction_update(issuable, opts = {})
touch = opts[:save_with_touch] || false
issuable.save(touch: touch)
issuable.save(touch: touch).tap do |saved|
@callbacks.each(&:after_save) if saved
end
end
def transaction_create(issuable)
issuable.save
issuable.save.tap do |saved|
@callbacks.each(&:after_save) if saved
end
end
def update_task(issuable)

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
module WorkItems
module Callbacks
class CrmContacts < Base
OPERATION_MODES = {
'APPEND' => :add_ids,
'REMOVE' => :remove_ids,
'REPLACE' => :replace_ids
}.freeze
def after_save
return clear_contacts if excluded_in_new_type?
set_contacts
end
private
def clear_contacts
call_service({ replace_ids: [] })
end
def set_contacts
return unless params.present?
contact_ids = params[:contact_ids]
return if contact_ids.nil?
return if operation_mode_attribute.nil?
raise_error(unsupported_work_item_message) if group.nil?
raise_error(feature_disabled_message) unless feature_enabled?
call_service({ operation_mode_attribute => contact_ids })
end
def call_service(params)
response = ::Issues::SetCrmContactsService.new(
container: work_item.resource_parent,
current_user: current_user,
params: params
).execute(work_item)
raise_error(response.message) unless response.success?
end
def feature_enabled?
group&.crm_enabled?
end
def group
@group ||= work_item.resource_parent.root_ancestor
end
def operation_mode_attribute
@operation_mode_attribute = OPERATION_MODES[params[:operation_mode] || 'REPLACE']
end
def feature_disabled_message
_('Feature disabled')
end
def unsupported_work_item_message
_('Work item not supported')
end
end
end
end

View File

@ -14,7 +14,7 @@ module WorkItems
if params[:relative_position]
link.relative_position = params[:relative_position]
else
link.move_to_end
link.move_to_start
end
create_notes_and_resource_event(work_item, link) if link.changed? && link.save

View File

@ -41,7 +41,8 @@
"enum": [
"project",
"user",
"namespace"
"namespace",
"feature_enabled_by_namespace_ids"
]
}
},

View File

@ -0,0 +1,69 @@
- name: Pipeline execution policy type # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
The pipeline execution policy type is a new type of [security policy](https://docs.gitlab.com/ee/user/application_security/policies/) that allows users to support enforcement of generic CI jobs, scripts, and instructions.
The pipeline execution policy type enables security and compliance teams to enforce customized [GitLab security scanning templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Jobs), [GitLab or partner-supported CI templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates), 3rd party security scanning templates, custom reporting rules through CI jobs, or custom scripts/rules through GitLab CI.
The pipeline execution policy has two modes: inject and override. The _inject_ mode injects jobs into the project's CI/CD pipeline. The _override_ mode replaces the project's CI/CD pipeline configuration.
As with all GitLab policies, enforcement can be managed centrally by designated security and compliance team members who create and manage the policies. [Learn how to get started by creating your first scan execution policy](https://docs.gitlab.com/ee/tutorials/scan_execution_policy/)!
stage: govern # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/application_security/policies/pipeline_execution_policies.html # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/17_2/pipeline-execution-policy-rp.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2024-07-18 # YYYY-MM-DD
release: 17.2 # XX.Y
- name: Document modules in the Terraform module registry # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
The Terraform module registry now displays Readme files! With this highly requested feature, you can transparently document the purpose, configuration, and requirements of each module.
Previously, you had to search other sources for this critical information, which made it difficult to properly evaluate and use modules. Now, with the module documentation readily available, you can quickly understand a module's capabilities before you use it. This accessibility empowers you to confidently share and reuse Terraform code across your organization.
stage: package # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/packages/terraform_module_registry/index.html#view-terraform-modules # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://img.youtube.com/vi/SWRwW4pS7Gk/hqdefault.jpg # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2024-07-18 # YYYY-MM-DD
release: 17.2 # XX.Y
- name: Log streaming for Kubernetes pods and containers # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
In GitLab 16.1, we introduced the Kubernetes pod list and detail views. However, you still had to use third-party tools for an in-depth analysis of your workloads.
GitLab now ships with a log streaming view for pods and containers, so you can quickly check and troubleshoot issues across your environments without leaving your application delivery tool.
stage: deploy # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Free, Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/ci/environments/kubernetes_dashboard.html # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/17_2/k8s-logs-view.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2024-07-18 # YYYY-MM-DD
release: 17.2 # XX.Y
- name: Vulnerability Explanation are generally available # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
Vulnerability Explanation is now a part of GitLab Duo Chat and is generally available. With Vulnerability Explanation, you can open chat from any SAST vulnerability to better understand the vulnerability, see how it could be exploited, and review a potential fix.
stage: Govern
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/application_security/vulnerabilities/#explaining-a-vulnerability # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/17_2/vulnerability_explanation_duo_chat.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2024-07-18 # YYYY-MM-DD
release: 17.2 # XX.Y
- name: GitLab Duo Chat and Code Suggestions available in workspaces # Match the release post entry
description: | # Do not modify this line, instead modify the lines below.
[GitLab Duo Chat](https://docs.gitlab.com/ee/user/gitlab_duo_chat/) and [Code Suggestions](https://docs.gitlab.com/ee/user/project/repository/code_suggestions/) are now available in workspaces! Whether you're seeking quick answers or efficient code improvements, Duo Chat and Code Suggestions are designed to boost productivity and streamline your workflow, making remote development in workspaces more efficient and effective than ever.
stage: create # String value of the stage that the feature was created in. e.g., Growth
self-managed: true # Boolean value (true or false)
gitlab-com: true # Boolean value (true or false)
available_in: [Premium, Ultimate] # Array of strings. The Array brackets are required here. e.g., [Free, Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/gitlab_duo/ # This is the documentation URL, but can be a URL to a video if there is one
image_url: https://about.gitlab.com/images/17_2/workspaces_duo.png # This should be a full URL, generally taken from the release post content. If a video, use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
published_at: 2024-07-18 # YYYY-MM-DD
release: 17.2 # XX.Y

View File

@ -7,5 +7,5 @@ feature_categories:
description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34069
milestone: '13.2'
gitlab_schema: gitlab_main
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442662
gitlab_schema: gitlab_main_cell
exempt_from_sharding: true # data is specific to each cell's Elasticsearch cluster, no customer data

View File

@ -8,5 +8,5 @@ description: Describes a Zoekt server that will be used for indexing and search
some configured namespaces
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105049
milestone: '15.9'
gitlab_schema: gitlab_main
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442657
gitlab_schema: gitlab_main_cell
exempt_from_sharding: true # table scheduled for deletion https://gitlab.com/gitlab-org/gitlab/-/issues/473263

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
class AddCrmContactsWidgetToWorkItemTypes < Gitlab::Database::Migration[2.2]
class WorkItemType < MigrationRecord
self.table_name = 'work_item_types'
end
class WidgetDefinition < MigrationRecord
self.table_name = 'work_item_widget_definitions'
end
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
milestone '17.3'
WIDGET_NAME = 'CrmContacts'
WIDGET_ENUM_VALUE = 24
WORK_ITEM_TYPES = %w[
Epic
Incident
Issue
Task
Ticket
].freeze
def up
widgets = []
WORK_ITEM_TYPES.each do |type_name|
type = WorkItemType.find_by_name_and_namespace_id(type_name, nil)
unless type
Gitlab::AppLogger.warn("type #{type_name} is missing, not adding widget")
next
end
widgets << {
work_item_type_id: type.id,
name: WIDGET_NAME,
widget_type: WIDGET_ENUM_VALUE
}
end
return if widgets.empty?
WidgetDefinition.upsert_all(
widgets,
unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
)
end
def down
WidgetDefinition.where(name: WIDGET_NAME).delete_all
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ValidateFkCiPipelineMetadataPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
TABLE_NAME = :ci_pipeline_metadata
FK_NAME = :fk_rails_50c1e9ea10_p
COLUMNS = [:partition_id, :pipeline_id]
def up
validate_foreign_key(TABLE_NAME, COLUMNS, name: FK_NAME)
end
def down
# no-op
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class RemoveFkToCiPipelinesCiPipelineMetadataOnPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :ci_pipeline_metadata
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_rails_50c1e9ea10
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: COLUMN,
target_column: TARGET_COLUMN,
validate: true,
reverse_lock_order: true,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ValidateFkCiPipelineMessagesPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
TABLE_NAME = :ci_pipeline_messages
FK_NAME = :fk_rails_8d3b04e3e1_p
COLUMNS = [:partition_id, :pipeline_id]
def up
validate_foreign_key(TABLE_NAME, COLUMNS, name: FK_NAME)
end
def down
# no-op
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class RemoveFkToCiPipelinesCiPipelineMessagesOnPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :ci_pipeline_messages
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_rails_8d3b04e3e1
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: COLUMN,
target_column: TARGET_COLUMN,
validate: true,
reverse_lock_order: true,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ValidateFkCiPipelinesConfigPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
TABLE_NAME = :ci_pipelines_config
FK_NAME = :fk_rails_906c9a2533_p
COLUMNS = [:partition_id, :pipeline_id]
def up
validate_foreign_key(TABLE_NAME, COLUMNS, name: FK_NAME)
end
def down
# no-op
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class RemoveFkToCiPipelinesCiPipelinesConfigOnPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :ci_pipelines_config
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_rails_906c9a2533
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: COLUMN,
target_column: TARGET_COLUMN,
validate: true,
reverse_lock_order: true,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ValidateFkCiPipelineArtifactsPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
TABLE_NAME = :ci_pipeline_artifacts
FK_NAME = :fk_rails_a9e811a466_p
COLUMNS = [:partition_id, :pipeline_id]
def up
validate_foreign_key(TABLE_NAME, COLUMNS, name: FK_NAME)
end
def down
# no-op
end
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
class RemoveFkToCiPipelinesCiPipelineArtifactsOnPipelineId < Gitlab::Database::Migration[2.2]
milestone '17.3'
disable_ddl_transaction!
SOURCE_TABLE_NAME = :ci_pipeline_artifacts
TARGET_TABLE_NAME = :ci_pipelines
COLUMN = :pipeline_id
TARGET_COLUMN = :id
FK_NAME = :fk_rails_a9e811a466
def up
with_lock_retries do
remove_foreign_key_if_exists(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
name: FK_NAME,
reverse_lock_order: true
)
end
end
def down
add_concurrent_foreign_key(
SOURCE_TABLE_NAME,
TARGET_TABLE_NAME,
column: COLUMN,
target_column: TARGET_COLUMN,
validate: true,
reverse_lock_order: true,
on_delete: :cascade,
name: FK_NAME
)
end
end

View File

@ -0,0 +1 @@
c56952de16b7aad4aeb45a431a61dc406b32e94bfb2363f7236a2b82fa44fe65

View File

@ -0,0 +1 @@
45e1e83ab25ec467f3ad14a96415fb2d335bf723daf8dc71b00115b2270b9f33

View File

@ -0,0 +1 @@
d77969a695bc010de8703a665e284f7f0fbf246544e48126f9a6be8bc3e0390f

View File

@ -0,0 +1 @@
dbcf6c220b3c32a8539a4526ee06ed5066d61786c1d77d5ed8335a26f3e9ce57

View File

@ -0,0 +1 @@
066447ce6b88ee72c36b8ee0825ae0daca494111f062b3b089f130b858002fbd

View File

@ -0,0 +1 @@
b6f401371482d45d74389295deb6210a8f300066d4feb1a897f88bbc68ce41a0

View File

@ -0,0 +1 @@
07e3bf40e8bdf430074c6713f26afb34cd69e059a37d1bd26b3444cd99892cfb

View File

@ -0,0 +1 @@
3928bda0368ff037bcee8b000cde9c4a032016717a805eb834456b46eb2a85d2

View File

@ -0,0 +1 @@
112202a11e673da5d1b587e958c6174d1a95fa193c33f5c0d500e11af74ad370

View File

@ -34066,10 +34066,7 @@ ALTER TABLE ONLY status_page_settings
ADD CONSTRAINT fk_rails_506e5ba391 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_metadata
ADD CONSTRAINT fk_rails_50c1e9ea10 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_metadata
ADD CONSTRAINT fk_rails_50c1e9ea10_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_rails_50c1e9ea10_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY project_repository_storage_moves
ADD CONSTRAINT fk_rails_5106dbd44a FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@ -34528,10 +34525,7 @@ ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT fk_rails_8c77e5891a FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_pipeline_messages
ADD CONSTRAINT fk_rails_8d3b04e3e1 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_messages
ADD CONSTRAINT fk_rails_8d3b04e3e1_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_rails_8d3b04e3e1_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE incident_management_pending_alert_escalations
ADD CONSTRAINT fk_rails_8d8de95da9 FOREIGN KEY (alert_id) REFERENCES alert_management_alerts(id) ON DELETE CASCADE;
@ -34558,10 +34552,7 @@ ALTER TABLE ONLY organization_details
ADD CONSTRAINT fk_rails_8facb04bef FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipelines_config
ADD CONSTRAINT fk_rails_906c9a2533 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipelines_config
ADD CONSTRAINT fk_rails_906c9a2533_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_rails_906c9a2533_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY approval_project_rules_groups
ADD CONSTRAINT fk_rails_9071e863d1 FOREIGN KEY (approval_project_rule_id) REFERENCES approval_project_rules(id) ON DELETE CASCADE;
@ -34747,10 +34738,7 @@ ALTER TABLE ONLY saved_replies
ADD CONSTRAINT fk_rails_a8bf5bf111 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_artifacts
ADD CONSTRAINT fk_rails_a9e811a466 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_artifacts
ADD CONSTRAINT fk_rails_a9e811a466_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
ADD CONSTRAINT fk_rails_a9e811a466_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_aa1b2961b1 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;

View File

@ -10216,6 +10216,7 @@ Input type: `WorkItemCreateInput`
| <a id="mutationworkitemcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemcreatecolorwidget"></a>`colorWidget` | [`WorkItemWidgetColorInput`](#workitemwidgetcolorinput) | Input for color widget. |
| <a id="mutationworkitemcreateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="mutationworkitemcreatecrmcontactswidget"></a>`crmContactsWidget` | [`WorkItemWidgetCrmContactsCreateInput`](#workitemwidgetcrmcontactscreateinput) | Input for CRM contacts widget. |
| <a id="mutationworkitemcreatedescription"></a>`description` **{warning-solid}** | [`String`](#string) | **Deprecated:** use description widget instead. Deprecated in GitLab 16.9. |
| <a id="mutationworkitemcreatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
| <a id="mutationworkitemcreatehealthstatuswidget"></a>`healthStatusWidget` | [`WorkItemWidgetHealthStatusInput`](#workitemwidgethealthstatusinput) | Input for health status widget. |
@ -10399,6 +10400,7 @@ Input type: `WorkItemUpdateInput`
| <a id="mutationworkitemupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemupdatecolorwidget"></a>`colorWidget` | [`WorkItemWidgetColorInput`](#workitemwidgetcolorinput) | Input for color widget. |
| <a id="mutationworkitemupdateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="mutationworkitemupdatecrmcontactswidget"></a>`crmContactsWidget` | [`WorkItemWidgetCrmContactsUpdateInput`](#workitemwidgetcrmcontactsupdateinput) | Input for CRM contacts widget. |
| <a id="mutationworkitemupdatecurrentusertodoswidget"></a>`currentUserTodosWidget` | [`WorkItemWidgetCurrentUserTodosInput`](#workitemwidgetcurrentusertodosinput) | Input for to-dos widget. |
| <a id="mutationworkitemupdatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. |
| <a id="mutationworkitemupdatehealthstatuswidget"></a>`healthStatusWidget` | [`WorkItemWidgetHealthStatusInput`](#workitemwidgethealthstatusinput) | Input for health status widget. |
@ -33298,6 +33300,17 @@ Represents a color widget.
| <a id="workitemwidgetcolortextcolor"></a>`textColor` | [`String`](#string) | Text color generated for the Work Item. |
| <a id="workitemwidgetcolortype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetCrmContacts`
Represents CRM contacts widget.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetcrmcontactscontacts"></a>`contacts` | [`CustomerRelationsContactConnection`](#customerrelationscontactconnection) | Collection of CRM contacts associated with the work item. (see [Connections](#connections)) |
| <a id="workitemwidgetcrmcontactstype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetCurrentUserTodos`
Represents a todos widget.
@ -36935,6 +36948,7 @@ Type of a work item widget.
| <a id="workitemwidgettypeassignees"></a>`ASSIGNEES` | Assignees widget. |
| <a id="workitemwidgettypeaward_emoji"></a>`AWARD_EMOJI` | Award Emoji widget. |
| <a id="workitemwidgettypecolor"></a>`COLOR` | Color widget. |
| <a id="workitemwidgettypecrm_contacts"></a>`CRM_CONTACTS` | Crm Contacts widget. |
| <a id="workitemwidgettypecurrent_user_todos"></a>`CURRENT_USER_TODOS` | Current User Todos widget. |
| <a id="workitemwidgettypedescription"></a>`DESCRIPTION` | Description widget. |
| <a id="workitemwidgettypedesigns"></a>`DESIGNS` | Designs widget. |
@ -38900,6 +38914,7 @@ Implementations:
- [`WorkItemWidgetAssignees`](#workitemwidgetassignees)
- [`WorkItemWidgetAwardEmoji`](#workitemwidgetawardemoji)
- [`WorkItemWidgetColor`](#workitemwidgetcolor)
- [`WorkItemWidgetCrmContacts`](#workitemwidgetcrmcontacts)
- [`WorkItemWidgetCurrentUserTodos`](#workitemwidgetcurrentusertodos)
- [`WorkItemWidgetDescription`](#workitemwidgetdescription)
- [`WorkItemWidgetDesigns`](#workitemwidgetdesigns)
@ -39807,6 +39822,23 @@ Attributes for value stream stage.
| ---- | ---- | ----------- |
| <a id="workitemwidgetcolorinputcolor"></a>`color` | [`Color!`](#color) | Color of the work item. |
### `WorkItemWidgetCrmContactsCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetcrmcontactscreateinputcontactids"></a>`contactIds` | [`[CustomerRelationsContactID!]!`](#customerrelationscontactid) | CRM contact IDs to set. |
### `WorkItemWidgetCrmContactsUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetcrmcontactsupdateinputcontactids"></a>`contactIds` | [`[CustomerRelationsContactID!]!`](#customerrelationscontactid) | CRM contact IDs to set. Replaces existing contacts by default. |
| <a id="workitemwidgetcrmcontactsupdateinputoperationmode"></a>`operationMode` | [`MutationOperationMode`](#mutationoperationmode) | Set the operation mode. |
### `WorkItemWidgetCurrentUserTodosInput`
#### Arguments

View File

@ -28,7 +28,7 @@ Each event is defined in a separate YAML file consisting of the following fields
| `internal_events` | no | Always `true` for events used in Internal Events. |
| `category` | no | Required for legacy events. Should not be used for Internal Events. |
| `action` | yes | A unique name for the event. Only lowercase, numbers, and underscores are allowed. Use the format `<operation>_<target_of_operation>_<where/when>`. <br/><br/> Ex: `publish_go_module_to_the_registry_from_pipeline` <br/>`<operation> = publish`<br/>`<target> = go_module`<br/>`<when/where> = to_the_registry_from_pipeline`. |
| `identifiers` | no | A list of identifiers sent with the event. Can be set to one or more of `project`, `user`, or `namespace`. |
| `identifiers` | no | A list of identifiers sent with the event. Can be set to one or more of `project`, `user`, `namespace` or `feature_enabled_by_namespace_ids` |
| `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the event. |
| `milestone` | no | The milestone when the event is introduced. |
| `introduced_by_url` | no | The URL to the merge request that introduced the event. |

View File

@ -119,11 +119,11 @@ registry, and copies the image to Google Artifact Registry.
In the example, replace the following:
- <var class="edit" scope="LOCATION"><code>LOCATION</code></var>: the
- <var><code>LOCATION</code></var>: the
Google Cloud region where you created your Google Artifact Registry repository.
- <var class="edit" scope="PROJECT"><code>PROJECT</code></var>: your
- <var><code>PROJECT</code></var>: your
Google Cloud project ID.
- <var class="edit" scope="REPOSITORY"><code>REPOSITORY</code></var>: the
- <var><code>REPOSITORY</code></var>: the
repository ID of your Google Artifact Registry repository.
```yaml
@ -150,7 +150,7 @@ registry, and copies the image to Google Artifact Registry.
inputs:
stage: deploy
source: $GITLAB_IMAGE
target: {{ov}}LOCATION{{cv}}-docker.pkg.dev/{{ov}}PROJECT{{cv}}/{{ov}}REPOSITORY{{cv}}/image:v1.0.0
target: LOCATION-docker.pkg.dev/PROJECT/REPOSITORY/image:v1.0.0
```
The pipeline uses Docker in Docker to build the image `docker:24.0.5`, stores it
@ -231,4 +231,4 @@ avoid exceeding project quota limits.
- Learn how to [Optimize GitLab CI/CD configuration files](../../ci/yaml/yaml_optimization.md).
- Read about how the GitLab on Google Cloud integration uses IAM with
workload identity federation to control access to Google Cloud in [Access control with {{iam_name}}](https://cloud.google.com/docs/gitlab/access-control).
workload identity federation to control access to Google Cloud in [Access control with IAM](https://cloud.google.com/docs/gitlab/access-control).

View File

@ -28,7 +28,8 @@ module Gitlab
participants: 'Participants',
time_tracking: 'Time tracking',
designs: 'Designs',
development: 'Development'
development: 'Development',
crm_contacts: 'CRM contacts'
}.freeze
WIDGETS_FOR_TYPE = {
@ -50,7 +51,8 @@ module Gitlab
:participants,
:time_tracking,
:designs,
:development
:development,
:crm_contacts
],
incident: [
:assignees,
@ -63,7 +65,8 @@ module Gitlab
:linked_items,
:participants,
:time_tracking,
:development
:development,
:crm_contacts
],
test_case: [
:description,
@ -103,7 +106,8 @@ module Gitlab
:award_emoji,
:linked_items,
:participants,
:time_tracking
:time_tracking,
:crm_contacts
],
objective: [
:assignees,
@ -152,7 +156,8 @@ module Gitlab
:color,
:rolledup_dates,
:participants,
:time_tracking
:time_tracking,
:crm_contacts
],
ticket: [
:assignees,
@ -170,7 +175,8 @@ module Gitlab
:award_emoji,
:linked_items,
:participants,
:time_tracking
:time_tracking,
:crm_contacts
]
}.freeze

View File

@ -298,7 +298,7 @@ module Gitlab
types Issue
condition do
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
CustomerRelations::Contact.exists_for_group?(quick_action_target.project.root_ancestor)
CustomerRelations::Contact.exists_for_group?(quick_action_target.resource_parent.root_ancestor)
end
execution_message do
_('One or more contacts were successfully added.')

View File

@ -22189,6 +22189,9 @@ msgstr ""
msgid "Feature Flags"
msgstr ""
msgid "Feature disabled"
msgstr ""
msgid "Feature flag status"
msgstr ""
@ -60265,6 +60268,9 @@ msgstr ""
msgid "Work in progress limit: %{wipLimit}"
msgstr ""
msgid "Work item not supported"
msgstr ""
msgid "WorkItems|An error occurred while fetching children"
msgstr ""

View File

@ -27,10 +27,12 @@ module InternalEventsCli
'(ex - service_desk_request_received)',
%w[namespace] => 'Use case: For namespace-level events without user interaction ' \
'(ex - stale_runners_cleaned_up)',
%w[feature_enabled_by_namespace_ids user] => 'Use case: For user actions attributable to multiple namespaces ' \
'(ex - Code-Suggestions / Duo Pro)',
%w[] => "Use case: For instance-level events without user interaction [LEAST COMMON]"
}.freeze
IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.max_by(&:length).join(', ')}]".length
IDENTIFIER_FORMATTING_BUFFER = "[#{IDENTIFIER_OPTIONS.keys.map { |k| k.join(', ') }.max_by(&:length)}]".length
attr_reader :cli, :event
@ -95,7 +97,8 @@ module InternalEventsCli
identifiers = prompt_for_array_selection(
'Which identifiers are available when the event occurs?',
IDENTIFIER_OPTIONS.keys
IDENTIFIER_OPTIONS.keys,
per_page: IDENTIFIER_OPTIONS.length
) { |choice| format_identifier_choice(choice) }
event.identifiers = identifiers if identifiers.any?

View File

@ -4,14 +4,14 @@
module InternalEventsCli
module Helpers
module CliInputs
def prompt_for_array_selection(message, choices, default = nil, &formatter)
def prompt_for_array_selection(message, choices, default = nil, **opts, &formatter)
formatter ||= ->(choice) { choice.sort.join(", ") }
choices = choices.map do |choice|
{ name: formatter.call(choice), value: choice }
end
cli.select(message, choices, **select_opts) do |menu|
cli.select(message, choices, **select_opts, **opts) do |menu|
menu.enum "."
menu.default formatter.call(default) if default
end

View File

@ -62,7 +62,7 @@ RSpec.describe 'ci jobs dependency', feature_category: :tooling,
let_it_be(:group) { create(:group, path: 'ci-org') }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :empty_repo, group: group, path: "ci") }
let_it_be(:project) { create(:project, :empty_repo, group: group, path: 'ci') }
let_it_be(:ci_glob) { Dir.glob("{.gitlab-ci.yml,.gitlab/**/*.yml}").freeze }
let_it_be(:master_branch) { 'master' }
@ -70,7 +70,8 @@ RSpec.describe 'ci jobs dependency', feature_category: :tooling,
[
{ key: 'CI_SERVER_HOST', value: 'gitlab.com' },
{ key: 'CI_PROJECT_NAMESPACE', value: 'gitlab-org' },
{ key: 'CI_PROJECT_PATH', value: 'gitlab-org/gitlab' }
{ key: 'CI_PROJECT_PATH', value: 'gitlab-org/gitlab' },
{ key: 'CI_PROJECT_NAME', value: 'gitlab' }
]
end
@ -114,7 +115,7 @@ RSpec.describe 'ci jobs dependency', feature_category: :tooling,
context 'with default master pipeline' do
let(:variables_attributes) { gitlab_com_variables_attributes_base }
let(:trigger_source) { :push }
let(:expected_job_name) { 'rspec background_migration pg14 1/5' }
let(:expected_job_name) { 'db:migrate:multi-version-upgrade' }
# Test: remove rules from .rails:rules:setup-test-env
it_behaves_like 'master pipeline'
@ -122,7 +123,7 @@ RSpec.describe 'ci jobs dependency', feature_category: :tooling,
context 'with scheduled nightly' do
let(:trigger_source) { :schedule }
let(:expected_job_name) { 'rspec migration pg16 1/15' }
let(:expected_job_name) { 'db:rollback single-db' }
let(:variables_attributes) do
[
*gitlab_com_variables_attributes_base,
@ -144,7 +145,7 @@ RSpec.describe 'ci jobs dependency', feature_category: :tooling,
context 'with scheduled maintenance' do
let(:trigger_source) { :schedule }
let(:expected_job_name) { 'rspec-ee system pg14 no_gitaly_transactions 1/14' }
let(:expected_job_name) { 'generate-frontend-fixtures-mapping' }
let(:variables_attributes) do
[
*gitlab_com_variables_attributes_base,
@ -161,20 +162,21 @@ RSpec.describe 'ci jobs dependency', feature_category: :tooling,
[
{ key: 'CI_SERVER_HOST', value: 'gitlab.com' },
{ key: 'CI_PROJECT_NAMESPACE', value: 'gitlab-org' },
{ key: 'CI_PROJECT_PATH', value: 'gitlab-org/gitlab-foss' }
{ key: 'CI_PROJECT_PATH', value: 'gitlab-org/gitlab-foss' },
{ key: 'CI_PROJECT_NAME', value: 'gitlab-foss' }
]
end
context 'with master pipeline triggered by push' do
let(:trigger_source) { :push }
let(:expected_job_name) { 'rspec background_migration pg14 1/5' }
let(:expected_job_name) { 'db:backup_and_restore single-db' }
it_behaves_like 'master pipeline'
end
context 'with scheduled master pipeline' do
let(:trigger_source) { :schedule }
let(:expected_job_name) { 'rspec background_migration pg14 1/5' }
let(:expected_job_name) { 'db:backup_and_restore single-db' }
# Verify by removing the following rule from .qa:rules:e2e:test-on-cng
# - !reference [".qa:rules:package-and-test-never-run", rules]

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::RoleParser, feature_category: :groups_and_projects do
using RSpec::Parameterized::TableSyntax
let(:finder_class) do
Class.new do
include Members::RoleParser
def initialize(params)
@params = params
end
def static
get_access_level(params[:max_role])
end
def custom
get_member_role_id(params[:max_role])
end
private
attr_reader :params
end
end
subject(:finder) { finder_class.new(max_role: max_role) }
describe '#static' do
subject(:static) { finder.static }
where(:max_role) { [nil, '', 'static', "xstatic-1", "static-1x"] }
with_them do
it { is_expected.to be_nil }
end
context 'when containing a valid value' do
let(:max_role) { 'static-1' }
it { is_expected.to eq(1) }
end
end
describe '#custom' do
let(:params) { { max_role: max_role } }
subject(:custom) { finder.custom }
where(:max_role) { [nil, '', 'custom', "xcustom-1", "custom-1x"] }
with_them do
it { is_expected.to be_nil }
end
context 'when containing a valid value' do
let(:max_role) { 'custom-1' }
it { is_expected.to eq(1) }
end
end
end

View File

@ -319,6 +319,33 @@ RSpec.describe GroupMembersFinder, '#execute', feature_category: :groups_and_pro
end
end
context 'filter by max role' do
subject(:by_max_role) { described_class.new(group, user1, params: { max_role: max_role }).execute }
let_it_be(:guest_member) { create(:group_member, :guest, group: group, user: user2) }
let_it_be(:owner_member) { create(:group_member, :owner, group: group, user: user3) }
describe 'provided access level is incorrect' do
where(:max_role) { [nil, '', 'static', 'xstatic-50', 'static-50x', 'static-99'] }
with_them do
it { is_expected.to match_array(group.members) }
end
end
describe 'none of the members have the provided access level' do
let(:max_role) { 'static-20' }
it { is_expected.to be_empty }
end
describe 'one of the members has the provided access level' do
let(:max_role) { 'static-50' }
it { is_expected.to contain_exactly(owner_member) }
end
end
context 'filter by non-invite' do
let_it_be(:member) { group.add_maintainer(user1) }
let_it_be(:invited_member) do

View File

@ -279,4 +279,33 @@ RSpec.describe MembersFinder, feature_category: :groups_and_projects do
end
end
end
context 'when filtering by max role' do
subject(:by_max_role) { described_class.new(project, user1, params: { max_role: max_role }).execute }
let_it_be(:guest_member) { create(:project_member, :guest, project: project, user: user2) }
let_it_be(:owner_member) { create(:project_member, :owner, project: project, user: user3) }
describe 'provided access level is incorrect' do
using RSpec::Parameterized::TableSyntax
where(:max_role) { [nil, '', 'static', 'xstatic-50', 'static-50x', 'static-99'] }
with_them do
it { is_expected.to match_array(project.members) }
end
end
describe 'none of the members have the provided access level' do
let(:max_role) { 'static-20' }
it { is_expected.to be_empty }
end
describe 'one of the members has the provided access level' do
let(:max_role) { 'static-50' }
it { is_expected.to contain_exactly(owner_member) }
end
end
end

View File

@ -0,0 +1,24 @@
---
description: Engineer uses Internal Event CLI to define a new event
internal_events: true
action: internal_events_cli_used
identifiers:
- feature_enabled_by_namespace_ids
- user
additional_properties:
label:
description: TODO
property:
description: TODO
value:
description: Time the CLI ran before closing (seconds)
product_group: analytics_instrumentation
milestone: '16.6'
introduced_by_url: TODO
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -75,7 +75,7 @@
- "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
- "Internal Event CLI is opened\n" # Submit description
- "internal_events_cli_opened\n" # Submit action name
- "6\n" # Select: None
- "7\n" # Select: None
- "\n" # Select (add props): None! Continue to next step!
- "\n" # Skip MR URL
- "instrumentation" # Filters to the analytics instrumentation group
@ -130,7 +130,7 @@
- "y\n" # Yes --> Ready to start?
- "Internal Event CLI is opened\n" # Submit description
- "internal_events_cli_opened\n" # Submit action name
- "6\n" # Select: None
- "7\n" # Select: None
- "\n" # Select (add props): None! Continue to next step!
- "\n" # Skip MR URL
- "instrumentation" # Filters to the analytics instrumentation group
@ -248,7 +248,7 @@
- "1\n" # Create another event
- "Internal Event CLI is opened\n" # Submit description
- "internal_events_cli_opened\n" # Submit action name
- "6\n" # Select: None
- "7\n" # Select: None
- "\n" # Select (add props): None! Continue to next step!
- "\n" # Skip MR URL
- "instrumentation" # Filters to the analytics instrumentation group
@ -286,3 +286,27 @@
files:
- path: config/events/internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/events/event_with_all_additional_properties.yml
- description: Event with feature_enabled_by_namespace_ids identifier
inputs:
keystrokes:
- "1\n" # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
- "Engineer uses Internal Event CLI to define a new event\n" # Submit description
- "internal_events_cli_used\n" # Submit action name
- "6\n" # Select: [feature_enabled_by_namespace_ids, user]
- "\e[B\n" # Arrow down to & Select: String 1 (aka label)
- "\n" # Skip label description
- "\e[B\n" # Arrow down to: String 2 (aka property)
- "\n" # Skip property description
- "\e[B\n" # Arrow down to: Number (aka value)
- "Time the CLI ran before closing (seconds)\n" # value description
- "\n" # Skip MR URL
- "instrumentation" # Filters to the analytics instrumentation group
- "\n" # Accept analytics:monitor:analytics_instrumentation
- "1\n" # Select: [free, premium, ultimate]
- "y\n" # Create file
- "4\n" # Exit
outputs:
files:
- path: config/events/internal_events_cli_used.yml
content: spec/fixtures/scripts/internal_events/events/event_with_feature_enabled_by_namespace_ids_identifier.yml

View File

@ -244,7 +244,7 @@
- "1\n" # Create an event
- "Internal Event CLI is opened\n" # Submit description
- "internal_events_cli_opened\n" # Submit action name
- "6\n" # Select: None
- "7\n" # Select: None
- "\n" # Select (add props): None! Continue to next step!
- "\n" # Skip MR URL
- "instrumentation" # Filters to the analytics instrumentation group

View File

@ -9,6 +9,7 @@ import {
MEMBERS_TAB_TYPES,
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
FILTERED_SEARCH_MAX_ROLE,
} from '~/members/constants';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@ -36,7 +37,7 @@ describe('MembersFilteredSearchBar', () => {
state: {
filteredSearchBar: {
show: true,
tokens: [FILTERED_SEARCH_TOKEN_TWO_FACTOR.type],
tokens: [FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, FILTERED_SEARCH_MAX_ROLE.type],
searchParam: 'search',
placeholder: 'Filter members',
recentSearchesStorageKey: 'group_members',
@ -52,6 +53,7 @@ describe('MembersFilteredSearchBar', () => {
sourceId: 1,
canManageMembers: true,
namespace: MEMBERS_TAB_TYPES.user,
availableRoles: [],
...provide,
},
store,
@ -74,7 +76,22 @@ describe('MembersFilteredSearchBar', () => {
it('includes tokens set in `filteredSearchBar.tokens`', () => {
createComponent();
expect(findFilteredSearchBar().props('tokens')).toEqual([FILTERED_SEARCH_TOKEN_TWO_FACTOR]);
expect(findFilteredSearchBar().props('tokens')).toEqual([
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
FILTERED_SEARCH_MAX_ROLE,
]);
});
it('sets the provided `availableRoles` as options to the `max_role` token', () => {
const availableRoles = { title: 'Guest', value: 'static-10' };
createComponent({ provide: { availableRoles } });
const maxRoleToken = findFilteredSearchBar()
.props('tokens')
.find((token) => token.type === FILTERED_SEARCH_MAX_ROLE.type);
expect(maxRoleToken.options).toEqual(availableRoles);
});
describe('when `canManageMembers` is false', () => {
@ -97,9 +114,9 @@ describe('MembersFilteredSearchBar', () => {
},
});
expect(findFilteredSearchBar().props('tokens')).toEqual([
FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS,
]);
expect(findFilteredSearchBar().props('tokens')).not.toContain(
FILTERED_SEARCH_TOKEN_TWO_FACTOR,
);
});
});
});

View File

@ -50,11 +50,11 @@ describe('work items graphql cache utils', () => {
type: WIDGET_TYPE_HIERARCHY,
children: {
nodes: [
child,
{
id: 'gid://gitlab/WorkItem/20',
title: 'Child',
},
child,
],
},
},

View File

@ -30,6 +30,8 @@ RSpec.describe Types::WorkItems::WidgetInterface, feature_category: :team_planni
WorkItems::Widgets::Participants | Types::WorkItems::Widgets::ParticipantsType
WorkItems::Widgets::TimeTracking | Types::WorkItems::Widgets::TimeTracking::TimeTrackingType
WorkItems::Widgets::Designs | Types::WorkItems::Widgets::DesignsType
WorkItems::Widgets::Development | Types::WorkItems::Widgets::DevelopmentType
WorkItems::Widgets::CrmContacts | Types::WorkItems::Widgets::CrmContactsType
end
with_them do

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Types::WorkItems::Widgets::CrmContactsCreateInputType, feature_category: :service_desk do
it { expect(described_class.graphql_name).to eq('WorkItemWidgetCrmContactsCreateInput') }
it { expect(described_class.arguments.keys).to match_array(%w[contactIds]) }
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::CrmContactsType, feature_category: :service_desk do
it 'exposes the expected fields' do
expected_fields = %i[type contacts]
expected_fields.each do |field|
expect(described_class).to have_graphql_field(field)
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Types::WorkItems::Widgets::CrmContactsUpdateInputType, feature_category: :service_desk do
it { expect(described_class.graphql_name).to eq('WorkItemWidgetCrmContactsUpdateInput') }
it { expect(described_class.arguments.keys).to match_array(%w[contactIds operationMode]) }
end

View File

@ -15,6 +15,9 @@ RSpec.describe Groups::GroupMembersHelper do
let(:members) { create_list(:group_member, 2, group: shared_group, created_by: current_user) }
let(:invited) { create_list(:group_member, 2, :invited, group: shared_group, created_by: current_user) }
let!(:access_requests) { create_list(:group_member, 2, :access_request, group: shared_group, created_by: current_user) }
let(:available_roles) do
Gitlab::Access.options_with_owner.map { |name, access_level| { title: name, value: "static-#{access_level}" } }
end
let(:members_collection) { members }
@ -61,7 +64,8 @@ RSpec.describe Groups::GroupMembersHelper do
can_manage_access_requests: be_in([true, false]),
group_name: shared_group.name,
group_path: shared_group.full_path,
can_approve_access_requests: true
can_approve_access_requests: true,
available_roles: available_roles
}
expect(subject).to include(expected)

View File

@ -16,6 +16,9 @@ RSpec.describe Projects::ProjectMembersHelper do
let_it_be(:members) { create_list(:project_member, 2, project: project) }
let_it_be(:invited) { create_list(:project_member, 2, :invited, project: project) }
let_it_be(:access_requests) { create_list(:project_member, 2, :access_request, project: project) }
let(:available_roles) do
Gitlab::Access.options_with_owner.map { |name, access_level| { title: name, value: "static-#{access_level}" } }
end
let(:members_collection) { members }
@ -46,7 +49,8 @@ RSpec.describe Projects::ProjectMembersHelper do
can_manage_access_requests: true,
group_name: project.group.name,
group_path: project.group.path,
can_approve_access_requests: true
can_approve_access_requests: true,
available_roles: available_roles
}.as_json
expect(subject).to include(expected)

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddCrmContactsWidgetToWorkItemTypes, :migration, feature_category: :team_planning do
it_behaves_like 'migration that adds widget to work items definitions',
widget_name: described_class::WIDGET_NAME,
work_item_types: described_class::WORK_ITEM_TYPES
end

View File

@ -3,7 +3,7 @@
require 'spec_helper'
require_migration!
RSpec.describe AddWorkItemsRelatedLinkRestrictions, :migration, feature_category: :portfolio_management do
RSpec.describe AddWorkItemsRelatedLinkRestrictions, :migration_with_transaction, feature_category: :portfolio_management do
let!(:restrictions) { table(:work_item_related_link_restrictions) }
let!(:work_item_types) { table(:work_item_types) }

View File

@ -919,6 +919,14 @@ RSpec.describe Member, feature_category: :groups_and_projects do
end
end
describe '.with_static_role' do
let_it_be(:membership_without_custom_role) { create(:group_member) }
subject { described_class.with_static_role }
it { is_expected.to contain_exactly(membership_without_custom_role) }
end
describe '.with_group_group_sharing_access' do
let_it_be(:shared_group) { create(:group) }
let_it_be(:invited_group) { create(:group) }

View File

@ -19,7 +19,8 @@ RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
::WorkItems::Widgets::Participants,
::WorkItems::Widgets::TimeTracking,
::WorkItems::Widgets::Designs,
::WorkItems::Widgets::Development
::WorkItems::Widgets::Development,
::WorkItems::Widgets::CrmContacts
]
if Gitlab.ee?

View File

@ -144,7 +144,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
create(:parent_link, work_item_parent: parent, work_item: adjacent, relative_position: 0)
end
it 'creates work item and sets the relative position to be AFTER adjacent' do
it 'creates work item and sets the relative position to be BEFORE adjacent' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
@ -157,7 +157,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
'type' => 'HIERARCHY'
}
)
expect(work_item.parent_link.relative_position).to be > adjacent.parent_link.relative_position
expect(work_item.parent_link.relative_position).to be < adjacent.parent_link.relative_position
end
end
end
@ -522,4 +522,71 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
end
end
end
context 'with CRM contacts widget input' do
let(:mutation) { graphql_mutation(:workItemCreate, input.merge('namespacePath' => project.full_path), fields) }
let(:fields) do
<<~FIELDS
workItem {
widgets {
... on WorkItemWidgetCrmContacts {
type
contacts {
nodes {
id
firstName
}
}
}
}
}
errors
FIELDS
end
let_it_be(:contact) { create(:contact, group: project.group) }
shared_examples 'mutation setting work item contacts' do
it 'creates work item with contact data' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
expect(mutation_response['workItem']['widgets']).to include(
'contacts' => {
'nodes' => [
{
'id' => expected_result[:id],
'firstName' => expected_result[:first_name]
}
]
},
'type' => 'CRM_CONTACTS'
)
end
end
context 'when setting the contacts' do
context 'when mutating the work item' do
let(:input) do
{
'title' => 'item1',
'workItemTypeId' => WorkItems::Type.default_by_type(:issue).to_gid.to_s,
'crmContactsWidget' => {
'contactIds' => [global_id_of(contact)]
}
}
end
let(:expected_result) do
{
id: global_id_of(contact).to_s,
first_name: contact.first_name
}
end
it_behaves_like 'mutation setting work item contacts'
end
end
end
end

View File

@ -29,6 +29,16 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:mutation_response) { graphql_mutation_response(:work_item_update) }
shared_examples 'request with error' do |message|
it 'ignores update and returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['workItem']).to be_nil
expect(mutation_response['errors'].first).to include(message)
end
end
context 'the user is not allowed to update a work item' do
let(:current_user) { create(:user) }
@ -1398,18 +1408,6 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
context 'when user can award work item' do
shared_examples 'request with error' do |message|
it 'ignores update and returns an error' do
expect do
update_work_item
end.not_to change(AwardEmoji, :count)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['workItem']).to be_nil
expect(mutation_response['errors'].first).to include(message)
end
end
shared_examples 'request that removes emoji' do
it "updates work item's award emoji" do
expect do
@ -1600,16 +1598,6 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
context 'with time tracking widget input', time_travel_to: "2024-02-20" do
shared_examples 'request with error' do |message|
it 'ignores update and returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['workItem']).to be_nil
expect(mutation_response['errors'].first).to include(message)
end
end
shared_examples 'mutation updating work item with time tracking data' do
it 'updates time tracking' do
expect do
@ -1787,6 +1775,73 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
end
context 'with CRM contacts widget input' do
let(:fields) do
<<~FIELDS
workItem {
widgets {
... on WorkItemWidgetCrmContacts {
type
contacts {
nodes {
id
firstName
}
}
}
}
}
errors
FIELDS
end
let_it_be(:contact) { create(:contact, group: project.group) }
shared_examples 'mutation updating work item contacts' do
it 'updates contacts' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
mutation_work_item.reload
end.to change { mutation_work_item.customer_relations_contacts.to_a }.from([]).to([
contact
])
expect(mutation_response['workItem']['widgets']).to include(
'contacts' => {
'nodes' => [
{
'id' => expected_result[:id],
'firstName' => expected_result[:first_name]
}
]
},
'type' => 'CRM_CONTACTS'
)
end
end
context 'when updating the contacts' do
context 'when mutating the work item' do
let(:input) do
{
'crmContactsWidget' => {
'contactIds' => [global_id_of(contact)]
}
}
end
let(:expected_result) do
{
id: global_id_of(contact).to_s,
first_name: contact.first_name
}
end
it_behaves_like 'mutation updating work item contacts'
end
end
end
context 'when unsupported widget input is sent' do
let_it_be(:work_item) { create(:work_item, :test_case, project: project) }

View File

@ -1036,7 +1036,7 @@ RSpec.describe Cli, feature_category: :service_ping do
"1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
"Internal Event CLI is opened\n", # Submit description
"internal_events_cli_opened\n", # Submit action name
"6\n", # Select: None
"7\n", # Select: None
"\n", # Select: None! Continue to next section!
"\n", # Skip MR URL
"analytics_instrumentation\n", # Input group
@ -1109,7 +1109,7 @@ RSpec.describe Cli, feature_category: :service_ping do
"1\n", # Enum-select: New Event -- start tracking when an action or scenario occurs on gitlab instances
"Internal Event CLI is opened\n", # Submit description
"internal_events_cli_opened\n", # Submit action name
"6\n", # Select: None
"7\n", # Select: None
"\n", # Select: None! Continue to next section!
"\n", # Skip MR URL
"instrumentation\n", # Filter & select group

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::Callbacks::CrmContacts, feature_category: :service_desk do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, owners: user) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:work_item) { create(:work_item, project: project) }
let_it_be(:contact) { create(:contact, group: group) }
let(:default_params) { { contact_ids: [contact.id] } }
let(:params) { default_params }
let(:set_crm_contacts_service) { instance_double(::Issues::SetCrmContactsService, execute: nil) }
subject(:callback) { described_class.new(issuable: work_item, current_user: user, params: params).after_save }
before do
allow(::Issues::SetCrmContactsService).to receive(:new).and_return(set_crm_contacts_service)
end
shared_examples 'does not call SetCrmContactsService' do
it 'is not called' do
callback
expect(::Issues::SetCrmContactsService).not_to have_received(:new)
end
end
shared_examples 'raises a WidgetError' do
let(:error_class) { ::WorkItems::Widgets::BaseService::WidgetError }
it { expect { callback }.to raise_error(error_class, message) }
end
context 'when work item belongs to a project' do
it 'updates the contacts' do
allow(::Issues::SetCrmContactsService).to receive(:new).and_call_original
callback
expect(work_item.customer_relations_contacts).to contain_exactly(contact)
end
end
context 'when work item belongs to a group' do
let(:work_item) { create(:work_item, :group_level, namespace: group) }
it 'updates the contacts' do
allow(::Issues::SetCrmContactsService).to receive(:new).and_call_original
callback
expect(work_item.customer_relations_contacts).to contain_exactly(contact)
end
end
context 'when contact_ids param is missing' do
let(:params) { { operation_mode: 'APPEND' } }
it_behaves_like 'does not call SetCrmContactsService'
end
context 'when operation_mode param is invalid' do
let(:params) { { operation_mode: 'BOB' } }
it_behaves_like 'does not call SetCrmContactsService'
end
context 'when work item does not have a parent group' do
let(:user_namespace_project) { build_stubbed(:project, namespace: user.namespace) }
let(:work_item) { build_stubbed(:work_item, project: user_namespace_project) }
let(:message) { 'Work item not supported' }
it_behaves_like 'raises a WidgetError'
end
context 'when feature is disabled' do
let(:work_item) { WorkItem.new(project: Project.new(group: create(:group, :crm_disabled))) }
let(:message) { 'Feature disabled' }
it_behaves_like 'raises a WidgetError'
end
context 'when SetCrmContactsService returns error response' do
let(:message) { 'Something went wrong!' }
before do
allow(set_crm_contacts_service).to receive(:execute).and_return(ServiceResponse.error(message: message))
end
it_behaves_like 'raises a WidgetError'
end
end

View File

@ -83,9 +83,9 @@ RSpec.describe WorkItems::ParentLinks::CreateService, feature_category: :portfol
subject { described_class.new(parent_item, user, { target_issuable: current_item }).execute }
where(:adjacent_position, :expected_order) do
-100 | lazy { [adjacent, current_item] }
0 | lazy { [adjacent, current_item] }
100 | lazy { [adjacent, current_item] }
-100 | lazy { [current_item, adjacent] }
0 | lazy { [current_item, adjacent] }
100 | lazy { [current_item, adjacent] }
end
with_them do

View File

@ -8,7 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
github.com/BurntSushi/toml v1.4.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/aws/aws-sdk-go v1.54.6
github.com/aws/aws-sdk-go v1.54.18
github.com/disintegration/imaging v1.6.2
github.com/dlclark/regexp2 v1.11.1
github.com/getsentry/raven-go v0.2.0
@ -34,7 +34,7 @@ require (
golang.org/x/net v0.26.0
golang.org/x/oauth2 v0.21.0
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
google.golang.org/grpc v1.64.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
honnef.co/go/tools v0.4.7
)
@ -61,7 +61,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/client9/reopen v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@ -127,8 +127,8 @@ require (
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.169.0 // indirect
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -96,8 +96,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.54.6 h1:HEYUib3yTt8E6vxjMWM3yAq5b+qjj/6aKA62mkgux9g=
github.com/aws/aws-sdk-go v1.54.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.54.18 h1:t8DGtN8A2wEiazoJxeDbfPsbxCKtjoRLuO7jBSgJzo4=
github.com/aws/aws-sdk-go v1.54.18/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0=
github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
@ -151,8 +151,8 @@ github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMr
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -912,10 +912,10 @@ google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKr
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s=
google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@ -942,8 +942,8 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=