Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a33559afd2
commit
06319330df
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2f8c46ee159b96b2c50b43e11654a3864654b05c
|
||||
32916bff1cffbe5287550db0b960f6b4e47addfe
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@
|
|||
"WorkItemWidgetAssignees",
|
||||
"WorkItemWidgetAwardEmoji",
|
||||
"WorkItemWidgetColor",
|
||||
"WorkItemWidgetCrmContacts",
|
||||
"WorkItemWidgetCurrentUserTodos",
|
||||
"WorkItemWidgetDescription",
|
||||
"WorkItemWidgetDesigns",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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? }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@
|
|||
"enum": [
|
||||
"project",
|
||||
"user",
|
||||
"namespace"
|
||||
"namespace",
|
||||
"feature_enabled_by_namespace_ids"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
c56952de16b7aad4aeb45a431a61dc406b32e94bfb2363f7236a2b82fa44fe65
|
||||
|
|
@ -0,0 +1 @@
|
|||
45e1e83ab25ec467f3ad14a96415fb2d335bf723daf8dc71b00115b2270b9f33
|
||||
|
|
@ -0,0 +1 @@
|
|||
d77969a695bc010de8703a665e284f7f0fbf246544e48126f9a6be8bc3e0390f
|
||||
|
|
@ -0,0 +1 @@
|
|||
dbcf6c220b3c32a8539a4526ee06ed5066d61786c1d77d5ed8335a26f3e9ce57
|
||||
|
|
@ -0,0 +1 @@
|
|||
066447ce6b88ee72c36b8ee0825ae0daca494111f062b3b089f130b858002fbd
|
||||
|
|
@ -0,0 +1 @@
|
|||
b6f401371482d45d74389295deb6210a8f300066d4feb1a897f88bbc68ce41a0
|
||||
|
|
@ -0,0 +1 @@
|
|||
07e3bf40e8bdf430074c6713f26afb34cd69e059a37d1bd26b3444cd99892cfb
|
||||
|
|
@ -0,0 +1 @@
|
|||
3928bda0368ff037bcee8b000cde9c4a032016717a805eb834456b46eb2a85d2
|
||||
|
|
@ -0,0 +1 @@
|
|||
112202a11e673da5d1b587e958c6174d1a95fa193c33f5c0d500e11af74ad370
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
Loading…
Reference in New Issue