Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9486811b62
commit
1b72313041
|
@ -1 +1 @@
|
|||
e0e146ec58d2515c2767f77d2860afc4bb7c1c73
|
||||
e023bdab6cea4ad7124d64b12e77f2cdb0681e21
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -25,5 +25,9 @@ module Namespaces
|
|||
def self.sti_name
|
||||
'User'
|
||||
end
|
||||
|
||||
def owners
|
||||
Array.wrap(owner)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
return false unless move_between_ids
|
||||
|
||||
attrs = issue_params(group)
|
||||
return false if attrs.empty?
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
a0e9bb92512b9ddb3fd718e086b0fd116ec307be6acc5789a872b1d3004fdebd
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
accept_confirm do
|
||||
find('[data-testid="cancel"]').click
|
||||
end
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
mockFormFunctions({ checkValidityReturn });
|
||||
|
||||
await findProjectSaveButton().vm.$emit('click', new Event('click'));
|
||||
});
|
||||
|
||||
it('submits form', () => {
|
||||
expect(findFormElement().submit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
describe('when `save` button is clicked', () => {
|
||||
describe('buttons', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({
|
||||
customStateProps: {
|
||||
showActive: true,
|
||||
canTest: true,
|
||||
initialActivated: true,
|
||||
},
|
||||
);
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
|
||||
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'));
|
||||
});
|
||||
|
||||
await findProjectSaveButton().vm.$emit('click', new Event('click'));
|
||||
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);
|
||||
|
||||
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('submit form', () => {
|
||||
expect(findGlForm().element.submit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('sets test button `disabled` prop to `false`', () => {
|
||||
expect(findTestButton().props('disabled')).toBe(false);
|
||||
});
|
||||
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,
|
||||
});
|
||||
jest.spyOn(findGlForm().element, 'submit');
|
||||
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
|
||||
|
||||
it('sets `isValidated` props on form fields', () => {
|
||||
expect(findDynamicField().props('isValidated')).toBe(true);
|
||||
});
|
||||
await findProjectSaveButton().vm.$emit('click', new Event('click'));
|
||||
});
|
||||
|
||||
it('does not submit form', () => {
|
||||
expect(findGlForm().element.submit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets save button `loading` prop to `false`', () => {
|
||||
expect(findProjectSaveButton().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
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 form is valid', () => {
|
||||
const mockTestPath = '/test';
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
customStateProps: {
|
||||
showActive: true,
|
||||
canTest: true,
|
||||
testPath: mockTestPath,
|
||||
},
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe('buttons', () => {
|
||||
beforeEach(async () => {
|
||||
await findTestButton().vm.$emit('click', new Event('click'));
|
||||
});
|
||||
|
||||
it('sets test button `loading` prop to `true`', () => {
|
||||
expect(findTestButton().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets save button `disabled` prop to `true`', () => {
|
||||
expect(findProjectSaveButton().props('disabled')).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 });
|
||||
|
||||
await findTestButton().vm.$emit('click', new Event('click'));
|
||||
|
||||
expect(findDynamicField().props('isValidated')).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,
|
||||
});
|
||||
|
||||
await findTestButton().vm.$emit('click', new Event('click'));
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
describe('when form is valid', () => {
|
||||
const mockTestPath = '/test';
|
||||
it(`calls toast with '${expectToast}'`, () => {
|
||||
expect(mockToastShow).toHaveBeenCalledWith(expectToast);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
customStateProps: {
|
||||
showActive: true,
|
||||
canTest: true,
|
||||
testPath: mockTestPath,
|
||||
},
|
||||
mountFn: mountExtended,
|
||||
});
|
||||
mockFormFunctions({ checkValidityReturn: true });
|
||||
});
|
||||
it('sets `loading` prop of test button to `false`', () => {
|
||||
expect(findTestButton().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
describe('buttons', () => {
|
||||
beforeEach(async () => {
|
||||
await findTestButton().vm.$emit('click', new Event('click'));
|
||||
});
|
||||
it('sets save button `disabled` prop to `false`', () => {
|
||||
expect(findProjectSaveButton().props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('sets test button `loading` prop to `true`', () => {
|
||||
expect(findTestButton().props('loading')).toBe(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,
|
||||
});
|
||||
|
||||
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(`${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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue