Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4c7e34071e
commit
4220cf46a3
69 changed files with 889 additions and 125 deletions
|
@ -11,7 +11,6 @@ Style/OpenStructUse:
|
|||
- lib/gitlab/git/diff_collection.rb
|
||||
- lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
|
||||
- lib/gitlab/testing/request_inspector_middleware.rb
|
||||
- lib/mattermost/session.rb
|
||||
- spec/factories/go_module_versions.rb
|
||||
- spec/factories/wiki_pages.rb
|
||||
- spec/graphql/mutations/branches/create_spec.rb
|
||||
|
|
|
@ -1 +1 @@
|
|||
f21e9469e94600f50ecb01b98d46f54dd7b33b5c
|
||||
719c5a5bd2b5ddb54de519d6873ccb1636f7b450
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { uniqueId } from 'lodash';
|
||||
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
|
||||
|
@ -8,11 +8,12 @@ export default {
|
|||
components: {
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
inject: ['path'],
|
||||
inject: ['path', 'name'],
|
||||
data() {
|
||||
return {
|
||||
modalId: uniqueId('remove-topic-avatar-'),
|
||||
|
@ -25,8 +26,8 @@ export default {
|
|||
},
|
||||
i18n: {
|
||||
remove: __('Remove avatar'),
|
||||
title: __('Confirm remove avatar'),
|
||||
body: __('Avatar will be removed. Are you sure?'),
|
||||
title: __('Remove topic avatar'),
|
||||
body: __('Topic avatar for %{name} will be removed. This cannot be undone.'),
|
||||
},
|
||||
modal: {
|
||||
actionPrimary: {
|
||||
|
@ -57,7 +58,9 @@ export default {
|
|||
:modal-id="modalId"
|
||||
size="sm"
|
||||
@primary="deleteApplication"
|
||||
>{{ $options.i18n.body }}
|
||||
><gl-sprintf :message="$options.i18n.body"
|
||||
><template #name>{{ name }}</template></gl-sprintf
|
||||
>
|
||||
<form ref="deleteForm" method="post" :action="path">
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
|
||||
|
|
|
@ -8,12 +8,13 @@ export default () => {
|
|||
return false;
|
||||
}
|
||||
|
||||
const { path } = el.dataset;
|
||||
const { path, name } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
path,
|
||||
name,
|
||||
},
|
||||
render(h) {
|
||||
return h(RemoveAvatar);
|
||||
|
|
|
@ -12,6 +12,7 @@ import ZenMode from '~/zen_mode';
|
|||
|
||||
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
|
||||
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
|
||||
const DATA_ISSUES_NEW_PATH = 'data-new-issue-path';
|
||||
|
||||
function organizeQuery(obj, isFallbackKey = false) {
|
||||
if (!obj[MR_SOURCE_BRANCH] && !obj[MR_TARGET_BRANCH]) {
|
||||
|
@ -68,6 +69,7 @@ export default class IssuableForm {
|
|||
this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search');
|
||||
this.zenMode = new ZenMode();
|
||||
|
||||
this.newIssuePath = form[0].getAttribute(DATA_ISSUES_NEW_PATH);
|
||||
this.titleField = this.form.find('input[name*="[title]"]');
|
||||
this.descriptionField = this.form.find('textarea[name*="[description]"]');
|
||||
if (!(this.titleField.length && this.descriptionField.length)) {
|
||||
|
@ -104,8 +106,8 @@ export default class IssuableForm {
|
|||
}
|
||||
|
||||
initAutosave() {
|
||||
const { search } = document.location;
|
||||
const searchTerm = format(search);
|
||||
const { search, pathname } = document.location;
|
||||
const searchTerm = this.newIssuePath === pathname ? '' : format(search);
|
||||
const fallbackKey = getFallbackKey();
|
||||
|
||||
this.autosave = new Autosave(
|
||||
|
|
|
@ -48,4 +48,4 @@ export function initJiraConnect() {
|
|||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initJiraConnect);
|
||||
initJiraConnect();
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import initClustersListApp from '~/clusters_list';
|
||||
import PersistentUserCallout from '~/persistent_user_callout';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const callout = document.querySelector('.gcp-signup-offer');
|
||||
PersistentUserCallout.factory(callout);
|
||||
initClustersListApp();
|
||||
});
|
||||
const callout = document.querySelector('.gcp-signup-offer');
|
||||
PersistentUserCallout.factory(callout);
|
||||
initClustersListApp();
|
||||
|
|
|
@ -10,21 +10,19 @@ import initSearchSettings from '~/search_settings';
|
|||
import initSettingsPanels from '~/settings_panels';
|
||||
import initConfirmDanger from '~/init_confirm_danger';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initFilePickers();
|
||||
initConfirmDanger();
|
||||
initSettingsPanels();
|
||||
initTransferGroupForm();
|
||||
dirtySubmitFactory(
|
||||
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
|
||||
);
|
||||
mountBadgeSettings(GROUP_BADGE);
|
||||
initFilePickers();
|
||||
initConfirmDanger();
|
||||
initSettingsPanels();
|
||||
initTransferGroupForm();
|
||||
dirtySubmitFactory(
|
||||
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
|
||||
);
|
||||
mountBadgeSettings(GROUP_BADGE);
|
||||
|
||||
// Initialize Subgroups selector
|
||||
groupsSelect();
|
||||
// Initialize Subgroups selector
|
||||
groupsSelect();
|
||||
|
||||
projectSelect();
|
||||
projectSelect();
|
||||
|
||||
initSearchSettings();
|
||||
initCascadingSettingsLockPopovers();
|
||||
});
|
||||
initSearchSettings();
|
||||
initCascadingSettingsLockPopovers();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initProfilePreferences);
|
||||
initProfilePreferences();
|
||||
|
|
|
@ -10,36 +10,34 @@ import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_to
|
|||
import initSettingsPanels from '~/settings_panels';
|
||||
import { initTokenAccess } from '~/token_access';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize expandable settings panels
|
||||
initSettingsPanels();
|
||||
// Initialize expandable settings panels
|
||||
initSettingsPanels();
|
||||
|
||||
const runnerToken = document.querySelector('.js-secret-runner-token');
|
||||
if (runnerToken) {
|
||||
const runnerTokenSecretValue = new SecretValues({
|
||||
container: runnerToken,
|
||||
});
|
||||
runnerTokenSecretValue.init();
|
||||
}
|
||||
|
||||
initVariableList();
|
||||
|
||||
// hide extra auto devops settings based checkbox state
|
||||
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
|
||||
const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
|
||||
document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
|
||||
const { target } = event;
|
||||
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
|
||||
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
|
||||
const runnerToken = document.querySelector('.js-secret-runner-token');
|
||||
if (runnerToken) {
|
||||
const runnerTokenSecretValue = new SecretValues({
|
||||
container: runnerToken,
|
||||
});
|
||||
runnerTokenSecretValue.init();
|
||||
}
|
||||
|
||||
registrySettingsApp();
|
||||
initDeployFreeze();
|
||||
initVariableList();
|
||||
|
||||
initSettingsPipelinesTriggers();
|
||||
initArtifactsSettings();
|
||||
initSharedRunnersToggle();
|
||||
initInstallRunner();
|
||||
initRunnerAwsDeployments();
|
||||
initTokenAccess();
|
||||
// hide extra auto devops settings based checkbox state
|
||||
const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
|
||||
const instanceDefaultBadge = document.querySelector('.js-instance-default-badge');
|
||||
document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => {
|
||||
const { target } = event;
|
||||
if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none';
|
||||
autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked);
|
||||
});
|
||||
|
||||
registrySettingsApp();
|
||||
initDeployFreeze();
|
||||
|
||||
initSettingsPipelinesTriggers();
|
||||
initArtifactsSettings();
|
||||
initSharedRunnersToggle();
|
||||
initInstallRunner();
|
||||
initRunnerAwsDeployments();
|
||||
initTokenAccess();
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import MirrorRepos from '~/mirrors/mirror_repos';
|
||||
import initForm from '../form';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initForm();
|
||||
initForm();
|
||||
|
||||
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
|
||||
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
|
||||
});
|
||||
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
|
||||
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
|
||||
|
|
|
@ -239,6 +239,12 @@ module IssuesHelper
|
|||
)
|
||||
end
|
||||
|
||||
def issues_form_data(project)
|
||||
{
|
||||
new_issue_path: new_project_issue_path(project)
|
||||
}
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def scoped_labels_available?(parent)
|
||||
false
|
||||
|
|
|
@ -23,7 +23,7 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
validates :last_name, presence: true, length: { maximum: 255 }
|
||||
validates :email, length: { maximum: 255 }
|
||||
validates :description, length: { maximum: 1024 }
|
||||
validates :email, uniqueness: { scope: :group_id }
|
||||
validates :email, uniqueness: { case_sensitive: false, scope: :group_id }
|
||||
validate :validate_email_format
|
||||
validate :validate_root_group
|
||||
|
||||
|
@ -42,7 +42,7 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
def self.find_ids_by_emails(group, emails)
|
||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||
|
||||
where(group: group, email: emails).pluck(:id)
|
||||
where(group: group).where('lower(email) in (?)', emails.map(&:downcase)).pluck(:id)
|
||||
end
|
||||
|
||||
def self.exists_for_group?(group)
|
||||
|
@ -51,6 +51,34 @@ class CustomerRelations::Contact < ApplicationRecord
|
|||
exists?(group: group)
|
||||
end
|
||||
|
||||
def self.move_to_root_group(group)
|
||||
update_query = <<~SQL
|
||||
UPDATE #{CustomerRelations::IssueContact.table_name}
|
||||
SET contact_id = new_contacts.id
|
||||
FROM #{table_name} AS existing_contacts
|
||||
JOIN #{table_name} AS new_contacts ON new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
|
||||
WHERE existing_contacts.group_id = :new_group_id AND contact_id = existing_contacts.id
|
||||
SQL
|
||||
connection.execute(sanitize_sql([
|
||||
update_query,
|
||||
old_group_id: group.root_ancestor.id,
|
||||
new_group_id: group.id
|
||||
]))
|
||||
|
||||
dupes_query = <<~SQL
|
||||
DELETE FROM #{table_name} AS existing_contacts
|
||||
USING #{table_name} AS new_contacts
|
||||
WHERE existing_contacts.group_id = :new_group_id AND new_contacts.group_id = :old_group_id AND LOWER(new_contacts.email) = LOWER(existing_contacts.email)
|
||||
SQL
|
||||
connection.execute(sanitize_sql([
|
||||
dupes_query,
|
||||
old_group_id: group.root_ancestor.id,
|
||||
new_group_id: group.id
|
||||
]))
|
||||
|
||||
where(group: group).update_all(group_id: group.root_ancestor.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_email_format
|
||||
|
|
|
@ -8,6 +8,8 @@ class CustomerRelations::IssueContact < ApplicationRecord
|
|||
|
||||
validate :contact_belongs_to_root_group
|
||||
|
||||
BATCH_DELETE_SIZE = 1_000
|
||||
|
||||
def self.find_contact_ids_by_emails(issue_id, emails)
|
||||
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
|
||||
|
||||
|
@ -17,9 +19,17 @@ class CustomerRelations::IssueContact < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.delete_for_project(project_id)
|
||||
joins(:issue)
|
||||
.where(issues: { project_id: project_id })
|
||||
.delete_all
|
||||
loop do
|
||||
deleted_records = joins(:issue).where(issues: { project_id: project_id }).limit(BATCH_DELETE_SIZE).delete_all
|
||||
break if deleted_records == 0
|
||||
end
|
||||
end
|
||||
|
||||
def self.delete_for_group(group)
|
||||
loop do
|
||||
deleted_records = joins(issue: :project).where(projects: { namespace: group.self_and_descendants }).limit(BATCH_DELETE_SIZE).delete_all
|
||||
break if deleted_records == 0
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -26,6 +26,34 @@ class CustomerRelations::Organization < ApplicationRecord
|
|||
.where('LOWER(name) = LOWER(?)', name)
|
||||
end
|
||||
|
||||
def self.move_to_root_group(group)
|
||||
update_query = <<~SQL
|
||||
UPDATE #{CustomerRelations::Contact.table_name}
|
||||
SET organization_id = new_organizations.id
|
||||
FROM #{table_name} AS existing_organizations
|
||||
JOIN #{table_name} AS new_organizations ON new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
|
||||
WHERE existing_organizations.group_id = :new_group_id AND organization_id = existing_organizations.id
|
||||
SQL
|
||||
connection.execute(sanitize_sql([
|
||||
update_query,
|
||||
old_group_id: group.root_ancestor.id,
|
||||
new_group_id: group.id
|
||||
]))
|
||||
|
||||
dupes_query = <<~SQL
|
||||
DELETE FROM #{table_name} AS existing_organizations
|
||||
USING #{table_name} AS new_organizations
|
||||
WHERE existing_organizations.group_id = :new_group_id AND new_organizations.group_id = :old_group_id AND LOWER(new_organizations.name) = LOWER(existing_organizations.name)
|
||||
SQL
|
||||
connection.execute(sanitize_sql([
|
||||
dupes_query,
|
||||
old_group_id: group.root_ancestor.id,
|
||||
new_group_id: group.id
|
||||
]))
|
||||
|
||||
where(group: group).update_all(group_id: group.root_ancestor.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_root_group
|
||||
|
|
|
@ -61,8 +61,9 @@ class Group < Namespace
|
|||
has_many :boards
|
||||
has_many :badges, class_name: 'GroupBadge'
|
||||
|
||||
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
|
||||
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
|
||||
# AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all
|
||||
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
||||
has_many :cluster_groups, class_name: 'Clusters::Group'
|
||||
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
|
||||
|
|
|
@ -15,6 +15,7 @@ class Namespace < ApplicationRecord
|
|||
include Namespaces::Traversal::Recursive
|
||||
include Namespaces::Traversal::Linear
|
||||
include EachBatch
|
||||
include BlocksUnsafeSerialization
|
||||
|
||||
# Temporary column used for back-filling project namespaces.
|
||||
# Remove it once the back-filling of all project namespaces is done.
|
||||
|
@ -660,6 +661,10 @@ class Namespace < ApplicationRecord
|
|||
# Use SHA2 of `traversal_ids` to account for moving a namespace within the same root ancestor hierarchy.
|
||||
"namespaces:{#{traversal_ids.first}}:first_auto_devops_config:#{group_id}:#{Digest::SHA2.hexdigest(traversal_ids.join(' '))}"
|
||||
end
|
||||
|
||||
def allow_serialization?(options = nil)
|
||||
Feature.disabled?(:block_namespace_serialization, self, default_enabled: :yaml) || super
|
||||
end
|
||||
end
|
||||
|
||||
Namespace.prepend_mod_with('Namespace')
|
||||
|
|
|
@ -9,10 +9,5 @@ module IncidentManagement
|
|||
|
||||
track_usage_event(:"incident_management_#{action}", current_user.id)
|
||||
end
|
||||
|
||||
# No-op as optionally overridden in implementing classes.
|
||||
# For use to provide checks before calling #track_incident_action.
|
||||
def track_event
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,10 +25,15 @@ module Groups
|
|||
private
|
||||
|
||||
def proceed_to_transfer
|
||||
old_root_ancestor_id = @group.root_ancestor.id
|
||||
was_root_group = @group.root?
|
||||
|
||||
Group.transaction do
|
||||
update_group_attributes
|
||||
ensure_ownership
|
||||
update_integrations
|
||||
remove_issue_contacts(old_root_ancestor_id, was_root_group)
|
||||
update_crm_objects(was_root_group)
|
||||
end
|
||||
|
||||
post_update_hooks(@updated_project_ids)
|
||||
|
@ -53,6 +58,17 @@ module Groups
|
|||
raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
|
||||
raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup?
|
||||
raise_transfer_error(:group_contains_npm_packages) if group_with_npm_packages?
|
||||
raise_transfer_error(:no_permissions_to_migrate_crm) if no_permissions_to_migrate_crm?
|
||||
end
|
||||
|
||||
def no_permissions_to_migrate_crm?
|
||||
return false unless group && @new_parent_group
|
||||
return false if group.root_ancestor == @new_parent_group.root_ancestor
|
||||
|
||||
return true if group.contacts.exists? && !current_user.can?(:admin_crm_contact, @new_parent_group.root_ancestor)
|
||||
return true if group.organizations.exists? && !current_user.can?(:admin_crm_organization, @new_parent_group.root_ancestor)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def group_with_npm_packages?
|
||||
|
@ -202,7 +218,8 @@ module Groups
|
|||
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
|
||||
group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.'),
|
||||
cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.'),
|
||||
group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.')
|
||||
group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.'),
|
||||
no_permissions_to_migrate_crm: s_("TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
|
||||
}.freeze
|
||||
end
|
||||
|
||||
|
@ -238,6 +255,20 @@ module Groups
|
|||
namespace_id: group.id
|
||||
}
|
||||
end
|
||||
|
||||
def update_crm_objects(was_root_group)
|
||||
return unless was_root_group
|
||||
|
||||
CustomerRelations::Contact.move_to_root_group(group)
|
||||
CustomerRelations::Organization.move_to_root_group(group)
|
||||
end
|
||||
|
||||
def remove_issue_contacts(old_root_ancestor_id, was_root_group)
|
||||
return if was_root_group
|
||||
return if old_root_ancestor_id == @group.root_ancestor.id
|
||||
|
||||
CustomerRelations::IssueContact.delete_for_group(@group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
module IssuableLinks
|
||||
class CreateService < BaseService
|
||||
include IncidentManagement::UsageData
|
||||
|
||||
attr_reader :issuable, :current_user, :params
|
||||
|
||||
def initialize(issuable, user, params)
|
||||
|
@ -25,7 +23,7 @@ module IssuableLinks
|
|||
end
|
||||
|
||||
@errors = []
|
||||
create_links
|
||||
references = create_links
|
||||
|
||||
if @errors.present?
|
||||
return error(@errors.join('. '), 422)
|
||||
|
@ -33,7 +31,7 @@ module IssuableLinks
|
|||
|
||||
track_event
|
||||
|
||||
success
|
||||
success(created_references: references)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
@ -66,7 +64,7 @@ module IssuableLinks
|
|||
end
|
||||
|
||||
def link_issuables(target_issuables)
|
||||
target_issuables.each do |referenced_object|
|
||||
target_issuables.map do |referenced_object|
|
||||
link = relate_issuables(referenced_object)
|
||||
|
||||
unless link.valid?
|
||||
|
@ -75,6 +73,8 @@ module IssuableLinks
|
|||
error: link.errors.messages.values.flatten.to_sentence
|
||||
}
|
||||
end
|
||||
|
||||
link
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -142,6 +142,10 @@ module IssuableLinks
|
|||
def set_link_type(_link)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def track_event
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
module IssuableLinks
|
||||
class DestroyService < BaseService
|
||||
include IncidentManagement::UsageData
|
||||
|
||||
attr_reader :link, :current_user, :source, :target
|
||||
|
||||
def initialize(link, user)
|
||||
|
@ -41,5 +39,9 @@ module IssuableLinks
|
|||
def not_found_message
|
||||
'No Issue Link found'
|
||||
end
|
||||
|
||||
def track_event
|
||||
# no op
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module IssueLinks
|
||||
class CreateService < IssuableLinks::CreateService
|
||||
include IncidentManagement::UsageData
|
||||
|
||||
def linkable_issuables(issues)
|
||||
@linkable_issuables ||= begin
|
||||
issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module IssueLinks
|
||||
class DestroyService < IssuableLinks::DestroyService
|
||||
include IncidentManagement::UsageData
|
||||
|
||||
private
|
||||
|
||||
def permission_to_remove_relation?
|
||||
|
|
|
@ -111,7 +111,7 @@ module Notes
|
|||
def track_event(note, user)
|
||||
track_note_creation_usage_for_issues(note) if note.for_issue?
|
||||
track_note_creation_usage_for_merge_requests(note) if note.for_merge_request?
|
||||
track_usage_event(:incident_management_incident_comment, user.id) if note.for_issue? && note.noteable.incident?
|
||||
track_incident_action(user, note.noteable, 'incident_comment') if note.for_issue?
|
||||
|
||||
if Feature.enabled?(:notes_create_service_tracking, project)
|
||||
Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note))
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
= topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s90')
|
||||
= render 'shared/choose_avatar_button', f: f
|
||||
- if @topic.avatar?
|
||||
.js-remove-topic-avatar{ data: { path: admin_topic_avatar_path(@topic) } }
|
||||
.js-remove-topic-avatar{ data: { path: admin_topic_avatar_path(@topic), name: @topic.name } }
|
||||
|
||||
- if @topic.new_record?
|
||||
.form-actions
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
= form_for [@project, @issue],
|
||||
html: { class: 'issue-form common-note-form gl-mt-3 js-quick-submit gl-show-field-errors' } do |f|
|
||||
html: { class: 'issue-form common-note-form gl-mt-3 js-quick-submit gl-show-field-errors', data: issues_form_data(@project) } do |f|
|
||||
= render 'shared/issuable/form', f: f, issuable: @issue
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: block_namespace_serialization
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82661
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355553
|
||||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::global search
|
||||
default_enabled: false
|
|
@ -29,7 +29,7 @@ class Gitlab::Seeder::Crm
|
|||
group_id: group.id,
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
email: "#{first_name}.#{last_name}@example.org",
|
||||
email: "#{first_name}.#{last_name}-#{index}@example.org",
|
||||
organization_id: organization_id
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
|
||||
def up
|
||||
create_table :protected_environment_approval_rules do |t|
|
||||
t.references :protected_environment,
|
||||
index: { name: :index_approval_rule_on_protected_environment_id },
|
||||
foreign_key: { to_table: :protected_environments, on_delete: :cascade },
|
||||
null: false
|
||||
|
||||
t.bigint :user_id
|
||||
t.bigint :group_id
|
||||
t.timestamps_with_timezone null: false
|
||||
t.integer :access_level, limit: 2
|
||||
t.integer :required_approvals, null: false, limit: 2
|
||||
|
||||
t.index :user_id
|
||||
t.index :group_id
|
||||
|
||||
t.check_constraint "((access_level IS NOT NULL) AND (group_id IS NULL) AND (user_id IS NULL)) OR " \
|
||||
"((user_id IS NOT NULL) AND (access_level IS NULL) AND (group_id IS NULL)) OR " \
|
||||
"((group_id IS NOT NULL) AND (user_id IS NULL) AND (access_level IS NULL))"
|
||||
t.check_constraint "required_approvals > 0"
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :protected_environment_approval_rules
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUserFkToProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :protected_environment_approval_rules, :users, column: :user_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :protected_environment_approval_rules, column: :user_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddGroupFkToProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :protected_environment_approval_rules, :namespaces, column: :group_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :protected_environment_approval_rules, column: :group_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddApprovalRuleIdToDeploymentApprovals < Gitlab::Database::Migration[1.0]
|
||||
def change
|
||||
add_column :deployment_approvals, :approval_rule_id, :bigint
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddApprovalRuleFkToDeploymentApprovals < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_deployment_approvals_on_approval_rule_id'
|
||||
|
||||
def up
|
||||
add_concurrent_index :deployment_approvals, :approval_rule_id, name: INDEX_NAME
|
||||
add_concurrent_foreign_key :deployment_approvals, :protected_environment_approval_rules, column: :approval_rule_id, on_delete: :nullify
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :deployment_approvals, column: :approval_rule_id
|
||||
end
|
||||
|
||||
remove_concurrent_index_by_name :deployment_approvals, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateOrganizationsNameIndexAddId < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
OLD_INDEX = 'index_customer_relations_organizations_on_unique_name_per_group'
|
||||
NEW_INDEX = 'index_organizations_on_unique_name_per_group'
|
||||
|
||||
def up
|
||||
add_concurrent_index :customer_relations_organizations, 'group_id, lower(name), id', name: NEW_INDEX, unique: true
|
||||
|
||||
remove_concurrent_index_by_name :customer_relations_organizations, OLD_INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :customer_relations_organizations, 'group_id, lower(name)', name: OLD_INDEX, unique: true
|
||||
|
||||
remove_concurrent_index_by_name :customer_relations_organizations, NEW_INDEX
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddContactsIndexOnGroupEmailAndId < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_customer_relations_contacts_on_unique_email_per_group'
|
||||
|
||||
def up
|
||||
add_concurrent_index :customer_relations_contacts, 'group_id, lower(email), id', name: INDEX_NAME, unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :customer_relations_contacts, INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20220314184009
Normal file
1
db/schema_migrations/20220314184009
Normal file
|
@ -0,0 +1 @@
|
|||
258c7a3409aea1c713c2ddd6679de586e7548ce4d7c0811db1d4903f2794c660
|
1
db/schema_migrations/20220314184109
Normal file
1
db/schema_migrations/20220314184109
Normal file
|
@ -0,0 +1 @@
|
|||
85be80bb8c929d017fedfe66c1f18e4a0dbd7dd8f3b683ded60213e621ec06f4
|
1
db/schema_migrations/20220314184209
Normal file
1
db/schema_migrations/20220314184209
Normal file
|
@ -0,0 +1 @@
|
|||
41e7a36164fe3b1b582149d9cfbefc6ee2ce804ac85207f918c064b0ef738b53
|
1
db/schema_migrations/20220314194009
Normal file
1
db/schema_migrations/20220314194009
Normal file
|
@ -0,0 +1 @@
|
|||
e58b89906cd09577c1a773768e4cf3f97b870744e4ee6a323e0276895dff0de5
|
1
db/schema_migrations/20220314204009
Normal file
1
db/schema_migrations/20220314204009
Normal file
|
@ -0,0 +1 @@
|
|||
e2fa0265f3c14c8e6f08a4ffc4b682d8805fa634bac66c578684faaee97541cf
|
1
db/schema_migrations/20220316112118
Normal file
1
db/schema_migrations/20220316112118
Normal file
|
@ -0,0 +1 @@
|
|||
659accb8efe0223028a74346ecf3aa5b649cda825ac7941bc932bc1d7e6f8d9a
|
1
db/schema_migrations/20220316112206
Normal file
1
db/schema_migrations/20220316112206
Normal file
|
@ -0,0 +1 @@
|
|||
d24c5a5414e44385a132e8f342cb67cc5a7c5fe4bfcc4c15c473397076350bc2
|
|
@ -14270,6 +14270,7 @@ CREATE TABLE deployment_approvals (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
status smallint NOT NULL,
|
||||
comment text,
|
||||
approval_rule_id bigint,
|
||||
CONSTRAINT check_e2eb6a17d8 CHECK ((char_length(comment) <= 255))
|
||||
);
|
||||
|
||||
|
@ -19681,6 +19682,28 @@ CREATE SEQUENCE protected_branches_id_seq
|
|||
|
||||
ALTER SEQUENCE protected_branches_id_seq OWNED BY protected_branches.id;
|
||||
|
||||
CREATE TABLE protected_environment_approval_rules (
|
||||
id bigint NOT NULL,
|
||||
protected_environment_id bigint NOT NULL,
|
||||
user_id bigint,
|
||||
group_id bigint,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
access_level smallint,
|
||||
required_approvals smallint NOT NULL,
|
||||
CONSTRAINT chk_rails_bed75249bc CHECK ((((access_level IS NOT NULL) AND (group_id IS NULL) AND (user_id IS NULL)) OR ((user_id IS NOT NULL) AND (access_level IS NULL) AND (group_id IS NULL)) OR ((group_id IS NOT NULL) AND (user_id IS NULL) AND (access_level IS NULL)))),
|
||||
CONSTRAINT chk_rails_cfa90ae3b5 CHECK ((required_approvals > 0))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE protected_environment_approval_rules_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE protected_environment_approval_rules_id_seq OWNED BY protected_environment_approval_rules.id;
|
||||
|
||||
CREATE TABLE protected_environment_deploy_access_levels (
|
||||
id integer NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
@ -22950,6 +22973,8 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels ALTER COLUMN id SET DE
|
|||
|
||||
ALTER TABLE ONLY protected_branches ALTER COLUMN id SET DEFAULT nextval('protected_branches_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY protected_environment_approval_rules ALTER COLUMN id SET DEFAULT nextval('protected_environment_approval_rules_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY protected_environment_deploy_access_levels ALTER COLUMN id SET DEFAULT nextval('protected_environment_deploy_access_levels_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY protected_environments ALTER COLUMN id SET DEFAULT nextval('protected_environments_id_seq'::regclass);
|
||||
|
@ -25066,6 +25091,9 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels
|
|||
ALTER TABLE ONLY protected_branches
|
||||
ADD CONSTRAINT protected_branches_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY protected_environment_approval_rules
|
||||
ADD CONSTRAINT protected_environment_approval_rules_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY protected_environment_deploy_access_levels
|
||||
ADD CONSTRAINT protected_environment_deploy_access_levels_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -26707,6 +26735,8 @@ CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON approv
|
|||
|
||||
CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2);
|
||||
|
||||
CREATE INDEX index_approval_rule_on_protected_environment_id ON protected_environment_approval_rules USING btree (protected_environment_id);
|
||||
|
||||
CREATE INDEX index_approval_rules_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id) WHERE (rule_type = 2);
|
||||
|
||||
CREATE INDEX index_approvals_on_merge_request_id ON approvals USING btree (merge_request_id);
|
||||
|
@ -27273,7 +27303,7 @@ CREATE INDEX index_customer_relations_contacts_on_group_id ON customer_relations
|
|||
|
||||
CREATE INDEX index_customer_relations_contacts_on_organization_id ON customer_relations_contacts USING btree (organization_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_customer_relations_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name));
|
||||
CREATE UNIQUE INDEX index_customer_relations_contacts_on_unique_email_per_group ON customer_relations_contacts USING btree (group_id, lower(email), id);
|
||||
|
||||
CREATE UNIQUE INDEX index_cycle_analytics_stage_event_hashes_on_hash_sha_256 ON analytics_cycle_analytics_stage_event_hashes USING btree (hash_sha256);
|
||||
|
||||
|
@ -27343,6 +27373,8 @@ CREATE INDEX index_deploy_tokens_on_token_and_expires_at_and_id ON deploy_tokens
|
|||
|
||||
CREATE UNIQUE INDEX index_deploy_tokens_on_token_encrypted ON deploy_tokens USING btree (token_encrypted);
|
||||
|
||||
CREATE INDEX index_deployment_approvals_on_approval_rule_id ON deployment_approvals USING btree (approval_rule_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_deployment_approvals_on_deployment_id_and_user_id ON deployment_approvals USING btree (deployment_id, user_id);
|
||||
|
||||
CREATE INDEX index_deployment_approvals_on_user_id ON deployment_approvals USING btree (user_id);
|
||||
|
@ -28393,6 +28425,8 @@ CREATE UNIQUE INDEX index_ops_feature_flags_issues_on_feature_flag_id_and_issue_
|
|||
|
||||
CREATE UNIQUE INDEX index_ops_strategies_user_lists_on_strategy_id_and_user_list_id ON operations_strategies_user_lists USING btree (strategy_id, user_list_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_organizations_on_unique_name_per_group ON customer_relations_organizations USING btree (group_id, lower(name), id);
|
||||
|
||||
CREATE INDEX index_packages_build_infos_on_pipeline_id ON packages_build_infos USING btree (pipeline_id);
|
||||
|
||||
CREATE INDEX index_packages_build_infos_package_id_pipeline_id ON packages_build_infos USING btree (package_id, pipeline_id);
|
||||
|
@ -28785,6 +28819,10 @@ CREATE INDEX index_protected_branch_unprotect_access_levels_on_user_id ON protec
|
|||
|
||||
CREATE INDEX index_protected_branches_on_project_id ON protected_branches USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_protected_environment_approval_rules_on_group_id ON protected_environment_approval_rules USING btree (group_id);
|
||||
|
||||
CREATE INDEX index_protected_environment_approval_rules_on_user_id ON protected_environment_approval_rules USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_protected_environment_deploy_access ON protected_environment_deploy_access_levels USING btree (protected_environment_id);
|
||||
|
||||
CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON protected_environment_deploy_access_levels USING btree (group_id);
|
||||
|
@ -31170,6 +31208,9 @@ ALTER TABLE ONLY ci_pipelines
|
|||
ALTER TABLE ONLY merge_request_reviewers
|
||||
ADD CONSTRAINT fk_3d674b9f23 FOREIGN KEY (updated_state_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY protected_environment_approval_rules
|
||||
ADD CONSTRAINT fk_405568b491 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_pipeline_schedule_variables
|
||||
ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -31233,6 +31274,9 @@ ALTER TABLE ONLY project_access_tokens
|
|||
ALTER TABLE ONLY merge_requests
|
||||
ADD CONSTRAINT fk_6149611a04 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY deployment_approvals
|
||||
ADD CONSTRAINT fk_61cdbdc5b9 FOREIGN KEY (approval_rule_id) REFERENCES protected_environment_approval_rules(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY dast_profile_schedules
|
||||
ADD CONSTRAINT fk_61d52aa0e7 FOREIGN KEY (dast_profile_id) REFERENCES dast_profiles(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -31272,6 +31316,9 @@ ALTER TABLE ONLY projects
|
|||
ALTER TABLE ONLY terraform_state_versions
|
||||
ADD CONSTRAINT fk_6e81384d7f FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY protected_environment_approval_rules
|
||||
ADD CONSTRAINT fk_6ee8249821 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY protected_branch_push_access_levels
|
||||
ADD CONSTRAINT fk_7111b68cdb FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -32283,6 +32330,9 @@ ALTER TABLE ONLY scim_identities
|
|||
ALTER TABLE ONLY snippet_user_mentions
|
||||
ADD CONSTRAINT fk_rails_4d3f96b2cb FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY protected_environment_approval_rules
|
||||
ADD CONSTRAINT fk_rails_4e554f96f5 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY deployment_clusters
|
||||
ADD CONSTRAINT fk_rails_4e6243e120 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
> - Support for reStructuredText and Textile documents [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/324766) in GitLab 13.12.
|
||||
|
||||
When [Kroki](https://kroki.io) integration is enabled and configured in
|
||||
GitLab you can use it to create diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents.
|
||||
GitLab, you can use it to create diagrams in AsciiDoc, Markdown, reStructuredText, and Textile documents.
|
||||
|
||||
## Kroki Server
|
||||
|
||||
|
|
|
@ -88,3 +88,120 @@ Example response:
|
|||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Create a related epic link
|
||||
|
||||
Create a two-way relation between two epics. The user must be allowed to
|
||||
update both epics to succeed.
|
||||
|
||||
```plaintext
|
||||
POST /groups/:id/epics/:epic_iid/related_epics
|
||||
```
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|---------------------|----------------|-----------------------------|---------------------------------------|
|
||||
| `epic_iid` | integer | **{check-circle}** Yes | Internal ID of a group's epic. |
|
||||
| `id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user. |
|
||||
| `target_epic_iid` | integer/string | **{check-circle}** Yes | Internal ID of a target group's epic. |
|
||||
| `target_group_id` | integer/string | **{check-circle}** Yes | ID or [URL-encoded path of the target group](index.md#namespaced-path-encoding). |
|
||||
| `link_type` | string | **{dotted-circle}** No | Type of the relation (`relates_to`, `blocks`, `is_blocked_by`), defaults to `relates_to`. |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/26/epics/1/related_epics?target_group_id=26&target_epic_iid=5"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"source_epic": {
|
||||
"id": 21,
|
||||
"iid": 1,
|
||||
"color": "#1068bf",
|
||||
"text_color": "#FFFFFF",
|
||||
"group_id": 26,
|
||||
"parent_id": null,
|
||||
"parent_iid": null,
|
||||
"title": "Aspernatur recusandae distinctio omnis et qui est iste.",
|
||||
"description": "some description",
|
||||
"confidential": false,
|
||||
"author": {
|
||||
"id": 15,
|
||||
"username": "trina",
|
||||
"name": "Theresia Robel",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/085e28df717e16484cbf6ceca75e9a93?s=80&d=identicon",
|
||||
"web_url": "http://gitlab.example.com/trina"
|
||||
},
|
||||
"start_date": null,
|
||||
"end_date": null,
|
||||
"due_date": null,
|
||||
"state": "opened",
|
||||
"web_url": "http://gitlab.example.com/groups/flightjs/-/epics/1",
|
||||
"references": {
|
||||
"short": "&1",
|
||||
"relative": "&1",
|
||||
"full": "flightjs&1"
|
||||
},
|
||||
"created_at": "2022-01-31T15:10:44.988Z",
|
||||
"updated_at": "2022-03-16T09:32:35.712Z",
|
||||
"closed_at": null,
|
||||
"labels": [],
|
||||
"upvotes": 0,
|
||||
"downvotes": 0,
|
||||
"_links": {
|
||||
"self": "http://gitlab.example.com/api/v4/groups/26/epics/1",
|
||||
"epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/1/issues",
|
||||
"group": "http://gitlab.example.com/api/v4/groups/26",
|
||||
"parent": null
|
||||
}
|
||||
},
|
||||
"target_epic": {
|
||||
"id": 25,
|
||||
"iid": 5,
|
||||
"color": "#1068bf",
|
||||
"text_color": "#FFFFFF",
|
||||
"group_id": 26,
|
||||
"parent_id": null,
|
||||
"parent_iid": null,
|
||||
"title": "Aut assumenda id nihil distinctio fugiat vel numquam est.",
|
||||
"description": "some description",
|
||||
"confidential": false,
|
||||
"author": {
|
||||
"id": 3,
|
||||
"username": "valerie",
|
||||
"name": "Erika Wolf",
|
||||
"state": "active",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/9ef7666abb101418a4716a8ed4dded80?s=80&d=identicon",
|
||||
"web_url": "http://gitlab.example.com/valerie"
|
||||
},
|
||||
"start_date": null,
|
||||
"end_date": null,
|
||||
"due_date": null,
|
||||
"state": "opened",
|
||||
"web_url": "http://gitlab.example.com/groups/flightjs/-/epics/5",
|
||||
"references": {
|
||||
"short": "&5",
|
||||
"relative": "&5",
|
||||
"full": "flightjs&5"
|
||||
},
|
||||
"created_at": "2022-01-31T15:10:45.080Z",
|
||||
"updated_at": "2022-03-16T09:32:35.842Z",
|
||||
"closed_at": null,
|
||||
"labels": [],
|
||||
"upvotes": 0,
|
||||
"downvotes": 0,
|
||||
"_links": {
|
||||
"self": "http://gitlab.example.com/api/v4/groups/26/epics/5",
|
||||
"epic_issues": "http://gitlab.example.com/api/v4/groups/26/epics/5/issues",
|
||||
"group": "http://gitlab.example.com/api/v4/groups/26",
|
||||
"parent": null
|
||||
}
|
||||
},
|
||||
"link_type": "relates_to"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -99,6 +99,8 @@ GET /users?exclude_external=true
|
|||
|
||||
### For admins
|
||||
|
||||
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
|
||||
|
||||
```plaintext
|
||||
GET /users
|
||||
```
|
||||
|
@ -151,7 +153,8 @@ GET /users
|
|||
"external": false,
|
||||
"private_profile": false,
|
||||
"current_sign_in_ip": "196.165.1.102",
|
||||
"last_sign_in_ip": "172.127.2.22"
|
||||
"last_sign_in_ip": "172.127.2.22",
|
||||
"namespace_id": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
@ -185,7 +188,8 @@ GET /users
|
|||
"external": false,
|
||||
"private_profile": false,
|
||||
"current_sign_in_ip": "10.165.1.102",
|
||||
"last_sign_in_ip": "172.127.2.22"
|
||||
"last_sign_in_ip": "172.127.2.22",
|
||||
"namespace_id": 2
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -300,6 +304,8 @@ Parameters:
|
|||
|
||||
### For admin
|
||||
|
||||
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
|
||||
|
||||
```plaintext
|
||||
GET /users/:id
|
||||
```
|
||||
|
@ -355,7 +361,8 @@ Example Responses:
|
|||
"last_sign_in_ip": "172.127.2.22",
|
||||
"plan": "gold",
|
||||
"trial": true,
|
||||
"sign_in_count": 1337
|
||||
"sign_in_count": 1337,
|
||||
"namespace_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -404,6 +411,8 @@ GET /users/:id?with_custom_attributes=true
|
|||
|
||||
## User creation
|
||||
|
||||
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
|
||||
|
||||
Creates a new user. Note only administrators can create new
|
||||
users. Either `password`, `reset_password`, or `force_random_password`
|
||||
must be specified. If `reset_password` and `force_random_password` are
|
||||
|
@ -459,6 +468,8 @@ Parameters:
|
|||
|
||||
## User modification
|
||||
|
||||
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
|
||||
|
||||
Modifies an existing user. Only administrators can change attributes of a user.
|
||||
|
||||
```plaintext
|
||||
|
@ -583,6 +594,8 @@ GET /user
|
|||
|
||||
## List current user (for admins)
|
||||
|
||||
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
|
||||
|
||||
```plaintext
|
||||
GET /user
|
||||
```
|
||||
|
@ -632,7 +645,8 @@ Parameters:
|
|||
"private_profile": false,
|
||||
"commit_email": "john-codes@example.com",
|
||||
"current_sign_in_ip": "196.165.1.102",
|
||||
"last_sign_in_ip": "172.127.2.22"
|
||||
"last_sign_in_ip": "172.127.2.22",
|
||||
"namespace_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -598,6 +598,9 @@ To upload payload manually:
|
|||
1. Select **Choose file** and choose the file from p5.
|
||||
1. Select **Upload**.
|
||||
|
||||
The uploaded file is encrypted and sent using secure [HTTPS protocol](https://en.wikipedia.org/wiki/HTTPS). HTTPS creates a secure
|
||||
communication channel between web browser and the server, and protects transmitted data against man-in-the-middle attacks.
|
||||
|
||||
## Monitoring
|
||||
|
||||
Service Ping reporting process state is monitored with [internal SiSense dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health).
|
||||
|
|
|
@ -5,6 +5,7 @@ module API
|
|||
class UserWithAdmin < UserPublic
|
||||
expose :admin?, as: :is_admin
|
||||
expose :note
|
||||
expose :namespace_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -120,8 +120,11 @@ module API
|
|||
users = reorder_users(users)
|
||||
|
||||
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
|
||||
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
|
||||
users = users.preload(:identities, :webauthn_registrations) if entity == Entities::UserWithAdmin
|
||||
|
||||
if entity == Entities::UserWithAdmin
|
||||
users = users.preload(:identities, :u2f_registrations, :webauthn_registrations, :namespace)
|
||||
end
|
||||
|
||||
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
|
||||
|
||||
users = users.preload(:user_detail)
|
||||
|
|
|
@ -435,6 +435,7 @@ protected_branches: :gitlab_main
|
|||
protected_branch_merge_access_levels: :gitlab_main
|
||||
protected_branch_push_access_levels: :gitlab_main
|
||||
protected_branch_unprotect_access_levels: :gitlab_main
|
||||
protected_environment_approval_rules: :gitlab_main
|
||||
protected_environment_deploy_access_levels: :gitlab_main
|
||||
protected_environments: :gitlab_main
|
||||
protected_tag_create_access_levels: :gitlab_main
|
||||
|
|
|
@ -92,7 +92,7 @@ module Gitlab
|
|||
|
||||
def simple_serialize
|
||||
subject.as_json(
|
||||
tree.merge(include: nil, preloads: nil))
|
||||
tree.merge(include: nil, preloads: nil, unsafe: true))
|
||||
end
|
||||
|
||||
def serialize_includes
|
||||
|
|
|
@ -37,7 +37,7 @@ module Gitlab
|
|||
|
||||
def serialize_root(exportable_path = @exportable_path)
|
||||
attributes = exportable.as_json(
|
||||
relations_schema.merge(include: nil, preloads: nil))
|
||||
relations_schema.merge(include: nil, preloads: nil, unsafe: true))
|
||||
|
||||
json_writer.write_attributes(exportable_path, attributes)
|
||||
end
|
||||
|
|
|
@ -27,6 +27,11 @@ module Mattermost
|
|||
|
||||
LEASE_TIMEOUT = 60
|
||||
|
||||
Request = Struct.new(:parameters, keyword_init: true) do
|
||||
def method_missing(method_name, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :current_resource_owner, :token, :base_uri
|
||||
|
||||
def initialize(current_user)
|
||||
|
@ -64,7 +69,7 @@ module Mattermost
|
|||
end
|
||||
|
||||
def request
|
||||
@request ||= OpenStruct.new(parameters: params)
|
||||
@request ||= Request.new(parameters: params)
|
||||
end
|
||||
|
||||
def params
|
||||
|
|
|
@ -9424,9 +9424,6 @@ msgstr ""
|
|||
msgid "Confirm new password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm remove avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm user"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22703,6 +22700,9 @@ msgstr ""
|
|||
msgid "Manual"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manual iteration cadences are deprecated. Only automatic iteration cadences are allowed."
|
||||
msgstr ""
|
||||
|
||||
msgid "ManualOrdering|Couldn't save the order of the issues"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30947,6 +30947,9 @@ msgstr ""
|
|||
msgid "Remove time estimate"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove topic avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove user"
|
||||
msgstr ""
|
||||
|
||||
|
@ -39156,6 +39159,9 @@ msgstr ""
|
|||
msgid "Topic avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic avatar for %{name} will be removed. This cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -39249,6 +39255,9 @@ msgstr ""
|
|||
msgid "TransferGroup|Database is not supported."
|
||||
msgstr ""
|
||||
|
||||
msgid "TransferGroup|Group contains contacts/organizations and you don't have enough permissions to move them to the new root group."
|
||||
msgstr ""
|
||||
|
||||
msgid "TransferGroup|Group contains projects with NPM packages."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -56,11 +56,8 @@ module DeprecationToolkitEnv
|
|||
# In this case, we recommend to add a silence together with an issue to patch or update
|
||||
# the dependency causing the problem.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
|
||||
#
|
||||
# - ruby/lib/grpc/generic/interceptors.rb: https://gitlab.com/gitlab-org/gitlab/-/issues/339305
|
||||
def self.allowed_kwarg_warning_paths
|
||||
%w[
|
||||
ruby/lib/grpc/generic/interceptors.rb
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
"can_create_group",
|
||||
"can_create_project",
|
||||
"two_factor_enabled",
|
||||
"external"
|
||||
"external",
|
||||
"namespace_id"
|
||||
],
|
||||
"properties": {
|
||||
"$ref": "full.json"
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { GlButton, GlModal } from '@gitlab/ui';
|
||||
import { GlButton, GlModal, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import RemoveAvatar from '~/admin/topics/components/remove_avatar.vue';
|
||||
|
||||
const modalID = 'fake-id';
|
||||
const path = 'topic/path/1';
|
||||
const name = 'Topic 1';
|
||||
|
||||
jest.mock('lodash/uniqueId', () => () => 'fake-id');
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
@ -16,10 +17,14 @@ describe('RemoveAvatar', () => {
|
|||
wrapper = shallowMount(RemoveAvatar, {
|
||||
provide: {
|
||||
path,
|
||||
name,
|
||||
},
|
||||
directives: {
|
||||
GlModal: createMockDirective(),
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -55,8 +60,8 @@ describe('RemoveAvatar', () => {
|
|||
const modal = findModal();
|
||||
|
||||
expect(modal.exists()).toBe(true);
|
||||
expect(modal.props('title')).toBe('Confirm remove avatar');
|
||||
expect(modal.text()).toBe('Avatar will be removed. Are you sure?');
|
||||
expect(modal.props('title')).toBe('Remove topic avatar');
|
||||
expect(modal.text()).toBe(`Topic avatar for ${name} will be removed. This cannot be undone.`);
|
||||
});
|
||||
|
||||
it('contains the correct modal ID', () => {
|
||||
|
|
|
@ -1,20 +1,46 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
import IssuableForm from '~/issuable/issuable_form';
|
||||
|
||||
function createIssuable() {
|
||||
const instance = new IssuableForm($(document.createElement('form')));
|
||||
|
||||
instance.titleField = $(document.createElement('input'));
|
||||
|
||||
return instance;
|
||||
}
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
||||
describe('IssuableForm', () => {
|
||||
let instance;
|
||||
|
||||
const createIssuable = (form) => {
|
||||
instance = new IssuableForm(form);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
instance = createIssuable();
|
||||
setFixtures(`
|
||||
<form>
|
||||
<input name="[title]" />
|
||||
</form>
|
||||
`);
|
||||
createIssuable($('form'));
|
||||
});
|
||||
|
||||
describe('initAutosave', () => {
|
||||
it('creates autosave with the searchTerm included', () => {
|
||||
setWindowLocation('https://gitlab.test/foo?bar=true');
|
||||
const autosave = instance.initAutosave();
|
||||
|
||||
expect(autosave.key.includes('bar=true')).toBe(true);
|
||||
});
|
||||
|
||||
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
|
||||
setFixtures(`
|
||||
<form data-new-issue-path="/issues/new">
|
||||
<input name="[title]" />
|
||||
</form>
|
||||
`);
|
||||
createIssuable($('form'));
|
||||
|
||||
setWindowLocation('https://gitlab.test/issues/new?bar=true');
|
||||
|
||||
const autosave = instance.initAutosave();
|
||||
|
||||
expect(autosave.key.includes('bar=true')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeWip', () => {
|
||||
|
|
|
@ -368,6 +368,16 @@ RSpec.describe IssuesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#issues_form_data' do
|
||||
it 'returns expected result' do
|
||||
expected = {
|
||||
new_issue_path: new_project_issue_path(project)
|
||||
}
|
||||
|
||||
expect(helper.issues_form_data(project)).to include(expected)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_manual_ordering_class' do
|
||||
context 'when sorting by relative position' do
|
||||
before do
|
||||
|
|
|
@ -665,6 +665,7 @@ protected_environments:
|
|||
- project
|
||||
- group
|
||||
- deploy_access_levels
|
||||
- approval_rules
|
||||
deploy_access_levels:
|
||||
- protected_environment
|
||||
- user
|
||||
|
|
|
@ -35,6 +35,12 @@ RSpec.describe Mattermost::Session, type: :request do
|
|||
it 'makes a request to the oauth uri' do
|
||||
expect { subject.with_session }.to raise_error(::Mattermost::NoSessionError)
|
||||
end
|
||||
|
||||
it 'returns nill on calling a non exisitng method on request' do
|
||||
return_value = subject.request.method_missing("non_existing_method", "something") do
|
||||
end
|
||||
expect(return_value).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with oauth_uri' do
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
it { is_expected.to validate_length_of(:email).is_at_most(255) }
|
||||
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
|
||||
|
||||
it { is_expected.to validate_uniqueness_of(:email).scoped_to(:group_id) }
|
||||
it { is_expected.to validate_uniqueness_of(:email).case_insensitive.scoped_to(:group_id) }
|
||||
|
||||
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
|
||||
end
|
||||
|
@ -87,6 +87,15 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
too_many_emails = described_class::MAX_PLUCK + 1
|
||||
expect { described_class.find_ids_by_emails(group, Array(0..too_many_emails)) }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'finds contacts regardless of email casing' do
|
||||
new_contact = create(:contact, group: group, email: "UPPERCASE@example.com")
|
||||
emails = [group_contacts[0].email.downcase, group_contacts[1].email.upcase, new_contact.email]
|
||||
|
||||
contact_ids = described_class.find_ids_by_emails(group, emails)
|
||||
|
||||
expect(contact_ids).to contain_exactly(group_contacts[0].id, group_contacts[1].id, new_contact.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self.exists_for_group?' do
|
||||
|
@ -104,4 +113,33 @@ RSpec.describe CustomerRelations::Contact, type: :model do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self.move_to_root_group' do
|
||||
let!(:old_root_group) { create(:group) }
|
||||
let!(:contacts) { create_list(:contact, 4, group: old_root_group) }
|
||||
let!(:project) { create(:project, group: old_root_group) }
|
||||
let!(:issue) { create(:issue, project: project) }
|
||||
let!(:issue_contact1) { create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]) }
|
||||
let!(:issue_contact2) { create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]) }
|
||||
let!(:new_root_group) { create(:group) }
|
||||
let!(:dupe_contact1) { create(:contact, group: new_root_group, email: contacts[1].email) }
|
||||
let!(:dupe_contact2) { create(:contact, group: new_root_group, email: contacts[3].email.upcase) }
|
||||
|
||||
before do
|
||||
old_root_group.update!(parent: new_root_group)
|
||||
CustomerRelations::Contact.move_to_root_group(old_root_group)
|
||||
end
|
||||
|
||||
it 'moves contacts with unique emails and deletes the rest' do
|
||||
expect(contacts[0].reload.group_id).to eq(new_root_group.id)
|
||||
expect(contacts[2].reload.group_id).to eq(new_root_group.id)
|
||||
expect { contacts[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { contacts[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'updates issue_contact.contact_id for dupes and leaves the rest untouched' do
|
||||
expect(issue_contact1.reload.contact_id).to eq(contacts[0].id)
|
||||
expect(issue_contact2.reload.contact_id).to eq(dupe_contact1.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -92,4 +92,16 @@ RSpec.describe CustomerRelations::IssueContact do
|
|||
expect { described_class.delete_for_project(project.id) }.to change { described_class.count }.by(-3)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.delete_for_group' do
|
||||
let(:project_for_root_group) { create(:project, group: group) }
|
||||
|
||||
it 'destroys all issue_contacts for projects in group and subgroups' do
|
||||
create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project))
|
||||
create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project_for_root_group))
|
||||
create(:issue_customer_relations_contact)
|
||||
|
||||
expect { described_class.delete_for_group(group) }.to change { described_class.count }.by(-4)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -50,4 +50,32 @@ RSpec.describe CustomerRelations::Organization, type: :model do
|
|||
expect(described_class.find_by_name(group.id, 'TEST')).to eq([organiztion1])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self.move_to_root_group' do
|
||||
let!(:old_root_group) { create(:group) }
|
||||
let!(:organizations) { create_list(:organization, 4, group: old_root_group) }
|
||||
let!(:new_root_group) { create(:group) }
|
||||
let!(:contact1) { create(:contact, group: new_root_group, organization: organizations[0]) }
|
||||
let!(:contact2) { create(:contact, group: new_root_group, organization: organizations[1]) }
|
||||
|
||||
let!(:dupe_organization1) { create(:organization, group: new_root_group, name: organizations[1].name) }
|
||||
let!(:dupe_organization2) { create(:organization, group: new_root_group, name: organizations[3].name.upcase) }
|
||||
|
||||
before do
|
||||
old_root_group.update!(parent: new_root_group)
|
||||
CustomerRelations::Organization.move_to_root_group(old_root_group)
|
||||
end
|
||||
|
||||
it 'moves organizations with unique names and deletes the rest' do
|
||||
expect(organizations[0].reload.group_id).to eq(new_root_group.id)
|
||||
expect(organizations[2].reload.group_id).to eq(new_root_group.id)
|
||||
expect { organizations[1].reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { organizations[3].reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it 'updates contact.organization_id for dupes and leaves the rest untouched' do
|
||||
expect(contact1.reload.organization_id).to eq(organizations[0].id)
|
||||
expect(contact2.reload.organization_id).to eq(dupe_organization1.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1021,4 +1021,54 @@ RSpec.describe Integration do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'boolean_accessor' do
|
||||
let(:klass) do
|
||||
Class.new(Integration) do
|
||||
boolean_accessor :test_value
|
||||
end
|
||||
end
|
||||
|
||||
let(:integration) { klass.new(properties: { test_value: input }) }
|
||||
|
||||
where(:input, :method_result, :predicate_method_result) do
|
||||
true | true | true
|
||||
false | false | false
|
||||
1 | true | true
|
||||
0 | false | false
|
||||
'1' | true | true
|
||||
'0' | false | false
|
||||
'true' | true | true
|
||||
'false' | false | false
|
||||
'foobar' | nil | false
|
||||
'' | nil | false
|
||||
nil | nil | false
|
||||
'on' | true | true
|
||||
'off' | false | false
|
||||
'yes' | true | true
|
||||
'no' | false | false
|
||||
'n' | false | false
|
||||
'y' | true | true
|
||||
't' | true | true
|
||||
'f' | false | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'has the correct value' do
|
||||
expect(integration).to have_attributes(
|
||||
test_value: be(method_result),
|
||||
test_value?: be(predicate_method_result)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns values when initialized without input' do
|
||||
integration = klass.new
|
||||
|
||||
expect(integration).to have_attributes(
|
||||
test_value: be(nil),
|
||||
test_value?: be(false)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2230,4 +2230,18 @@ RSpec.describe Namespace do
|
|||
expect(namespace.storage_enforcement_date).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'serialization' do
|
||||
let(:object) { build(:namespace) }
|
||||
|
||||
it_behaves_like 'blocks unsafe serialization'
|
||||
|
||||
context 'when feature flag block_namespace_serialization is disabled' do
|
||||
before do
|
||||
stub_feature_flags(block_namespace_serialization: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'allows unsafe serialization'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,19 +83,21 @@ RSpec.describe API::Users do
|
|||
|
||||
describe 'GET /users/' do
|
||||
context 'when unauthenticated' do
|
||||
it "does not contain the note of users" do
|
||||
it "does not contain certain fields" do
|
||||
get api("/users"), params: { username: user.username }
|
||||
|
||||
expect(json_response.first).not_to have_key('note')
|
||||
expect(json_response.first).not_to have_key('namespace_id')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
context 'as a regular user' do
|
||||
it 'does not contain the note of users' do
|
||||
it 'does not contain certain fields' do
|
||||
get api("/users", user), params: { username: user.username }
|
||||
|
||||
expect(json_response.first).not_to have_key('note')
|
||||
expect(json_response.first).not_to have_key('namespace_id')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -154,6 +156,7 @@ RSpec.describe API::Users do
|
|||
get api("/user", user)
|
||||
|
||||
expect(json_response).not_to have_key('note')
|
||||
expect(json_response).not_to have_key('namespace_id')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -384,6 +387,15 @@ RSpec.describe API::Users do
|
|||
expect(response).to include_pagination_headers
|
||||
end
|
||||
|
||||
it "users contain the `namespace_id` field" do
|
||||
get api("/users", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(response).to match_response_schema('public_api/v4/user/admins')
|
||||
expect(json_response.size).to eq(2)
|
||||
expect(json_response.map { |u| u['namespace_id'] }).to include(user.namespace_id, admin.namespace_id)
|
||||
end
|
||||
|
||||
it "returns an array of external users" do
|
||||
create(:user, external: true)
|
||||
|
||||
|
@ -697,6 +709,14 @@ RSpec.describe API::Users do
|
|||
expect(json_response['highest_role']).to be(0)
|
||||
end
|
||||
|
||||
it 'includes the `namespace_id` field' do
|
||||
get api("/users/#{user.id}", admin)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(response).to match_response_schema('public_api/v4/user/admin')
|
||||
expect(json_response['namespace_id']).to eq(user.namespace_id)
|
||||
end
|
||||
|
||||
if Gitlab.ee?
|
||||
it 'does not include values for plan or trial' do
|
||||
get api("/users/#{user.id}", admin)
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
|||
end
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:new_parent_group) { create(:group, :public) }
|
||||
let_it_be(:new_parent_group) { create(:group, :public, :crm_enabled) }
|
||||
|
||||
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
|
||||
let(:transfer_service) { described_class.new(group, user) }
|
||||
|
@ -876,5 +876,108 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'crm' do
|
||||
let(:root_group) { create(:group, :public) }
|
||||
let(:subgroup) { create(:group, :public, parent: root_group) }
|
||||
let(:another_subgroup) { create(:group, :public, parent: root_group) }
|
||||
let(:subsubgroup) { create(:group, :public, parent: subgroup) }
|
||||
|
||||
let(:root_project) { create(:project, group: root_group) }
|
||||
let(:sub_project) { create(:project, group: subgroup) }
|
||||
let(:another_project) { create(:project, group: another_subgroup) }
|
||||
let(:subsub_project) { create(:project, group: subsubgroup) }
|
||||
|
||||
let!(:contacts) { create_list(:contact, 4, group: root_group) }
|
||||
let!(:organizations) { create_list(:organization, 2, group: root_group) }
|
||||
|
||||
before do
|
||||
create(:issue_customer_relations_contact, contact: contacts[0], issue: create(:issue, project: root_project))
|
||||
create(:issue_customer_relations_contact, contact: contacts[1], issue: create(:issue, project: sub_project))
|
||||
create(:issue_customer_relations_contact, contact: contacts[2], issue: create(:issue, project: another_project))
|
||||
create(:issue_customer_relations_contact, contact: contacts[3], issue: create(:issue, project: subsub_project))
|
||||
root_group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'moving up' do
|
||||
let(:group) { subsubgroup }
|
||||
|
||||
it 'retains issue contacts' do
|
||||
expect { transfer_service.execute(root_group) }
|
||||
.not_to change { CustomerRelations::IssueContact.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'moving down' do
|
||||
let(:group) { subgroup }
|
||||
|
||||
it 'retains issue contacts' do
|
||||
expect { transfer_service.execute(another_subgroup) }
|
||||
.not_to change { CustomerRelations::IssueContact.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'moving sideways' do
|
||||
let(:group) { subsubgroup }
|
||||
|
||||
it 'retains issue contacts' do
|
||||
expect { transfer_service.execute(another_subgroup) }
|
||||
.not_to change { CustomerRelations::IssueContact.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'moving to new root group' do
|
||||
let(:group) { root_group }
|
||||
|
||||
before do
|
||||
new_parent_group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'moves all crm objects' do
|
||||
expect { transfer_service.execute(new_parent_group) }
|
||||
.to change { root_group.contacts.count }.by(-4)
|
||||
.and change { root_group.organizations.count }.by(-2)
|
||||
end
|
||||
|
||||
it 'retains issue contacts' do
|
||||
expect { transfer_service.execute(new_parent_group) }
|
||||
.not_to change { CustomerRelations::IssueContact.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'moving to a subgroup within a new root group' do
|
||||
let(:group) { root_group }
|
||||
let(:subgroup_in_new_parent_group) { create(:group, parent: new_parent_group) }
|
||||
|
||||
context 'with permission on the root group' do
|
||||
before do
|
||||
new_parent_group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'moves all crm objects' do
|
||||
expect { transfer_service.execute(subgroup_in_new_parent_group) }
|
||||
.to change { root_group.contacts.count }.by(-4)
|
||||
.and change { root_group.organizations.count }.by(-2)
|
||||
end
|
||||
|
||||
it 'retains issue contacts' do
|
||||
expect { transfer_service.execute(subgroup_in_new_parent_group) }
|
||||
.not_to change { CustomerRelations::IssueContact.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with permission on the subgroup' do
|
||||
before do
|
||||
subgroup_in_new_parent_group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'raises error' do
|
||||
transfer_service.execute(subgroup_in_new_parent_group)
|
||||
|
||||
expect(transfer_service.error).to eq("Transfer failed: Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -70,8 +70,10 @@ shared_examples 'issuable link creation' do
|
|||
expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to')
|
||||
end
|
||||
|
||||
it 'returns success status' do
|
||||
is_expected.to eq(status: :success)
|
||||
it 'returns success status and created links', :aggregate_failures do
|
||||
expect(subject.keys).to match_array([:status, :created_references])
|
||||
expect(subject[:status]).to eq(:success)
|
||||
expect(subject[:created_references].map(&:target_id)).to match_array([issuable2.id, issuable3.id])
|
||||
end
|
||||
|
||||
it 'creates notes' do
|
||||
|
|
Loading…
Reference in a new issue