Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3e9023894d
commit
1c7411c597
|
@ -1 +1 @@
|
|||
9de3dd28a5c8248903160ea35d9f718899f51c89
|
||||
4892c8502cc45217903a8a584a7b5edb15edf86e
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
cancelAction: { text: __('Cancel') },
|
||||
components: {
|
||||
GlModal,
|
||||
},
|
||||
props: {
|
||||
primaryText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: __('OK'),
|
||||
},
|
||||
primaryVariant: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'confirm',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
primaryAction() {
|
||||
return { text: this.primaryText, attributes: { variant: this.primaryVariant } };
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
ref="modal"
|
||||
size="sm"
|
||||
modal-id="confirmationModal"
|
||||
body-class="gl-display-flex"
|
||||
:action-primary="primaryAction"
|
||||
:action-cancel="$options.cancelAction"
|
||||
hide-header
|
||||
@primary="$emit('confirmed')"
|
||||
@hidden="$emit('closed')"
|
||||
>
|
||||
<div class="gl-align-self-center"><slot></slot></div>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -0,0 +1,47 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export function confirmViaGlModal(message, element) {
|
||||
return new Promise((resolve) => {
|
||||
let confirmed = false;
|
||||
|
||||
const props = {};
|
||||
|
||||
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
|
||||
|
||||
if (confirmBtnVariant) {
|
||||
props.primaryVariant = confirmBtnVariant;
|
||||
}
|
||||
const screenReaderText =
|
||||
element.querySelector('.gl-sr-only')?.textContent ||
|
||||
element.querySelector('.sr-only')?.textContent ||
|
||||
element.getAttribute('aria-label');
|
||||
|
||||
if (screenReaderText) {
|
||||
props.primaryText = screenReaderText;
|
||||
}
|
||||
|
||||
const component = new Vue({
|
||||
components: {
|
||||
ConfirmModal: () => import('./confirm_modal.vue'),
|
||||
},
|
||||
render(h) {
|
||||
return h(
|
||||
'confirm-modal',
|
||||
{
|
||||
props,
|
||||
on: {
|
||||
confirmed() {
|
||||
confirmed = true;
|
||||
},
|
||||
closed() {
|
||||
component.$destroy();
|
||||
resolve(confirmed);
|
||||
},
|
||||
},
|
||||
},
|
||||
[message],
|
||||
);
|
||||
},
|
||||
}).$mount();
|
||||
});
|
||||
}
|
|
@ -1,4 +1,42 @@
|
|||
import Rails from '@rails/ujs';
|
||||
import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
|
||||
function monkeyPatchConfirmModal() {
|
||||
/**
|
||||
* This function is used to replace the `Rails.confirm` which uses `window.confirm`
|
||||
*
|
||||
* This function opens a confirmation modal which will resolve in a promise.
|
||||
* Because the `Rails.confirm` API is synchronous, we go with a little hack here:
|
||||
*
|
||||
* 1. User clicks on something with `data-confirm`
|
||||
* 2. We open the modal and return `false`, ending the "Rails" event chain
|
||||
* 3. If the modal is closed and the user "confirmed" the action
|
||||
* 1. replace the `Rails.confirm` with a function that always returns `true`
|
||||
* 2. click the same element programmatically
|
||||
*
|
||||
* @param message {String} Message to be shown in the modal
|
||||
* @param element {HTMLElement} Element that was clicked on
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function confirmViaModal(message, element) {
|
||||
confirmViaGlModal(message, element)
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
Rails.confirm = () => true;
|
||||
element.click();
|
||||
Rails.confirm = confirmViaModal;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return false;
|
||||
}
|
||||
|
||||
Rails.confirm = confirmViaModal;
|
||||
}
|
||||
|
||||
if (gon?.features?.bootstrapConfirmationModals) {
|
||||
monkeyPatchConfirmModal();
|
||||
}
|
||||
|
||||
export const initRails = () => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
|
|
|
@ -16,9 +16,11 @@ import { __, s__, sprintf } from '~/locale';
|
|||
import Tracking from '~/tracking';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import {
|
||||
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
CONTENT_EDITOR_LOADED_ACTION,
|
||||
SAVED_USING_CONTENT_EDITOR_ACTION,
|
||||
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
WIKI_FORMAT_LABEL,
|
||||
WIKI_FORMAT_UPDATED_ACTION,
|
||||
} from '../constants';
|
||||
|
||||
const trackingMixin = Tracking.mixin({
|
||||
|
@ -219,6 +221,8 @@ export default {
|
|||
this.trackFormSubmit();
|
||||
}
|
||||
|
||||
this.trackWikiFormat();
|
||||
|
||||
// Wait until form field values are refreshed
|
||||
await this.$nextTick();
|
||||
|
||||
|
@ -304,6 +308,14 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
trackWikiFormat() {
|
||||
this.track(WIKI_FORMAT_UPDATED_ACTION, {
|
||||
label: WIKI_FORMAT_LABEL,
|
||||
value: this.format,
|
||||
extra: { project_path: this.pageInfo.path, old_format: this.pageInfo.format },
|
||||
});
|
||||
},
|
||||
|
||||
dismissContentEditorAlert() {
|
||||
this.isContentEditorAlertDismissed = true;
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
|
||||
|
||||
export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded';
|
||||
export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor';
|
||||
export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor';
|
||||
export const WIKI_FORMAT_LABEL = 'wiki_format';
|
||||
export const WIKI_FORMAT_UPDATED_ACTION = 'wiki_format_updated';
|
||||
|
|
|
@ -75,6 +75,7 @@ export default {
|
|||
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
|
||||
projectKey: this.initialProjectKey,
|
||||
searchTerm: '',
|
||||
projectKeyError: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -104,6 +105,14 @@ export default {
|
|||
this.selectedFileTemplateProjectId = selectedFileTemplateProjectId;
|
||||
this.selectedTemplate = selectedTemplate;
|
||||
},
|
||||
validateProjectKey() {
|
||||
if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) {
|
||||
this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.projectKeyError = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -169,8 +178,17 @@ export default {
|
|||
v-model.trim="projectKey"
|
||||
data-testid="project-suffix"
|
||||
class="form-control"
|
||||
:state="!projectKeyError"
|
||||
@blur="validateProjectKey"
|
||||
/>
|
||||
<span v-if="hasProjectKeySupport" class="form-text text-muted">
|
||||
<span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger">
|
||||
{{ projectKeyError }}
|
||||
</span>
|
||||
<span
|
||||
v-if="hasProjectKeySupport"
|
||||
class="form-text text-muted"
|
||||
:class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }"
|
||||
>
|
||||
{{ __('A string appended to the project path to form the Service Desk email address.') }}
|
||||
</span>
|
||||
<span v-else class="form-text text-muted">
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<script>
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { GlBadge, GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
|
||||
import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlResizeObserver: GlResizeObserverDirective,
|
||||
},
|
||||
props: {
|
||||
tag: {
|
||||
type: String,
|
||||
|
@ -14,14 +18,39 @@ export default {
|
|||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'md',
|
||||
default: 'sm',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
overflowing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tooltip() {
|
||||
if (this.overflowing) {
|
||||
return this.tag;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onResize() {
|
||||
const { scrollWidth, offsetWidth } = this.$el;
|
||||
this.overflowing = scrollWidth > offsetWidth;
|
||||
},
|
||||
},
|
||||
RUNNER_TAG_BADGE_VARIANT,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
|
||||
<gl-badge
|
||||
v-gl-tooltip="tooltip"
|
||||
v-gl-resize-observer="onResize"
|
||||
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
|
||||
:size="size"
|
||||
:variant="$options.RUNNER_TAG_BADGE_VARIANT"
|
||||
>
|
||||
{{ tag }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
|
|
@ -14,13 +14,19 @@ export default {
|
|||
size: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'md',
|
||||
default: 'sm',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
|
||||
<runner-tag
|
||||
v-for="tag in tagList"
|
||||
:key="tag"
|
||||
class="gl-display-inline gl-mr-1"
|
||||
:tag="tag"
|
||||
:size="size"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -27,7 +27,7 @@ export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
|
|||
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
|
||||
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
|
||||
|
||||
export const RUNNER_TAG_BADGE_VARIANT = 'info';
|
||||
export const RUNNER_TAG_BADGE_VARIANT = 'neutral';
|
||||
export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
|
||||
|
||||
// Filtered search parameter names
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import $ from 'jquery';
|
||||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import initMRPopovers from '~/mr_popover/';
|
||||
import noteHeader from '~/notes/components/note_header.vue';
|
||||
|
@ -61,6 +62,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
lines: [],
|
||||
showLines: false,
|
||||
loadingDiff: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -94,10 +98,25 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
|
||||
async toggleDiff() {
|
||||
this.showLines = !this.showLines;
|
||||
|
||||
if (!this.lines.length) {
|
||||
this.loadingDiff = true;
|
||||
const { data } = await axios.get(this.note.outdated_line_change_path);
|
||||
|
||||
this.lines = data.map((l) => ({
|
||||
...l,
|
||||
rich_text: l.rich_text.replace(/^[+ -]/, ''),
|
||||
}));
|
||||
this.loadingDiff = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
safeHtmlConfig: {
|
||||
ADD_TAGS: ['use'], // to support icon SVGs
|
||||
},
|
||||
userColorSchemeClass: window.gon.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -112,15 +131,28 @@ export default {
|
|||
<div class="note-header">
|
||||
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
|
||||
<span v-safe-html="actionTextHtml"></span>
|
||||
<template v-if="canSeeDescriptionVersion" #extra-controls>
|
||||
<template
|
||||
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
|
||||
#extra-controls
|
||||
>
|
||||
·
|
||||
<gl-button
|
||||
v-if="canSeeDescriptionVersion"
|
||||
variant="link"
|
||||
:icon="descriptionVersionToggleIcon"
|
||||
data-testid="compare-btn"
|
||||
@click="toggleDescriptionVersion"
|
||||
>{{ __('Compare with previous version') }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
v-if="note.outdated_line_change_path"
|
||||
:icon="showLines ? 'chevron-up' : 'chevron-down'"
|
||||
variant="link"
|
||||
data-testid="outdated-lines-change-btn"
|
||||
@click="toggleDiff"
|
||||
>
|
||||
{{ __('Compare changes') }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</note-header>
|
||||
</div>
|
||||
|
@ -154,6 +186,37 @@ export default {
|
|||
@click="deleteDescriptionVersion"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="lines.length && showLines"
|
||||
class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
|
||||
>
|
||||
<table
|
||||
:class="$options.userColorSchemeClass"
|
||||
class="code js-syntax-highlight"
|
||||
data-testid="outdated-lines"
|
||||
>
|
||||
<tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
|
||||
<td
|
||||
:class="line.type"
|
||||
class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!"
|
||||
>
|
||||
{{ line.old_line }}
|
||||
</td>
|
||||
<td
|
||||
:class="line.type"
|
||||
class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
|
||||
>
|
||||
{{ line.new_line }}
|
||||
</td>
|
||||
<td
|
||||
:class="line.type"
|
||||
class="line_content gl-display-table-cell!"
|
||||
v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
|
||||
></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<gl-skeleton-loading v-else-if="showLines" class="gl-mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
</timeline-entry-item>
|
||||
|
|
|
@ -55,6 +55,14 @@ class Projects::NotesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def outdated_line_change
|
||||
diff_lines = Rails.cache.fetch(['note', note.id, 'oudated_line_change'], expires_in: 7.days) do
|
||||
::MergeRequests::OutdatedDiscussionDiffLinesService.new(project: @project, note: note).execute.to_json
|
||||
end
|
||||
|
||||
render json: diff_lines
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_json_with_notes_serializer
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Mixin for all resolver classes for type `Types::GroupType.connection_type`.
|
||||
module ResolvesGroups
|
||||
extend ActiveSupport::Concern
|
||||
include LooksAhead
|
||||
|
||||
def resolve_with_lookahead(**args)
|
||||
apply_lookahead(resolve_groups(**args))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The resolver should implement this method.
|
||||
def resolve_groups(**args)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def preloads
|
||||
{
|
||||
contacts: [:contacts],
|
||||
container_repositories_count: [:container_repositories],
|
||||
custom_emoji: [:custom_emoji],
|
||||
full_path: [:route],
|
||||
organizations: [:organizations],
|
||||
path: [:route],
|
||||
dependency_proxy_blob_count: [:dependency_proxy_blobs],
|
||||
dependency_proxy_blobs: [:dependency_proxy_blobs],
|
||||
dependency_proxy_image_count: [:dependency_proxy_manifests],
|
||||
dependency_proxy_image_ttl_policy: [:dependency_proxy_image_ttl_policy],
|
||||
dependency_proxy_setting: [:dependency_proxy_setting]
|
||||
}
|
||||
end
|
||||
end
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module Resolvers
|
||||
class GroupsResolver < BaseResolver
|
||||
include ResolvesGroups
|
||||
|
||||
type Types::GroupType, null: true
|
||||
|
||||
argument :include_parent_descendants, GraphQL::Types::Boolean,
|
||||
|
@ -19,16 +21,12 @@ module Resolvers
|
|||
|
||||
alias_method :parent, :object
|
||||
|
||||
def resolve(**args)
|
||||
return [] unless parent.present?
|
||||
|
||||
find_groups(args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_groups(args)
|
||||
def resolve_groups(args)
|
||||
return Group.none unless parent.present?
|
||||
|
||||
GroupsFinder
|
||||
.new(context[:current_user], args.merge(parent: parent))
|
||||
.execute
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
module Resolvers
|
||||
module Users
|
||||
class GroupsResolver < BaseResolver
|
||||
include ResolvesGroups
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
include LooksAhead
|
||||
|
||||
type Types::GroupType.connection_type, null: true
|
||||
|
||||
|
@ -23,19 +23,14 @@ module Resolvers
|
|||
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
|
||||
end
|
||||
|
||||
def resolve_with_lookahead(**args)
|
||||
return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
|
||||
|
||||
apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
|
||||
def ready?(**args)
|
||||
Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preloads
|
||||
{
|
||||
path: [:route],
|
||||
full_path: [:route]
|
||||
}
|
||||
def resolve_groups(**args)
|
||||
Groups::UserGroupsFinder.new(current_user, object, args).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,6 +34,7 @@ module Types
|
|||
null: true,
|
||||
method: :project_creation_level_str,
|
||||
description: 'Permission level required to create projects in the group.'
|
||||
|
||||
field :subgroup_creation_level,
|
||||
type: GraphQL::Types::String,
|
||||
null: true,
|
||||
|
@ -44,6 +45,7 @@ module Types
|
|||
type: GraphQL::Types::Boolean,
|
||||
null: true,
|
||||
description: 'Indicates if all users in this group are required to set up two-factor authentication.'
|
||||
|
||||
field :two_factor_grace_period,
|
||||
type: GraphQL::Types::Int,
|
||||
null: true,
|
||||
|
@ -225,11 +227,11 @@ module Types
|
|||
end
|
||||
|
||||
def dependency_proxy_image_count
|
||||
group.dependency_proxy_manifests.count
|
||||
group.dependency_proxy_manifests.size
|
||||
end
|
||||
|
||||
def dependency_proxy_blob_count
|
||||
group.dependency_proxy_blobs.count
|
||||
group.dependency_proxy_blobs.size
|
||||
end
|
||||
|
||||
def dependency_proxy_total_size
|
||||
|
|
|
@ -56,6 +56,9 @@ class Group < Namespace
|
|||
has_many :boards
|
||||
has_many :badges, class_name: 'GroupBadge'
|
||||
|
||||
has_many :organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group
|
||||
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group
|
||||
|
||||
has_many :cluster_groups, class_name: 'Clusters::Group'
|
||||
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
|
||||
|
||||
|
@ -757,14 +760,6 @@ class Group < Namespace
|
|||
Timelog.in_group(self)
|
||||
end
|
||||
|
||||
def organizations
|
||||
::CustomerRelations::Organization.where(group_id: self.id)
|
||||
end
|
||||
|
||||
def contacts
|
||||
::CustomerRelations::Contact.where(group_id: self.id)
|
||||
end
|
||||
|
||||
def dependency_proxy_image_ttl_policy
|
||||
super || build_dependency_proxy_image_ttl_policy
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ module Ci
|
|||
end
|
||||
|
||||
def runner_variables
|
||||
variables.sort_and_expand_all(project, keep_undefined: true).to_runner_variables
|
||||
variables.sort_and_expand_all(keep_undefined: true).to_runner_variables
|
||||
end
|
||||
|
||||
def refspecs
|
||||
|
|
|
@ -51,6 +51,10 @@ class NoteEntity < API::Entities::Note
|
|||
SystemNoteHelper.system_note_icon_name(note)
|
||||
end
|
||||
|
||||
expose :outdated_line_change_path, if: -> (note, _) { note.system? && note.change_position&.line_range && Feature.enabled?(:display_outdated_line_diff, note.project, default_enabled: :yaml) } do |note|
|
||||
outdated_line_change_namespace_project_note_path(namespace_id: note.project.namespace, project_id: note.project, id: note)
|
||||
end
|
||||
|
||||
expose :is_noteable_author do |note|
|
||||
note.noteable_author?(request.noteable)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MergeRequests
|
||||
class OutdatedDiscussionDiffLinesService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :project, :note
|
||||
|
||||
OVERFLOW_LINES_COUNT = 2
|
||||
|
||||
def initialize(project:, note:)
|
||||
@project = project
|
||||
@note = note
|
||||
end
|
||||
|
||||
def execute
|
||||
end_position = position.line_range["end"]
|
||||
diff_line_index = diff_lines.find_index { |l| l.new_line == end_position["new_line"] || l.old_line == end_position["old_line"] }
|
||||
initial_line_index = [diff_line_index - OVERFLOW_LINES_COUNT, 0].max
|
||||
last_line_index = [diff_line_index + OVERFLOW_LINES_COUNT, diff_lines.length].min
|
||||
|
||||
prev_lines = []
|
||||
|
||||
diff_lines[initial_line_index..last_line_index].each do |line|
|
||||
if line.meta?
|
||||
prev_lines.clear
|
||||
else
|
||||
prev_lines << line
|
||||
end
|
||||
end
|
||||
|
||||
prev_lines
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def position
|
||||
note.change_position
|
||||
end
|
||||
|
||||
def repository
|
||||
project.repository
|
||||
end
|
||||
|
||||
def diff_file
|
||||
position.diff_file(repository)
|
||||
end
|
||||
|
||||
def diff_lines
|
||||
strong_memoize(:diff_lines) do
|
||||
diff_file.highlighted_diff_lines
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: bootstrap_confirmation_modals
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73167
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344658
|
||||
milestone: '14.5'
|
||||
type: development
|
||||
group: group::foundations
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: display_outdated_line_diff
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72597
|
||||
rollout_issue_url:
|
||||
milestone: '14.5'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: false
|
|
@ -540,6 +540,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
delete :delete_attachment # rubocop:todo Cop/PutProjectRoutesUnderScope
|
||||
post :resolve # rubocop:todo Cop/PutProjectRoutesUnderScope
|
||||
delete :resolve, action: :unresolve # rubocop:todo Cop/PutProjectRoutesUnderScope
|
||||
get :outdated_line_change # rubocop:todo Cop/PutProjectRoutesUnderScope
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,6 @@ This setup is for when you have installed GitLab using the
|
|||
[Omnibus GitLab **Enterprise Edition** (EE) package](https://about.gitlab.com/install/?version=ee).
|
||||
|
||||
All the tools that are needed like PostgreSQL, PgBouncer, and Patroni are bundled in
|
||||
the package, so you can it to set up the whole PostgreSQL infrastructure (primary, replica).
|
||||
the package, so you can use it to set up the whole PostgreSQL infrastructure (primary, replica).
|
||||
|
||||
[> Read how to set up PostgreSQL replication and failover using Omnibus GitLab](replication_and_failover.md)
|
||||
|
|
|
@ -23,7 +23,7 @@ We recommend the GitLab.com for Jira Cloud app, because data is
|
|||
synchronized in real time. The DVCS connector updates data only once per hour.
|
||||
|
||||
The user configuring the GitLab.com for Jira Cloud app must have
|
||||
at least the [Maintainer](../../user/permissions.md) role the GitLab.com namespace.
|
||||
at least the [Maintainer](../../user/permissions.md) role in the GitLab.com namespace.
|
||||
|
||||
This integration method supports [Smart Commits](dvcs.md#smart-commits).
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def variable_expansion_errors
|
||||
expanded_collection = evaluate_context.variables.sort_and_expand_all(@pipeline.project)
|
||||
expanded_collection = evaluate_context.variables.sort_and_expand_all
|
||||
errors = expanded_collection.errors
|
||||
["#{name}: #{errors}"] if errors
|
||||
end
|
||||
|
|
|
@ -89,7 +89,7 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def sort_and_expand_all(project, keep_undefined: false)
|
||||
def sort_and_expand_all(keep_undefined: false)
|
||||
sorted = Sort.new(self)
|
||||
return self.class.new(self, sorted.errors) unless sorted.valid?
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ module Gitlab
|
|||
# the current state on the CD diff, so we treat it as outdated.
|
||||
ac_diff = ac_diffs.diff_file_with_new_path(c_path, c_mode)
|
||||
|
||||
{ position: new_position(ac_diff, nil, c_line), outdated: true }
|
||||
{ position: new_position(ac_diff, nil, c_line, position.line_range), outdated: true }
|
||||
end
|
||||
else
|
||||
# If the line is still in D and not in C, it is still added.
|
||||
|
@ -112,7 +112,7 @@ module Gitlab
|
|||
end
|
||||
else
|
||||
# If the line is no longer in D, it has been removed from the MR.
|
||||
{ position: new_position(bd_diff, b_line, nil), outdated: true }
|
||||
{ position: new_position(bd_diff, b_line, nil, position.line_range), outdated: true }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -140,14 +140,14 @@ module Gitlab
|
|||
# removed line into an unchanged one.
|
||||
bd_diff = bd_diffs.diff_file_with_new_path(d_path, d_mode)
|
||||
|
||||
{ position: new_position(bd_diff, nil, d_line), outdated: true }
|
||||
{ position: new_position(bd_diff, nil, d_line, position.line_range), outdated: true }
|
||||
else
|
||||
# If the line is still in C and not in D, it is still removed.
|
||||
{ position: new_position(cd_diff, c_line, nil, position.line_range), outdated: false }
|
||||
end
|
||||
else
|
||||
# If the line is no longer in C, it has been removed outside of the MR.
|
||||
{ position: new_position(ac_diff, a_line, nil), outdated: true }
|
||||
{ position: new_position(ac_diff, a_line, nil, position.line_range), outdated: true }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ module Gitlab
|
|||
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:suppress_apollo_errors_during_navigation, current_user, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:configure_iac_scanning_via_mr, current_user, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
# Exposes the state of a feature flag to the frontend code.
|
||||
|
|
|
@ -24184,6 +24184,9 @@ msgstr ""
|
|||
msgid "Only reCAPTCHA v2 is supported:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Only use lowercase letters, numbers, and underscores."
|
||||
msgstr ""
|
||||
|
||||
msgid "Only users from the specified IP address ranges are able to reach this group, including all subgroups, projects, and Git repositories."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1007,6 +1007,35 @@ RSpec.describe Projects::NotesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET outdated_line_change' do
|
||||
let(:request_params) do
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: note,
|
||||
format: 'json'
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
service = double
|
||||
allow(service).to receive(:execute).and_return([{ line_text: 'Test' }])
|
||||
allow(MergeRequests::OutdatedDiscussionDiffLinesService).to receive(:new).once.and_return(service)
|
||||
|
||||
sign_in(user)
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it "successfully renders expected JSON response" do
|
||||
get :outdated_line_change, params: request_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to be_an(Array)
|
||||
expect(json_response.count).to eq(1)
|
||||
expect(json_response.first).to include({ "line_text" => "Test" })
|
||||
end
|
||||
end
|
||||
|
||||
# Convert a time to an integer number of microseconds
|
||||
def microseconds(time)
|
||||
(time.to_i * 1_000_000) + time.usec
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'Admin disables 2FA for a user' do
|
||||
it 'successfully', :js do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
admin = create(:admin)
|
||||
sign_in(admin)
|
||||
gitlab_enable_admin_mode_sign_in(admin)
|
||||
|
|
|
@ -252,6 +252,7 @@ RSpec.describe 'Admin Groups' do
|
|||
|
||||
describe 'admin remove themself from a group', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/222342' do
|
||||
it 'removes admin from the group' do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
group.add_user(current_user, Gitlab::Access::DEVELOPER)
|
||||
|
||||
visit group_group_members_path(group)
|
||||
|
|
|
@ -79,6 +79,7 @@ RSpec.describe 'Admin::Hooks' do
|
|||
let(:hook_url) { generate(:url) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
create(:system_hook, url: hook_url)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ RSpec.describe 'admin issues labels' do
|
|||
|
||||
describe 'list' do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
visit admin_labels_path
|
||||
end
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
|
|||
let!(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
|
||||
it "allows revocation of an active impersonation token" do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
visit admin_user_impersonation_tokens_path(user_id: user.username)
|
||||
|
||||
accept_confirm { click_on "Revoke" }
|
||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe 'Admin uses repository checks', :request_store do
|
|||
let(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
|
||||
sign_in(admin)
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users::User' do
|
|||
let_it_be(:current_user) { create(:admin) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(current_user)
|
||||
gitlab_enable_admin_mode_sign_in(current_user)
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users' do
|
|||
let_it_be(:current_user) { create(:admin) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(current_user)
|
||||
gitlab_enable_admin_mode_sign_in(current_user)
|
||||
end
|
||||
|
|
|
@ -536,6 +536,7 @@ RSpec.describe 'Project issue boards', :js do
|
|||
let_it_be(:user_guest) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
project.add_guest(user_guest)
|
||||
sign_in(user_guest)
|
||||
visit project_board_path(project, board)
|
||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe 'Groups > Members > Leave group' do
|
|||
let(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ RSpec.describe 'User comments on a diff', :js do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do
|
|||
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
end
|
||||
|
||||
context 'when hovering over a parallel view diff file' do
|
||||
|
|
|
@ -18,8 +18,10 @@ RSpec.describe 'Merge request > User posts notes', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit project_merge_request_path(project, merge_request)
|
||||
end
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
|
|||
%w(parallel).each do |view|
|
||||
context "#{view} view" do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
visit diffs_project_merge_request_path(project, merge_request, view: view)
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
@ -110,6 +110,7 @@ RSpec.describe 'Merge request > User sees deployment widget', :js do
|
|||
let(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
build.success!
|
||||
deployment.update!(on_stop: manual.name)
|
||||
visit project_merge_request_path(project, merge_request)
|
||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe 'Profile account page', :js do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
@ -80,6 +81,7 @@ RSpec.describe 'Profile account page', :js do
|
|||
describe 'when I reset incoming email token' do
|
||||
before do
|
||||
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
visit profile_personal_access_tokens_path
|
||||
end
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
|
|||
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
end
|
||||
|
||||
it 'user sees their active sessions' do
|
||||
travel_to(Time.zone.parse('2018-03-12 09:06')) do
|
||||
Capybara::Session.new(:session1)
|
||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe 'Profile > Applications' do
|
|||
let(:application) { create(:oauth_application, owner: user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ RSpec.describe "User deletes branch", :js do
|
|||
|
||||
context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
stub_feature_flags(delete_branch_confirmation_modals: false)
|
||||
end
|
||||
|
||||
|
|
|
@ -179,6 +179,7 @@ RSpec.describe 'Branches' do
|
|||
context 'when the delete_branch_confirmation_modals feature flag is disabled' do
|
||||
it 'removes branch after confirmation', :js do
|
||||
stub_feature_flags(delete_branch_confirmation_modals: false)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
|
||||
visit project_branches_filtered_path(project, state: 'all')
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe "User deletes comments on a commit", :js do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
project.add_developer(user)
|
||||
|
||||
|
|
|
@ -93,6 +93,8 @@ RSpec.describe "User comments on commit", :js do
|
|||
|
||||
context "when deleting comment" do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
|
||||
visit(project_commit_path(project, sample_commit.id))
|
||||
|
||||
add_note(comment_text)
|
||||
|
|
|
@ -143,6 +143,8 @@ RSpec.describe 'Environments page', :js do
|
|||
create(:environment, project: project, state: :available)
|
||||
end
|
||||
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
|
||||
context 'when there are no deployments' do
|
||||
before do
|
||||
visit_environments(project)
|
||||
|
|
|
@ -12,6 +12,7 @@ RSpec.describe 'User browses a job', :js do
|
|||
before do
|
||||
project.add_maintainer(user)
|
||||
project.enable_ci
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ RSpec.describe 'Projects > Members > Member leaves project' do
|
|||
before do
|
||||
project.add_developer(user)
|
||||
sign_in(user)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
end
|
||||
|
||||
it 'user leaves project' do
|
||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe 'Projects > Members > User requests access', :js do
|
|||
before do
|
||||
sign_in(user)
|
||||
visit project_path(project)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
end
|
||||
|
||||
it 'request access feature is disabled' do
|
||||
|
|
|
@ -14,6 +14,8 @@ RSpec.describe 'User adds pages domain', :js do
|
|||
project.add_maintainer(user)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
end
|
||||
|
||||
context 'when pages are exposed on external HTTP address', :http_pages_enabled do
|
||||
|
|
|
@ -14,6 +14,7 @@ RSpec.describe "Pages with Let's Encrypt", :https_pages_enabled do
|
|||
before do
|
||||
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
|
||||
stub_lets_encrypt_settings
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
|
||||
project.add_role(user, role)
|
||||
sign_in(user)
|
||||
|
|
|
@ -176,6 +176,7 @@ RSpec.describe 'Pages edits pages settings', :js do
|
|||
describe 'Remove page' do
|
||||
context 'when pages are deployed' do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
project.mark_pages_as_deployed
|
||||
end
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe 'Pipeline Schedules', :js do
|
|||
|
||||
context 'logged in as maintainer' do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
project.add_maintainer(user)
|
||||
gitlab_sign_in(user)
|
||||
end
|
||||
|
|
|
@ -317,6 +317,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
visit_project_pipelines
|
||||
end
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ RSpec.describe 'Project > Settings > Access Tokens', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe 'User searches project settings', :js do
|
|||
let_it_be(:project) { create(:project, :repository, namespace: user.namespace, pages_https_only: false) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ RSpec.describe 'Comments on personal snippets', :js do
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in user
|
||||
visit snippet_path(snippet)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ RSpec.describe 'User creates snippet', :js do
|
|||
let(:snippet_title_field) { 'snippet-title' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
sign_in(user)
|
||||
|
||||
visit new_snippet_path
|
||||
|
|
|
@ -72,6 +72,7 @@ RSpec.describe 'Triggers', :js do
|
|||
|
||||
describe 'trigger "Revoke" workflow' do
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import ConfirmModal from '~/lib/utils/confirm_via_gl_modal/confirm_modal.vue';
|
||||
|
||||
describe('Confirm Modal', () => {
|
||||
let wrapper;
|
||||
let modal;
|
||||
|
||||
const createComponent = ({ primaryText, primaryVariant } = {}) => {
|
||||
wrapper = mount(ConfirmModal, {
|
||||
propsData: {
|
||||
primaryText,
|
||||
primaryVariant,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
describe('Modal events', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
modal = findGlModal();
|
||||
});
|
||||
|
||||
it('should emit `confirmed` event on `primary` modal event', () => {
|
||||
findGlModal().vm.$emit('primary');
|
||||
expect(wrapper.emitted('confirmed')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should emit closed` event on `hidden` modal event', () => {
|
||||
modal.vm.$emit('hidden');
|
||||
expect(wrapper.emitted('closed')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom properties', () => {
|
||||
it('should pass correct custom primary text & button variant to the modal when provided', () => {
|
||||
const primaryText = "Let's do it!";
|
||||
const primaryVariant = 'danger';
|
||||
|
||||
createComponent({ primaryText, primaryVariant });
|
||||
const customProps = findGlModal().props('actionPrimary');
|
||||
expect(customProps.text).toBe(primaryText);
|
||||
expect(customProps.attributes.variant).toBe(primaryVariant);
|
||||
});
|
||||
|
||||
it('should pass default primary text & button variant to the modal if no custom values provided', () => {
|
||||
createComponent();
|
||||
const customProps = findGlModal().props('actionPrimary');
|
||||
expect(customProps.text).toBe('OK');
|
||||
expect(customProps.attributes.variant).toBe('confirm');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,9 +8,11 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import ContentEditor from '~/content_editor/components/content_editor.vue';
|
||||
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
|
||||
import {
|
||||
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
CONTENT_EDITOR_LOADED_ACTION,
|
||||
SAVED_USING_CONTENT_EDITOR_ACTION,
|
||||
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
WIKI_FORMAT_LABEL,
|
||||
WIKI_FORMAT_UPDATED_ACTION,
|
||||
} from '~/pages/shared/wikis/constants';
|
||||
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
|
@ -65,7 +67,6 @@ describe('WikiForm', () => {
|
|||
const pageInfoPersisted = {
|
||||
...pageInfoNew,
|
||||
persisted: true,
|
||||
|
||||
title: 'My page',
|
||||
content: ' My page content ',
|
||||
format: 'markdown',
|
||||
|
@ -177,7 +178,7 @@ describe('WikiForm', () => {
|
|||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain(titleHelpText);
|
||||
expect(findTitleHelpLink().attributes().href).toEqual(titleHelpLink);
|
||||
expect(findTitleHelpLink().attributes().href).toBe(titleHelpLink);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -186,7 +187,7 @@ describe('WikiForm', () => {
|
|||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findMarkdownHelpLink().attributes().href).toEqual(
|
||||
expect(findMarkdownHelpLink().attributes().href).toBe(
|
||||
'/help/user/markdown#wiki-specific-markdown',
|
||||
);
|
||||
});
|
||||
|
@ -220,8 +221,8 @@ describe('WikiForm', () => {
|
|||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not trigger tracking event', async () => {
|
||||
expect(trackingSpy).not.toHaveBeenCalled();
|
||||
it('triggers wiki format tracking event', async () => {
|
||||
expect(trackingSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not trim page content', () => {
|
||||
|
@ -273,7 +274,7 @@ describe('WikiForm', () => {
|
|||
({ persisted, redirectLink }) => {
|
||||
createWrapper(persisted);
|
||||
|
||||
expect(findCancelButton().attributes().href).toEqual(redirectLink);
|
||||
expect(findCancelButton().attributes().href).toBe(redirectLink);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -438,7 +439,7 @@ describe('WikiForm', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('triggers tracking event on form submit', async () => {
|
||||
it('triggers tracking events on form submit', async () => {
|
||||
triggerFormSubmit();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
@ -446,6 +447,15 @@ describe('WikiForm', () => {
|
|||
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
|
||||
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
|
||||
});
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
|
||||
label: WIKI_FORMAT_LABEL,
|
||||
value: findFormat().element.value,
|
||||
extra: {
|
||||
old_format: pageInfoPersisted.format,
|
||||
project_path: pageInfoPersisted.path,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates content from content editor on form submit', async () => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GlButton, GlDropdown, GlLoadingIcon, GlToggle } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
|
||||
|
@ -16,9 +16,9 @@ describe('ServiceDeskSetting', () => {
|
|||
const findTemplateDropdown = () => wrapper.find(GlDropdown);
|
||||
const findToggle = () => wrapper.find(GlToggle);
|
||||
|
||||
const createComponent = ({ props = {} } = {}) =>
|
||||
const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
|
||||
extendedWrapper(
|
||||
shallowMount(ServiceDeskSetting, {
|
||||
mountFunction(ServiceDeskSetting, {
|
||||
propsData: {
|
||||
isEnabled: true,
|
||||
...props,
|
||||
|
@ -128,6 +128,23 @@ describe('ServiceDeskSetting', () => {
|
|||
expect(input.exists()).toBe(true);
|
||||
expect(input.attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shows error when value contains uppercase or special chars', async () => {
|
||||
wrapper = createComponent({
|
||||
props: { customEmailEnabled: true },
|
||||
mountFunction: mount,
|
||||
});
|
||||
|
||||
const input = wrapper.findByTestId('project-suffix');
|
||||
|
||||
input.setValue('abc_A.');
|
||||
input.trigger('blur');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const errorText = wrapper.find('.text-danger');
|
||||
expect(errorText.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('customEmail is the same as incomingEmail', () => {
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
import { GlBadge } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import RunnerTag from '~/runner/components/runner_tag.vue';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
|
||||
const mockTag = 'tag1';
|
||||
|
||||
describe('RunnerTag', () => {
|
||||
let wrapper;
|
||||
|
||||
const findBadge = () => wrapper.findComponent(GlBadge);
|
||||
const getTooltipValue = () => getBinding(findBadge().element, 'gl-tooltip').value;
|
||||
|
||||
const setDimensions = ({ scrollWidth, offsetWidth }) => {
|
||||
jest.spyOn(findBadge().element, 'scrollWidth', 'get').mockReturnValue(scrollWidth);
|
||||
jest.spyOn(findBadge().element, 'offsetWidth', 'get').mockReturnValue(offsetWidth);
|
||||
|
||||
// Mock trigger resize
|
||||
getBinding(findBadge().element, 'gl-resize-observer').value();
|
||||
};
|
||||
|
||||
const createComponent = ({ props = {} } = {}) => {
|
||||
wrapper = shallowMount(RunnerTag, {
|
||||
propsData: {
|
||||
tag: 'tag1',
|
||||
tag: mockTag,
|
||||
...props,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
GlResizeObserver: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -25,21 +42,36 @@ describe('RunnerTag', () => {
|
|||
});
|
||||
|
||||
it('Displays tag text', () => {
|
||||
expect(wrapper.text()).toBe('tag1');
|
||||
expect(wrapper.text()).toBe(mockTag);
|
||||
});
|
||||
|
||||
it('Displays tags with correct style', () => {
|
||||
expect(findBadge().props()).toMatchObject({
|
||||
size: 'md',
|
||||
variant: 'info',
|
||||
size: 'sm',
|
||||
variant: 'neutral',
|
||||
});
|
||||
});
|
||||
|
||||
it('Displays tags with small size', () => {
|
||||
it('Displays tags with md size', () => {
|
||||
createComponent({
|
||||
props: { size: 'sm' },
|
||||
props: { size: 'md' },
|
||||
});
|
||||
|
||||
expect(findBadge().props('size')).toBe('sm');
|
||||
expect(findBadge().props('size')).toBe('md');
|
||||
});
|
||||
|
||||
it.each`
|
||||
case | scrollWidth | offsetWidth | expectedTooltip
|
||||
${'overflowing'} | ${110} | ${100} | ${mockTag}
|
||||
${'not overflowing'} | ${90} | ${100} | ${''}
|
||||
${'almost overflowing'} | ${100} | ${100} | ${''}
|
||||
`(
|
||||
'Sets "$expectedTooltip" as tooltip when $case',
|
||||
async ({ scrollWidth, offsetWidth, expectedTooltip }) => {
|
||||
setDimensions({ scrollWidth, offsetWidth });
|
||||
await nextTick();
|
||||
|
||||
expect(getTooltipValue()).toBe(expectedTooltip);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -33,16 +33,16 @@ describe('RunnerTags', () => {
|
|||
});
|
||||
|
||||
it('Displays tags with correct style', () => {
|
||||
expect(findBadge().props('size')).toBe('md');
|
||||
expect(findBadge().props('variant')).toBe('info');
|
||||
expect(findBadge().props('size')).toBe('sm');
|
||||
expect(findBadge().props('variant')).toBe('neutral');
|
||||
});
|
||||
|
||||
it('Displays tags with small size', () => {
|
||||
it('Displays tags with md size', () => {
|
||||
createComponent({
|
||||
props: { size: 'sm' },
|
||||
props: { size: 'md' },
|
||||
});
|
||||
|
||||
expect(findBadge().props('size')).toBe('sm');
|
||||
expect(findBadge().props('size')).toBe('md');
|
||||
});
|
||||
|
||||
it('Is empty when there are no tags', () => {
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import initMRPopovers from '~/mr_popover/index';
|
||||
import createStore from '~/notes/stores';
|
||||
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
jest.mock('~/mr_popover/index', () => jest.fn());
|
||||
|
||||
describe('system note component', () => {
|
||||
let vm;
|
||||
let props;
|
||||
let mock;
|
||||
|
||||
function createComponent(propsData = {}) {
|
||||
const store = createStore();
|
||||
store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
|
||||
|
||||
vm = mount(IssueSystemNote, {
|
||||
store,
|
||||
propsData,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
|
@ -27,28 +41,29 @@ describe('system note component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const store = createStore();
|
||||
store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
|
||||
|
||||
vm = mount(IssueSystemNote, {
|
||||
store,
|
||||
propsData: props,
|
||||
});
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.destroy();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('should render a list item with correct id', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(vm.attributes('id')).toEqual(`note_${props.note.id}`);
|
||||
});
|
||||
|
||||
it('should render target class is note is target note', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(vm.classes()).toContain('target');
|
||||
});
|
||||
|
||||
it('should render svg icon', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(vm.find('.timeline-icon svg').exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -56,10 +71,31 @@ describe('system note component', () => {
|
|||
// we need to strip them because they break layout of commit lists in system notes:
|
||||
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
|
||||
it('removes wrapping paragraph from note HTML', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(vm.find('.system-note-message').html()).toContain('<span>closed</span>');
|
||||
});
|
||||
|
||||
it('should initMRPopovers onMount', () => {
|
||||
createComponent(props);
|
||||
|
||||
expect(initMRPopovers).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders outdated code lines', async () => {
|
||||
mock
|
||||
.onGet('/outdated_line_change_path')
|
||||
.reply(200, [
|
||||
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
|
||||
]);
|
||||
|
||||
createComponent({
|
||||
note: { ...props.note, outdated_line_change_path: '/outdated_line_change_path' },
|
||||
});
|
||||
|
||||
await vm.find("[data-testid='outdated-lines-change-btn']").trigger('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(vm.find("[data-testid='outdated-lines']").exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ResolvesGroups do
|
||||
include GraphqlHelpers
|
||||
include AfterNextHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:groups) { create_pair(:group) }
|
||||
|
||||
let_it_be(:resolver) do
|
||||
Class.new(Resolvers::BaseResolver) do
|
||||
include ResolvesGroups
|
||||
type Types::GroupType, null: true
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:query_type) do
|
||||
query_factory do |query|
|
||||
query.field :groups,
|
||||
Types::GroupType.connection_type,
|
||||
null: true,
|
||||
resolver: resolver
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:lookahead_fields) do
|
||||
<<~FIELDS
|
||||
contacts { nodes { id } }
|
||||
containerRepositoriesCount
|
||||
customEmoji { nodes { id } }
|
||||
fullPath
|
||||
organizations { nodes { id } }
|
||||
path
|
||||
dependencyProxyBlobCount
|
||||
dependencyProxyBlobs { nodes { fileName } }
|
||||
dependencyProxyImageCount
|
||||
dependencyProxyImageTtlPolicy { enabled }
|
||||
dependencyProxySetting { enabled }
|
||||
FIELDS
|
||||
end
|
||||
|
||||
it 'avoids N+1 queries on the fields marked with lookahead' do
|
||||
group_ids = groups.map(&:id)
|
||||
|
||||
allow_next(resolver).to receive(:resolve_groups).and_return(Group.id_in(group_ids))
|
||||
# Prevent authorization queries from affecting the test.
|
||||
allow(Ability).to receive(:allowed?).and_return(true)
|
||||
|
||||
single_group_query = ActiveRecord::QueryRecorder.new do
|
||||
data = query_groups(limit: 1)
|
||||
expect(data.size).to eq(1)
|
||||
end
|
||||
|
||||
multi_group_query = -> {
|
||||
data = query_groups(limit: 2)
|
||||
expect(data.size).to eq(2)
|
||||
}
|
||||
|
||||
expect { multi_group_query.call }.not_to exceed_query_limit(single_group_query)
|
||||
end
|
||||
|
||||
def query_groups(limit:)
|
||||
query_string = "{ groups(first: #{limit}) { nodes { id #{lookahead_fields} } } }"
|
||||
|
||||
data = execute_query(query_type, graphql: query_string)
|
||||
|
||||
graphql_dig_at(data, :data, :groups, :nodes)
|
||||
end
|
||||
end
|
|
@ -358,8 +358,6 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
|
|||
end
|
||||
|
||||
describe '#sort_and_expand_all' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
context 'table tests' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
@ -550,7 +548,7 @@ RSpec.describe Gitlab::Ci::Variables::Collection do
|
|||
with_them do
|
||||
let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
|
||||
|
||||
subject { collection.sort_and_expand_all(project, keep_undefined: keep_undefined) }
|
||||
subject { collection.sort_and_expand_all(keep_undefined: keep_undefined) }
|
||||
|
||||
it 'returns Collection' do
|
||||
is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection)
|
||||
|
|
|
@ -581,13 +581,16 @@ RSpec.describe Gitlab::Diff::PositionTracer::LineStrategy, :clean_gitlab_redis_c
|
|||
)
|
||||
end
|
||||
|
||||
it "returns the new position but drops line_range information" do
|
||||
it "returns the new position" do
|
||||
expect_change_position(
|
||||
old_path: file_name,
|
||||
new_path: file_name,
|
||||
old_line: nil,
|
||||
new_line: 2,
|
||||
line_range: nil
|
||||
line_range: {
|
||||
"start_line_code" => 1,
|
||||
"end_line_code" => 2
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,6 +37,8 @@ RSpec.describe Group do
|
|||
it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') }
|
||||
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) }
|
||||
it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') }
|
||||
it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') }
|
||||
it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') }
|
||||
|
||||
describe '#members & #requesters' do
|
||||
let(:requester) { create(:user) }
|
||||
|
|
|
@ -18,6 +18,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
gitlab_sign_in(user)
|
||||
user.update_attribute(:otp_required_for_login, true)
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue