summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/integrations/constants.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue66
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue23
-rw-r--r--app/controllers/concerns/integrations/actions.rb3
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb3
-rw-r--r--app/helpers/integrations_helper.rb4
-rw-r--r--app/models/namespaces/user_namespace.rb4
-rw-r--r--app/models/project.rb11
-rw-r--r--app/services/boards/base_item_move_service.rb2
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/issuable_base_service.rb7
-rw-r--r--app/services/issues/reorder_service.rb36
-rw-r--r--app/services/issues/update_service.rb15
-rw-r--r--app/views/projects/services/_form.html.haml8
-rw-r--r--app/views/shared/integrations/_form.html.haml4
-rw-r--r--app/views/shared/integrations/edit.html.haml5
-rw-r--r--config/feature_flags/development/vue_integration_form.yml8
-rw-r--r--db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb7
-rw-r--r--db/schema_migrations/202201311357251
-rw-r--r--db/structure.sql3
-rw-r--r--doc/ci/troubleshooting.md15
-rw-r--r--doc/system_hooks/system_hooks.md42
-rw-r--r--lib/api/ci/job_artifacts.rb4
-rw-r--r--lib/api/ci/pipelines.rb2
-rw-r--r--lib/api/ci/runner.rb4
-rw-r--r--lib/gitlab/hook_data/project_builder.rb21
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb18
-rw-r--r--spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb15
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb6
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb5
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb8
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb6
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js359
-rw-r--r--spec/lib/gitlab/hook_data/project_builder_spec.rb3
-rw-r--r--spec/models/namespaces/user_namespace_spec.rb9
-rw-r--r--spec/requests/api/issues/issues_spec.rb20
-rw-r--r--spec/services/issues/reorder_service_spec.rb12
-rw-r--r--spec/services/issues/update_service_spec.rb10
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb13
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb52
45 files changed, 388 insertions, 498 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index e409227560d..37fe4887872 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-e0e146ec58d2515c2767f77d2860afc4bb7c1c73
+e023bdab6cea4ad7124d64b12e77f2cdb0681e21
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index b26f6bdd370..004601bc0a3 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -24,5 +24,3 @@ export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection s
export const settingsTabTitle = __('Settings');
export const overridesTabTitle = s__('Integrations|Projects using custom settings');
-
-export const INTEGRATION_FORM_SELECTOR = '.js-integration-settings-form';
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index dcdb38d94c8..007a384f41e 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -8,7 +8,6 @@ import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
- INTEGRATION_FORM_SELECTOR,
integrationLevels,
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
@@ -82,28 +81,9 @@ export default {
disableButtons() {
return Boolean(this.isSaving || this.isResetting || this.isTesting);
},
- useVueForm() {
- return this.glFeatures?.vueIntegrationForm;
+ form() {
+ return this.$refs.integrationForm.$el;
},
- formContainerProps() {
- return this.useVueForm
- ? {
- ref: 'integrationForm',
- method: 'post',
- class: 'gl-mb-3 gl-show-field-errors integration-settings-form',
- action: this.propsSource.formPath,
- novalidate: !this.integrationActive,
- }
- : {};
- },
- formContainer() {
- return this.useVueForm ? GlForm : 'div';
- },
- },
- mounted() {
- this.form = this.useVueForm
- ? this.$refs.integrationForm.$el
- : document.querySelector(INTEGRATION_FORM_SELECTOR);
},
methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']),
@@ -122,13 +102,13 @@ export default {
this.form.submit();
},
onTestClick() {
- this.isTesting = true;
-
if (!this.form.checkValidity()) {
this.setIsValidated();
return;
}
+ this.isTesting = true;
+
testIntegrationSettings(this.propsSource.testPath, this.getFormData())
.then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => {
if (error) {
@@ -171,16 +151,6 @@ export default {
},
onToggleIntegrationState(integrationActive) {
this.integrationActive = integrationActive;
- if (!this.form || this.useVueForm) {
- return;
- }
-
- // If integration will be active, enable form validation.
- if (integrationActive) {
- this.form.removeAttribute('novalidate');
- } else {
- this.form.setAttribute('novalidate', true);
- }
},
},
helpHtmlConfig: {
@@ -193,17 +163,21 @@ export default {
</script>
<template>
- <component :is="formContainer" v-bind="formContainerProps">
- <template v-if="useVueForm">
- <input type="hidden" name="_method" value="put" />
- <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
- <input
- type="hidden"
- name="redirect_to"
- :value="propsSource.redirectTo"
- data-testid="redirect-to-field"
- />
- </template>
+ <gl-form
+ ref="integrationForm"
+ method="post"
+ class="gl-mb-3 gl-show-field-errors integration-settings-form"
+ :action="propsSource.formPath"
+ :novalidate="!integrationActive"
+ >
+ <input type="hidden" name="_method" value="put" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <input
+ type="hidden"
+ name="redirect_to"
+ :value="propsSource.redirectTo"
+ data-testid="redirect-to-field"
+ />
<override-dropdown
v-if="defaultState !== null"
@@ -316,5 +290,5 @@ export default {
</div>
</div>
</div>
- </component>
+ </gl-form>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 3250a4818c7..7bad10616cc 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -3,6 +3,7 @@ import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import $ from 'jquery';
import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -243,14 +244,18 @@ export default {
this.setSelectedCommentPositionHover();
this.$emit('handleEdit');
},
- deleteHandler() {
+ async deleteHandler() {
const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment');
- if (
- // eslint-disable-next-line no-alert
- window.confirm(
- sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { typeOfComment }),
- )
- ) {
+
+ const msg = sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), {
+ typeOfComment,
+ });
+ const confirmed = await confirmAction(msg, {
+ primaryBtnVariant: 'danger',
+ primaryBtnText: __('Delete Comment'),
+ });
+
+ if (confirmed) {
this.isDeleting = true;
this.$emit('handleDeleteNote', this.note);
@@ -345,10 +350,11 @@ export default {
parent: this.$el,
});
},
- formCancelHandler({ shouldConfirm, isDirty }) {
+ async formCancelHandler({ shouldConfirm, isDirty }) {
if (shouldConfirm && isDirty) {
- // eslint-disable-next-line no-alert
- if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return;
+ const msg = __('Are you sure you want to cancel editing this comment?');
+ const confirmed = await confirmAction(msg);
+ if (!confirmed) return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 810d9f782b9..3d48c74b40b 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -23,9 +23,19 @@ export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title:
export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+export const DEFAULT_MILESTONE_UPCOMING = {
+ value: FILTER_UPCOMING,
+ text: __('Upcoming'),
+ title: __('Upcoming'),
+};
+export const DEFAULT_MILESTONE_STARTED = {
+ value: FILTER_STARTED,
+ text: __('Started'),
+ title: __('Started'),
+};
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
- { value: FILTER_UPCOMING, text: __('Upcoming'), title: __('Upcoming') },
- { value: FILTER_STARTED, text: __('Started'), title: __('Started') },
+ DEFAULT_MILESTONE_UPCOMING,
+ DEFAULT_MILESTONE_STARTED,
]);
export const SortDirection = {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index bbc1888bc0b..157068b2c0f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -163,19 +163,22 @@ export default {
},
},
methods: {
- handleInput: debounce(function debouncedSearch({ data }) {
- this.searchKey = data;
+ handleInput: debounce(function debouncedSearch({ data, operator }) {
+ // Prevent fetching suggestions when data or operator is not present
+ if (data || operator) {
+ this.searchKey = data;
- if (!this.suggestionsLoading && !this.activeTokenValue) {
- let search = this.searchTerm ? this.searchTerm : data;
+ if (!this.suggestionsLoading && !this.activeTokenValue) {
+ let search = this.searchTerm ? this.searchTerm : data;
- if (search.startsWith('"') && search.endsWith('"')) {
- search = stripQuotes(search);
- } else if (search.startsWith('"')) {
- search = search.slice(1, search.length);
- }
+ if (search.startsWith('"') && search.endsWith('"')) {
+ search = stripQuotes(search);
+ } else if (search.startsWith('"')) {
+ search = search.slice(1, search.length);
+ }
- this.$emit('fetch-suggestions', search);
+ this.$emit('fetch-suggestions', search);
+ }
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
diff --git a/app/controllers/concerns/integrations/actions.rb b/app/controllers/concerns/integrations/actions.rb
index f6e98c25b72..1f788860c8f 100644
--- a/app/controllers/concerns/integrations/actions.rb
+++ b/app/controllers/concerns/integrations/actions.rb
@@ -8,9 +8,6 @@ module Integrations::Actions
include IntegrationsHelper
before_action :integration, only: [:edit, :update, :overrides, :test]
- before_action do
- push_frontend_feature_flag(:vue_integration_form, current_user, default_enabled: :yaml)
- end
urgency :low, [:test]
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4fabbe6a2b7..0079c56558a 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -321,7 +321,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def reorder_params
- params.permit(:move_before_id, :move_after_id, :group_full_path)
+ params.permit(:move_before_id, :move_after_id)
end
def store_uri
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 9896f75c099..1321111faaf 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -12,9 +12,6 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :web_hook_logs, only: [:edit, :update]
before_action :set_deprecation_notice_for_prometheus_integration, only: [:edit, :update]
before_action :redirect_deprecated_prometheus_integration, only: [:update]
- before_action do
- push_frontend_feature_flag(:vue_integration_form, current_user, default_enabled: :yaml)
- end
respond_to :html
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index 230f80e20a5..f5ba978e860 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -228,10 +228,6 @@ module IntegrationsHelper
name: integration.to_param
}
end
-
- def vue_integration_form_enabled?
- Feature.enabled?(:vue_integration_form, current_user, default_enabled: :yaml)
- end
end
IntegrationsHelper.prepend_mod_with('IntegrationsHelper')
diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb
index 14b867b2607..408acb6dcce 100644
--- a/app/models/namespaces/user_namespace.rb
+++ b/app/models/namespaces/user_namespace.rb
@@ -25,5 +25,9 @@ module Namespaces
def self.sti_name
'User'
end
+
+ def owners
+ Array.wrap(owner)
+ end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 1b920ab57b0..abe072a3a6d 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1520,8 +1520,17 @@ class Project < ApplicationRecord
group || namespace.try(:owner)
end
+ def deprecated_owner
+ # Kept in order to maintain webhook structures until we remove owner_name and owner_email
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/350603
+ group || namespace.try(:owner)
+ end
+
def owners
- Array.wrap(owner)
+ # This will be phased out and replaced with `owners` relationship
+ # backed by memberships with direct/inherited Owner access roles
+ # See https://gitlab.com/groups/gitlab-org/-/epics/7405
+ team.owners
end
def first_owner
diff --git a/app/services/boards/base_item_move_service.rb b/app/services/boards/base_item_move_service.rb
index be16c595abb..8bae810e676 100644
--- a/app/services/boards/base_item_move_service.rb
+++ b/app/services/boards/base_item_move_service.rb
@@ -39,7 +39,7 @@ module Boards
end
def reposition_params(reposition_ids)
- reposition_parent.merge(move_between_ids: reposition_ids)
+ { move_between_ids: reposition_ids }
end
def move_single_issuable(issuable, issuable_modification_params)
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 959a7fa3ad2..90226b9d4e0 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -54,10 +54,6 @@ module Boards
def update(issue, issue_modification_params)
::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: issue_modification_params).execute(issue)
end
-
- def reposition_parent
- { board_group_id: board.group&.id }
- end
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 6a8eec85d0c..11c6cc360de 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -495,10 +495,11 @@ class IssuableBaseService < ::BaseProjectService
return unless params[:move_between_ids]
after_id, before_id = params.delete(:move_between_ids)
- positioning_scope_id = params.delete(positioning_scope_key)
- issuable_before = issuable_for_positioning(before_id, positioning_scope_id)
- issuable_after = issuable_for_positioning(after_id, positioning_scope_id)
+ positioning_scope = issuable_position.class.relative_positioning_query_base(issuable_position)
+
+ issuable_before = issuable_for_positioning(before_id, positioning_scope)
+ issuable_after = issuable_for_positioning(after_id, positioning_scope)
raise ActiveRecord::RecordNotFound unless issuable_before || issuable_after
diff --git a/app/services/issues/reorder_service.rb b/app/services/issues/reorder_service.rb
index 9c5fbec7d8e..8be4f530dac 100644
--- a/app/services/issues/reorder_service.rb
+++ b/app/services/issues/reorder_service.rb
@@ -2,47 +2,31 @@
module Issues
class ReorderService < Issues::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
- return false if group && !can?(current_user, :read_group, group)
-
- attrs = issue_params(group)
- return false if attrs.empty?
+ return false unless move_between_ids
- update(issue, attrs)
+ update(issue, { move_between_ids: move_between_ids })
end
private
- def group
- return unless params[:group_full_path]
-
- @group ||= Group.find_by_full_path(params[:group_full_path])
- end
-
def update(issue, attrs)
::Issues::UpdateService.new(project: project, current_user: current_user, params: attrs).execute(issue)
rescue ActiveRecord::RecordNotFound
false
end
- def issue_params(group)
- attrs = {}
-
- if move_between_ids
- attrs[:move_between_ids] = move_between_ids
- attrs[:board_group_id] = group&.id
- end
-
- attrs
- end
-
def move_between_ids
- ids = [params[:move_after_id], params[:move_before_id]]
- .map(&:to_i)
- .map { |m| m > 0 ? m : nil }
+ strong_memoize(:move_between_ids) do
+ ids = [params[:move_after_id], params[:move_before_id]]
+ .map(&:to_i)
+ .map { |m| m > 0 ? m : nil }
- ids.any? ? ids : nil
+ ids.any? ? ids : nil
+ end
end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index c024cef2f82..4dc6294490f 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -100,10 +100,6 @@ module Issues
rebalance_if_needed(issue)
end
- def positioning_scope_key
- :board_group_id
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def change_issue_duplicate(issue)
canonical_issue_id = params.delete(:canonical_issue_id)
@@ -221,20 +217,13 @@ module Issues
).execute
end
- # rubocop: disable CodeReuse/ActiveRecord
- def issuable_for_positioning(id, board_group_id = nil)
+ def issuable_for_positioning(id, positioning_scope)
return unless id
- issue =
- if board_group_id
- IssuesFinder.new(current_user, group_id: board_group_id, include_subgroups: true).find_by(id: id)
- else
- project.issues.find(id)
- end
+ issue = positioning_scope.find(id)
issue if can?(current_user, :update_issue, issue)
end
- # rubocop: enable CodeReuse/ActiveRecord
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 419dd827e49..65b93dc930a 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -6,13 +6,7 @@
- if integration.operating?
= sprite_icon('check', css_class: 'gl-text-green-500')
-- if vue_integration_form_enabled?
- = render 'shared/integration_settings', integration: integration
-- else
- = form_for(integration, as: :service, url: scoped_integration_path(integration, project: @project, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_integration_path(@project, integration), testid: 'integration-form' } }) do |form|
- = render 'shared/integration_settings', form: form, integration: integration
- %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referer }
-
+= render 'shared/integration_settings', integration: integration
- if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true)
%hr
= render "projects/services/#{integration.to_param}/show", integration: integration
diff --git a/app/views/shared/integrations/_form.html.haml b/app/views/shared/integrations/_form.html.haml
deleted file mode 100644
index e2457bc0632..00000000000
--- a/app/views/shared/integrations/_form.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- integration = local_assigns.fetch(:integration)
-
-= form_for integration, as: :service, url: scoped_integration_path(integration, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration, group: @group), testid: 'integration-form' } } do |form|
- = render 'shared/integration_settings', form: form, integration: integration
diff --git a/app/views/shared/integrations/edit.html.haml b/app/views/shared/integrations/edit.html.haml
index 4ceaedc2a69..f2a31400698 100644
--- a/app/views/shared/integrations/edit.html.haml
+++ b/app/views/shared/integrations/edit.html.haml
@@ -7,7 +7,4 @@
= @integration.title
= render 'shared/integrations/tabs', integration: @integration, active_tab: 'edit' do
- - if vue_integration_form_enabled?
- = render 'shared/integration_settings', integration: @integration
- - else
- = render 'shared/integrations/form', integration: @integration
+ = render 'shared/integration_settings', integration: @integration
diff --git a/config/feature_flags/development/vue_integration_form.yml b/config/feature_flags/development/vue_integration_form.yml
deleted file mode 100644
index a11c42b8d4a..00000000000
--- a/config/feature_flags/development/vue_integration_form.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: vue_integration_form
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77934
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350444
-milestone: '14.7'
-type: development
-group: group::integrations
-default_enabled: false
diff --git a/db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb b/db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb
new file mode 100644
index 00000000000..50aa0121055
--- /dev/null
+++ b/db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddSeverityLevelToMergeRequestsComplianceViolations < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :merge_requests_compliance_violations, :severity_level, :integer, limit: 2, null: false, default: 0
+ end
+end
diff --git a/db/schema_migrations/20220131135725 b/db/schema_migrations/20220131135725
new file mode 100644
index 00000000000..d63b24d5d6c
--- /dev/null
+++ b/db/schema_migrations/20220131135725
@@ -0,0 +1 @@
+a0e9bb92512b9ddb3fd718e086b0fd116ec307be6acc5789a872b1d3004fdebd \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f76f7cfd3d2..b1f0a258634 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16489,7 +16489,8 @@ CREATE TABLE merge_requests_compliance_violations (
id bigint NOT NULL,
violating_user_id bigint NOT NULL,
merge_request_id bigint NOT NULL,
- reason smallint NOT NULL
+ reason smallint NOT NULL,
+ severity_level smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE merge_requests_compliance_violations_id_seq
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index c72e5a31926..62913c56853 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -192,6 +192,21 @@ To illustrate its life cycle:
The merge request pipeline widget shows information about the pipeline status in
a merge request. It's displayed above the [ability to merge status widget](#merge-request-status-messages).
+#### "Checking ability to merge automatically" message
+
+There is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/229352)
+where a merge request can be stuck with the `Checking ability to merge automatically`
+message.
+
+If your merge request has this message and it does not disappear after a few minutes,
+you can try one of these workarounds:
+
+- Refresh the merge request page.
+- Close & Re-open the merge request.
+- Rebase the merge request with the `/rebase` [quick action](../user/project/quick_actions.md).
+- If you have already confirmed the merge request is ready to be merged, you can merge
+ it with the `/merge` quick action.
+
#### "Checking pipeline status" message
This message is shown when the merge request has no pipeline associated with the
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 44429754747..71d7e7f1426 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -79,8 +79,12 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_create",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
+ "owner_email": "johnsmith@example.com",
"owner_name": "John Smith",
+ "owners": [{
+ "name": "John",
+ "email": "user1@example.com"
+ }],
"path": "storecloud",
"path_with_namespace": "jsmith/storecloud",
"project_id": 74,
@@ -96,8 +100,12 @@ X-Gitlab-Event: System Hook
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_destroy",
"name": "Underscore",
- "owner_email": "johnsmith@gmail.com",
+ "owner_email": "johnsmith@example.com",
"owner_name": "John Smith",
+ "owners": [{
+ "name": "John",
+ "email": "user1@example.com"
+ }],
"path": "underscore",
"path_with_namespace": "jsmith/underscore",
"project_id": 73,
@@ -117,7 +125,11 @@ X-Gitlab-Event: System Hook
"path_with_namespace": "jsmith/underscore",
"project_id": 73,
"owner_name": "John Smith",
- "owner_email": "johnsmith@gmail.com",
+ "owner_email": "johnsmith@example.com",
+ "owners": [{
+ "name": "John",
+ "email": "user1@example.com"
+ }],
"project_visibility": "internal",
"old_path_with_namespace": "jsmith/overscore"
}
@@ -138,7 +150,11 @@ Please refer to `group_rename` and `user_rename` for that case.
"path_with_namespace": "scores/underscore",
"project_id": 73,
"owner_name": "John Smith",
- "owner_email": "johnsmith@gmail.com",
+ "owner_email": "johnsmith@example.com",
+ "owners": [{
+ "name": "John",
+ "email": "user1@example.com"
+ }],
"project_visibility": "internal",
"old_path_with_namespace": "jsmith/overscore"
}
@@ -152,8 +168,12 @@ Please refer to `group_rename` and `user_rename` for that case.
"updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_update",
"name": "StoreCloud",
- "owner_email": "johnsmith@gmail.com",
+ "owner_email": "johnsmith@example.com",
"owner_name": "John Smith",
+ "owners": [{
+ "name": "John",
+ "email": "user1@example.com"
+ }],
"path": "storecloud",
"path_with_namespace": "jsmith/storecloud",
"project_id": 74,
@@ -173,7 +193,7 @@ Please refer to `group_rename` and `user_rename` for that case.
"project_name": "StoreCloud",
"project_path": "storecloud",
"project_path_with_namespace": "jsmith/storecloud",
- "user_email": "johnsmith@gmail.com",
+ "user_email": "johnsmith@example.com",
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41,
@@ -193,7 +213,7 @@ Please refer to `group_rename` and `user_rename` for that case.
"project_name": "StoreCloud",
"project_path": "storecloud",
"project_path_with_namespace": "jsmith/storecloud",
- "user_email": "johnsmith@gmail.com",
+ "user_email": "johnsmith@example.com",
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41,
@@ -213,7 +233,7 @@ Please refer to `group_rename` and `user_rename` for that case.
"project_name": "StoreCloud",
"project_path": "storecloud",
"project_path_with_namespace": "jsmith/storecloud",
- "user_email": "johnsmith@gmail.com",
+ "user_email": "johnsmith@example.com",
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41,
@@ -360,7 +380,7 @@ If the user is blocked via LDAP, `state` is `ldap_blocked`.
"group_id": 78,
"group_name": "StoreCloud",
"group_path": "storecloud",
- "user_email": "johnsmith@gmail.com",
+ "user_email": "johnsmith@example.com",
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41
@@ -378,7 +398,7 @@ If the user is blocked via LDAP, `state` is `ldap_blocked`.
"group_id": 78,
"group_name": "StoreCloud",
"group_path": "storecloud",
- "user_email": "johnsmith@gmail.com",
+ "user_email": "johnsmith@example.com",
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41
@@ -396,7 +416,7 @@ If the user is blocked via LDAP, `state` is `ldap_blocked`.
"group_id": 78,
"group_name": "StoreCloud",
"group_path": "storecloud",
- "user_email": "johnsmith@gmail.com",
+ "user_email": "johnsmith@example.com",
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41
diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb
index ca76d2664f8..9f59eea5013 100644
--- a/lib/api/ci/job_artifacts.rb
+++ b/lib/api/ci/job_artifacts.rb
@@ -47,7 +47,7 @@ module API
requires :artifact_path, type: String, desc: 'Artifact path'
end
route_setting :authentication, job_token_allowed: true
- get ':id/jobs/artifacts/:ref_name/raw/*artifact_path',
+ get ':id/jobs/artifacts/:ref_name/raw/*artifact_path', urgency: :low,
format: false,
requirements: { ref_name: /.+/ } do
authorize_download_artifacts!
@@ -70,7 +70,7 @@ module API
requires :job_id, type: Integer, desc: 'The ID of a job'
end
route_setting :authentication, job_token_allowed: true
- get ':id/jobs/:job_id/artifacts' do
+ get ':id/jobs/:job_id/artifacts', urgency: :low do
authorize_download_artifacts!
build = find_build!(params[:job_id])
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 4e5d6c264bf..e0086f624a8 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -179,7 +179,7 @@ module API
params do
requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
end
- get ':id/pipelines/:pipeline_id/test_report', feature_category: :code_testing do
+ get ':id/pipelines/:pipeline_id/test_report', feature_category: :code_testing, urgency: :low do
authorize! :read_build, pipeline
present pipeline.test_reports, with: TestReportEntity, details: true
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index 153738a8edc..9dcb10dd5e2 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -244,7 +244,7 @@ module API
optional :artifact_type, type: String, desc: %q(The type of artifact),
default: 'archive', values: ::Ci::JobArtifact.file_types.keys
end
- post '/:id/artifacts/authorize', feature_category: :build_artifacts do
+ post '/:id/artifacts/authorize', feature_category: :build_artifacts, urgency: :low do
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
@@ -280,7 +280,7 @@ module API
default: 'zip', values: ::Ci::JobArtifact.file_formats.keys
optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware))
end
- post '/:id/artifacts', feature_category: :build_artifacts do
+ post '/:id/artifacts', feature_category: :build_artifacts, urgency: :low do
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
diff --git a/lib/gitlab/hook_data/project_builder.rb b/lib/gitlab/hook_data/project_builder.rb
index 65c237f743f..ebd97d3ab1b 100644
--- a/lib/gitlab/hook_data/project_builder.rb
+++ b/lib/gitlab/hook_data/project_builder.rb
@@ -16,6 +16,7 @@ module Gitlab
# project_id: 1,
# owner_name: "John",
# owner_email: "user1@example.org",
+ # owners: [name: "John", email: "user1@example.org"],
# project_visibility: "internal",
# old_path_with_namespace: "old-path-with-namespace"
# }
@@ -32,19 +33,33 @@ module Gitlab
private
def project_data
- owner = project.owner
+ owners = project.owners.compact
+ # When this is removed, also remove the `deprecated_owner` method
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/350603
+ owner = project.deprecated_owner
{
name: project.name,
path: project.path,
path_with_namespace: project.full_path,
project_id: project.id,
- owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : "",
+ owner_name: owner.try(:name),
+ owner_email: user_email(owner),
+ owners: owners.map do |owner|
+ owner_data(owner)
+ end,
project_visibility: project.visibility.downcase
}
end
+ def owner_data(user)
+ { name: user.name, email: user_email(user) }
+ end
+
+ def user_email(user)
+ user.respond_to?(:email) ? user.email : ""
+ end
+
def event_specific_project_data(event)
return {} unless event == :rename || event == :transfer
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index d91c1b0d29a..bf0b833b311 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -502,10 +502,7 @@ RSpec.describe Projects::IssuesController do
context 'with valid params' do
it 'reorders issues and returns a successful 200 response' do
- reorder_issue(issue1,
- move_after_id: issue2.id,
- move_before_id: issue3.id,
- group_full_path: group.full_path)
+ reorder_issue(issue1, move_after_id: issue2.id, move_before_id: issue3.id)
[issue1, issue2, issue3].map(&:reload)
@@ -531,12 +528,10 @@ RSpec.describe Projects::IssuesController do
end
it 'returns a unprocessable entity 422 response for issues not in group' do
- another_group = create(:group)
+ other_group_project = create(:project, group: create(:group))
+ other_group_issue = create(:issue, project: other_group_project)
- reorder_issue(issue1,
- move_after_id: issue2.id,
- move_before_id: issue3.id,
- group_full_path: another_group.full_path)
+ reorder_issue(issue1, move_after_id: issue2.id, move_before_id: other_group_issue.id)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -555,15 +550,14 @@ RSpec.describe Projects::IssuesController do
end
end
- def reorder_issue(issue, move_after_id: nil, move_before_id: nil, group_full_path: nil)
+ def reorder_issue(issue, move_after_id: nil, move_before_id: nil)
put :reorder,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: issue.iid,
move_after_id: move_after_id,
- move_before_id: move_before_id,
- group_full_path: group_full_path
+ move_before_id: move_before_id
},
format: :json
end
diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
index 793a5bced00..22a27b33671 100644
--- a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb
@@ -19,19 +19,4 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ
expect(page).to have_link('Settings', href: edit_path)
expect(page).to have_link('Projects using custom settings', href: overrides_path)
end
-
- it 'does not render integration form element' do
- expect(page).not_to have_selector('[data-testid="integration-form"]')
- end
-
- context 'when `vue_integration_form` feature flag is disabled' do
- before do
- stub_feature_flags(vue_integration_form: false)
- visit_instance_integration('Mattermost slash commands')
- end
-
- it 'renders integration form element' do
- expect(page).to have_selector('[data-testid="integration-form"]')
- end
- end
end
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index f695b225915..a4aae7d8020 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -64,7 +64,11 @@ RSpec.describe 'Merge request > Batch comments', :js do
it 'deletes draft note' do
write_diff_comment
- accept_alert { find('.js-note-delete').click }
+ find('.js-note-delete').click
+
+ page.within('.modal') do
+ click_button('Delete Comment', match: :first)
+ end
wait_for_requests
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index 8f7ad9bd90e..29c346593d5 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -238,8 +238,11 @@ RSpec.describe 'User comments on a diff', :js do
page.within('.diff-file:nth-of-type(1) .discussion .note') do
find('.more-actions').click
find('.more-actions .dropdown-menu li', match: :first)
+ find('.js-note-delete').click
+ end
- accept_confirm { find('.js-note-delete').click }
+ page.within('.modal') do
+ click_button('Delete Comment', match: :first)
end
page.within('.merge-request-tabs') do
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 9e314e18563..76431d04e72 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -103,7 +103,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
- accept_confirm do
+ accept_gl_confirm(button_text: 'Delete Comment') do
first('button.more-actions-toggle').click
first('.js-note-delete').click
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 0416474218f..393108d8407 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -165,11 +165,13 @@ RSpec.describe 'Merge request > User posts notes', :js do
it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
+ find('[data-testid="cancel"]').click
+ end
- accept_confirm do
- find('[data-testid="cancel"]').click
- end
+ page.within('.modal') do
+ click_button('OK', match: :first)
end
+
expect(find('.js-note-text').text).to eq ''
end
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 64cd5aa2bb1..261d1ad325b 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -1,9 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
+include Spec::Support::Helpers::ModalHelpers # rubocop:disable Style/MixinUsage
RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
include NoteInteractionHelpers
+ include Spec::Support::Helpers::ModalHelpers
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -121,8 +123,8 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
it 'removes avatar when note is deleted' do
open_more_actions_dropdown(note)
- page.within find(".note-row-#{note.id}") do
- accept_confirm { find('.js-note-delete').click }
+ accept_gl_confirm(button_text: 'Delete Comment') do
+ find(".note-row-#{note.id} .js-note-delete").click
end
wait_for_requests
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 2f123508c06..7e01b79383a 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -33,13 +33,6 @@ describe('IntegrationForm', () => {
let wrapper;
let dispatch;
let mockAxios;
- let mockForm;
- let vueIntegrationFormFeatureFlag;
-
- const createForm = () => {
- mockForm = document.createElement('form');
- jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
- };
const createComponent = ({
customStateProps = {},
@@ -53,10 +46,6 @@ describe('IntegrationForm', () => {
});
dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
- if (!vueIntegrationFormFeatureFlag) {
- createForm();
- }
-
wrapper = mountFn(IntegrationForm, {
propsData: { ...props },
store,
@@ -72,11 +61,6 @@ describe('IntegrationForm', () => {
show: mockToastShow,
},
},
- provide: {
- glFeatures: {
- vueIntegrationForm: vueIntegrationFormFeatureFlag,
- },
- },
});
};
@@ -93,14 +77,8 @@ describe('IntegrationForm', () => {
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
- const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
const findDynamicField = () => wrapper.findComponent(DynamicField);
- const mockFormFunctions = ({ checkValidityReturn }) => {
- jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
- jest.spyOn(findFormElement(), 'submit');
- };
-
beforeEach(() => {
mockAxios = new MockAdapter(axios);
});
@@ -355,17 +333,14 @@ describe('IntegrationForm', () => {
});
});
- describe('when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', () => {
- it('renders hidden fields', () => {
- vueIntegrationFormFeatureFlag = true;
- createComponent({
- customStateProps: {
- redirectTo: '/services',
- },
- });
-
- expect(findRedirectToField().attributes('value')).toBe('/services');
+ it('renders hidden fields', () => {
+ createComponent({
+ customStateProps: {
+ redirectTo: '/services',
+ },
});
+
+ expect(findRedirectToField().attributes('value')).toBe('/services');
});
});
@@ -387,218 +362,200 @@ describe('IntegrationForm', () => {
});
describe.each`
- formActive | vueIntegrationFormEnabled | novalidate
- ${true} | ${true} | ${null}
- ${false} | ${true} | ${'novalidate'}
- ${true} | ${false} | ${null}
- ${false} | ${false} | ${'true'}
+ formActive | novalidate
+ ${true} | ${undefined}
+ ${false} | ${'true'}
`(
- 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled and `toggle-integration-active` is emitted with $formActive',
- ({ formActive, vueIntegrationFormEnabled, novalidate }) => {
+ 'when `toggle-integration-active` is emitted with $formActive',
+ ({ formActive, novalidate }) => {
beforeEach(async () => {
- vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
-
createComponent({
customStateProps: {
showActive: true,
initialActivated: false,
},
- mountFn: mountExtended,
});
- mockFormFunctions({ checkValidityReturn: false });
await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive);
});
it(`sets noValidate to ${novalidate}`, () => {
- expect(findFormElement().getAttribute('novalidate')).toBe(novalidate);
+ expect(findGlForm().attributes('novalidate')).toBe(novalidate);
});
},
);
});
- describe.each`
- vueIntegrationFormEnabled
- ${true}
- ${false}
- `(
- 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled',
- ({ vueIntegrationFormEnabled }) => {
- beforeEach(() => {
- vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled;
- });
-
- describe('when `save` button is clicked', () => {
- describe('buttons', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- },
- mountFn: mountExtended,
- });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
+ describe('when `save` button is clicked', () => {
+ describe('buttons', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ },
+ mountFn: mountExtended,
+ });
- it('sets save button `loading` prop to `true`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(true);
- });
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
- it('sets test button `disabled` prop to `true`', () => {
- expect(findTestButton().props('disabled')).toBe(true);
+ it('sets save button `loading` prop to `true`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(true);
+ });
+
+ it('sets test button `disabled` prop to `true`', () => {
+ expect(findTestButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe.each`
+ checkValidityReturn | integrationActive
+ ${true} | ${false}
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
+ ({ integrationActive, checkValidityReturn }) => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: integrationActive,
+ },
+ mountFn: mountExtended,
});
+ jest.spyOn(findGlForm().element, 'submit');
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
+
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
+
+ it('submit form', () => {
+ expect(findGlForm().element.submit).toHaveBeenCalledTimes(1);
});
+ },
+ );
- describe.each`
- checkValidityReturn | integrationActive
- ${true} | ${false}
- ${true} | ${true}
- ${false} | ${false}
- `(
- 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
- ({ integrationActive, checkValidityReturn }) => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: integrationActive,
- },
- mountFn: mountExtended,
- });
-
- mockFormFunctions({ checkValidityReturn });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
-
- it('submits form', () => {
- expect(findFormElement().submit).toHaveBeenCalledTimes(1);
- });
+ describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
+ beforeEach(async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ initialActivated: true,
+ fields: [mockField],
},
- );
-
- describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
- beforeEach(async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- initialActivated: true,
- fields: [mockField],
- },
- mountFn: mountExtended,
- });
- mockFormFunctions({ checkValidityReturn: false });
-
- await findProjectSaveButton().vm.$emit('click', new Event('click'));
- });
+ mountFn: mountExtended,
+ });
+ jest.spyOn(findGlForm().element, 'submit');
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
- it('does not submit form', () => {
- expect(findFormElement().submit).not.toHaveBeenCalled();
- });
+ await findProjectSaveButton().vm.$emit('click', new Event('click'));
+ });
- it('sets save button `loading` prop to `false`', () => {
- expect(findProjectSaveButton().props('loading')).toBe(false);
- });
+ it('does not submit form', () => {
+ expect(findGlForm().element.submit).not.toHaveBeenCalled();
+ });
- it('sets test button `disabled` prop to `false`', () => {
- expect(findTestButton().props('disabled')).toBe(false);
- });
+ it('sets save button `loading` prop to `false`', () => {
+ expect(findProjectSaveButton().props('loading')).toBe(false);
+ });
- it('sets `isValidated` props on form fields', () => {
- expect(findDynamicField().props('isValidated')).toBe(true);
- });
+ it('sets test button `disabled` prop to `false`', () => {
+ expect(findTestButton().props('disabled')).toBe(false);
+ });
+
+ it('sets `isValidated` props on form fields', () => {
+ expect(findDynamicField().props('isValidated')).toBe(true);
+ });
+ });
+ });
+
+ describe('when `test` button is clicked', () => {
+ describe('when form is invalid', () => {
+ it('sets `isValidated` props on form fields', async () => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ fields: [mockField],
+ },
+ mountFn: mountExtended,
});
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
+
+ await findTestButton().vm.$emit('click', new Event('click'));
+
+ expect(findDynamicField().props('isValidated')).toBe(true);
});
+ });
- describe('when `test` button is clicked', () => {
- describe('when form is invalid', () => {
- it('sets `isValidated` props on form fields', async () => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- fields: [mockField],
- },
- mountFn: mountExtended,
- });
- mockFormFunctions({ checkValidityReturn: false });
+ describe('when form is valid', () => {
+ const mockTestPath = '/test';
- await findTestButton().vm.$emit('click', new Event('click'));
+ beforeEach(() => {
+ createComponent({
+ customStateProps: {
+ showActive: true,
+ canTest: true,
+ testPath: mockTestPath,
+ },
+ mountFn: mountExtended,
+ });
+ jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
+ });
- expect(findDynamicField().props('isValidated')).toBe(true);
- });
+ describe('buttons', () => {
+ beforeEach(async () => {
+ await findTestButton().vm.$emit('click', new Event('click'));
});
- describe('when form is valid', () => {
- const mockTestPath = '/test';
+ it('sets test button `loading` prop to `true`', () => {
+ expect(findTestButton().props('loading')).toBe(true);
+ });
- beforeEach(() => {
- createComponent({
- customStateProps: {
- showActive: true,
- canTest: true,
- testPath: mockTestPath,
- },
- mountFn: mountExtended,
- });
- mockFormFunctions({ checkValidityReturn: true });
+ it('sets save button `disabled` prop to `true`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe.each`
+ scenario | replyStatus | errorMessage | expectToast | expectSentry
+ ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
+ ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
+ ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
+ `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
+ beforeEach(async () => {
+ mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
+ error: Boolean(errorMessage),
+ message: errorMessage,
});
- describe('buttons', () => {
- beforeEach(async () => {
- await findTestButton().vm.$emit('click', new Event('click'));
- });
+ await findTestButton().vm.$emit('click', new Event('click'));
+ await waitForPromises();
+ });
- it('sets test button `loading` prop to `true`', () => {
- expect(findTestButton().props('loading')).toBe(true);
- });
+ it(`calls toast with '${expectToast}'`, () => {
+ expect(mockToastShow).toHaveBeenCalledWith(expectToast);
+ });
- it('sets save button `disabled` prop to `true`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(true);
- });
- });
+ it('sets `loading` prop of test button to `false`', () => {
+ expect(findTestButton().props('loading')).toBe(false);
+ });
- describe.each`
- scenario | replyStatus | errorMessage | expectToast | expectSentry
- ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
- ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false}
- ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
- `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => {
- beforeEach(async () => {
- mockAxios.onPut(mockTestPath).replyOnce(replyStatus, {
- error: Boolean(errorMessage),
- message: errorMessage,
- });
-
- await findTestButton().vm.$emit('click', new Event('click'));
- await waitForPromises();
- });
-
- it(`calls toast with '${expectToast}'`, () => {
- expect(mockToastShow).toHaveBeenCalledWith(expectToast);
- });
-
- it('sets `loading` prop of test button to `false`', () => {
- expect(findTestButton().props('loading')).toBe(false);
- });
-
- it('sets save button `disabled` prop to `false`', () => {
- expect(findProjectSaveButton().props('disabled')).toBe(false);
- });
-
- it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
- expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
- });
- });
+ it('sets save button `disabled` prop to `false`', () => {
+ expect(findProjectSaveButton().props('disabled')).toBe(false);
+ });
+
+ it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => {
+ expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0);
});
});
- },
- );
+ });
+ });
describe('when `reset-confirmation-modal` emits `reset` event', () => {
const mockResetPath = '/reset';
diff --git a/spec/lib/gitlab/hook_data/project_builder_spec.rb b/spec/lib/gitlab/hook_data/project_builder_spec.rb
index 672dbab918f..e86ac66b1ad 100644
--- a/spec/lib/gitlab/hook_data/project_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/project_builder_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do
let(:attributes) do
[
:event_name, :created_at, :updated_at, :name, :path, :path_with_namespace, :project_id,
- :owner_name, :owner_email, :project_visibility
+ :owners, :owner_name, :owner_email, :project_visibility
]
end
@@ -30,6 +30,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do
expect(data[:project_id]).to eq(project.id)
expect(data[:owner_name]).to eq('John')
expect(data[:owner_email]).to eq('john@example.com')
+ expect(data[:owners]).to contain_exactly({ name: 'John', email: 'john@example.com' })
expect(data[:project_visibility]).to eq('internal')
end
end
diff --git a/spec/models/namespaces/user_namespace_spec.rb b/spec/models/namespaces/user_namespace_spec.rb
index 7c00a597756..fb9e7571666 100644
--- a/spec/models/namespaces/user_namespace_spec.rb
+++ b/spec/models/namespaces/user_namespace_spec.rb
@@ -9,4 +9,13 @@ RSpec.describe Namespaces::UserNamespace, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:owner) }
end
+
+ describe '#owners' do
+ let(:owner) { build(:user) }
+ let(:namespace) { build(:namespace, owner: owner) }
+
+ specify do
+ expect(namespace.owners).to match_array([owner])
+ end
+ end
end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index e15f847d866..c5e57b5b18b 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -1184,14 +1184,15 @@ RSpec.describe API::Issues do
end
describe 'PUT /projects/:id/issues/:issue_iid/reorder' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue1) { create(:issue, project: project, relative_position: 10) }
let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) }
let_it_be(:issue3) { create(:issue, project: project, relative_position: 30) }
context 'when user has access' do
- before do
- project.add_developer(user)
+ before_all do
+ group.add_developer(user)
end
context 'with valid params' do
@@ -1217,6 +1218,19 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with issue in different project' do
+ let(:other_project) { create(:project, group: group) }
+ let(:other_issue) { create(:issue, project: other_project, relative_position: 80) }
+
+ it 'reorders issues and returns a successful 200 response' do
+ put api("/projects/#{other_project.id}/issues/#{other_issue.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(other_issue.reload.relative_position)
+ .to be_between(issue2.reload.relative_position, issue3.reload.relative_position)
+ end
+ end
end
context 'with unauthorized user' do
diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb
index 15668a3aa23..392930c1b9f 100644
--- a/spec/services/issues/reorder_service_spec.rb
+++ b/spec/services/issues/reorder_service_spec.rb
@@ -67,20 +67,10 @@ RSpec.describe Issues::ReorderService do
it_behaves_like 'issues reorder service'
context 'when ordering in a group issue list' do
- let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id, group_full_path: group.full_path } }
+ let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id } }
subject { service(params) }
- it 'sends the board_group_id parameter' do
- match_params = { move_between_ids: [issue2.id, issue3.id], board_group_id: group.id }
-
- expect(Issues::UpdateService)
- .to receive(:new).with(project: project, current_user: user, params: match_params)
- .and_return(double(execute: build(:issue)))
-
- subject.execute(issue1)
- end
-
it 'sorts issues' do
project2 = create(:project, namespace: group)
issue4 = create(:issue, project: project2)
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index c53231c7042..95394ba6597 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -385,7 +385,6 @@ RSpec.describe Issues::UpdateService, :mailer do
[issue_1, issue_2, issue_3].map(&:save)
opts[:move_between_ids] = [issue_1.id, issue_2.id]
- opts[:board_group_id] = group.id
described_class.new(project: issue_3.project, current_user: user, params: opts).execute(issue_3)
expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position)
@@ -1309,19 +1308,12 @@ RSpec.describe Issues::UpdateService, :mailer do
end
context 'when moving an issue ' do
- it 'raises an error for invalid move ids within a project' do
+ it 'raises an error for invalid move ids' do
opts = { move_between_ids: [9000, non_existing_record_id] }
expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) }
.to raise_error(ActiveRecord::RecordNotFound)
end
-
- it 'raises an error for invalid move ids within a group' do
- opts = { move_between_ids: [9000, non_existing_record_id], board_group_id: create(:group).id }
-
- expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) }
- .to raise_error(ActiveRecord::RecordNotFound)
- end
end
include_examples 'issuable update service' do
diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
index d9b837258ce..a46c2f0ac5c 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
@@ -140,19 +140,6 @@ RSpec.shared_examples 'issues move service' do |group|
expect(issue2.reload.updated_at.change(usec: 0)).to eq updated_at2.change(usec: 0)
end
- if group
- context 'when on a group board' do
- it 'sends the board_group_id parameter' do
- params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
-
- match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id }
- expect(Issues::UpdateService).to receive(:new).with(project: issue.project, current_user: user, params: match_params).and_return(double(execute: build(:issue)))
-
- described_class.new(parent, user, params).execute(issue)
- end
- end
- end
-
def reorder_issues(params, issues: [])
issues.each do |issue|
issue.move_to_end && issue.save!
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
deleted file mode 100644
index f212fd78b1a..00000000000
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'projects/services/_form' do
- let(:project) { create(:redmine_project) }
- let(:user) { create(:admin) }
-
- before do
- assign(:project, project)
-
- allow(controller).to receive(:current_user).and_return(user)
-
- allow(view).to receive_messages(
- current_user: user,
- can?: true,
- current_application_settings: Gitlab::CurrentSettings.current_application_settings,
- integration: project.redmine_integration,
- request: double(referer: '/services')
- )
- end
-
- context 'integrations form' do
- it 'does not render form element' do
- render
-
- expect(rendered).not_to have_selector('[data-testid="integration-form"]')
- end
-
- context 'when vue_integration_form feature flag is disabled' do
- before do
- stub_feature_flags(vue_integration_form: false)
- end
-
- it 'renders form element' do
- render
-
- expect(rendered).to have_selector('[data-testid="integration-form"]')
- end
-
- context 'commit_events and merge_request_events' do
- it 'display merge_request_events and commit_events descriptions' do
- allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
-
- render
-
- expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
- end
- end
- end
- end
-end