diff options
79 files changed, 759 insertions, 83 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 87da2143dd2..27fb81c9ced 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9de3dd28a5c8248903160ea35d9f718899f51c89 +4892c8502cc45217903a8a584a7b5edb15edf86e diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue new file mode 100644 index 00000000000..733d0f69f5d --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -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> diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js new file mode 100644 index 00000000000..f0908a60ac5 --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -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(); + }); +} diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js index 8b40cc7bd11..6b1985a23ba 100644 --- a/app/assets/javascripts/lib/utils/rails_ujs.js +++ b/app/assets/javascripts/lib/utils/rails_ujs.js @@ -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 diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index 2ce1f0366c1..6f19a9f4379 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -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; }, diff --git a/app/assets/javascripts/pages/shared/wikis/constants.js b/app/assets/javascripts/pages/shared/wikis/constants.js index b358ac9cf52..94d086158f1 100644 --- a/app/assets/javascripts/pages/shared/wikis/constants.js +++ b/app/assets/javascripts/pages/shared/wikis/constants.js @@ -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'; diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index d964a701b08..b8053bf9ab5 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -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"> diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue index 06562e618a8..6ad2023a866 100644 --- a/app/assets/javascripts/runner/components/runner_tag.vue +++ b/app/assets/javascripts/runner/components/runner_tag.vue @@ -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> diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue index aec0d8e2c66..8da5e33076f 100644 --- a/app/assets/javascripts/runner/components/runner_tags.vue +++ b/app/assets/javascripts/runner/components/runner_tags.vue @@ -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> diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index c0b256ec7fa..3952e2398e0 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -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 diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 755e6f1f224..8877cfa39fb 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -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> diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1eb4f6f1b7a..e8057308386 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -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 diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb new file mode 100644 index 00000000000..c451d4e7936 --- /dev/null +++ b/app/graphql/resolvers/concerns/resolves_groups.rb @@ -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 diff --git a/app/graphql/resolvers/groups_resolver.rb b/app/graphql/resolvers/groups_resolver.rb index b090fdc49d4..abd3bf9e6e0 100644 --- a/app/graphql/resolvers/groups_resolver.rb +++ b/app/graphql/resolvers/groups_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/users/groups_resolver.rb b/app/graphql/resolvers/users/groups_resolver.rb index 0899b08e19c..0c1cdc70163 100644 --- a/app/graphql/resolvers/users/groups_resolver.rb +++ b/app/graphql/resolvers/users/groups_resolver.rb @@ -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 diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index b8f382681f3..4a20d84f2ab 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 15eb2ee81a6..2dd20300ad2 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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 diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 27d7dac2182..8e1b675d051 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -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 diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 8308e954c06..8f189f14dea 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -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 diff --git a/app/services/merge_requests/outdated_discussion_diff_lines_service.rb b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb new file mode 100644 index 00000000000..de06f18dd82 --- /dev/null +++ b/app/services/merge_requests/outdated_discussion_diff_lines_service.rb @@ -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 diff --git a/config/feature_flags/development/bootstrap_confirmation_modals.yml b/config/feature_flags/development/bootstrap_confirmation_modals.yml new file mode 100644 index 00000000000..e67fd03fea6 --- /dev/null +++ b/config/feature_flags/development/bootstrap_confirmation_modals.yml @@ -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 diff --git a/config/feature_flags/development/display_outdated_line_diff.yml b/config/feature_flags/development/display_outdated_line_diff.yml new file mode 100644 index 00000000000..6baa258c52b --- /dev/null +++ b/config/feature_flags/development/display_outdated_line_diff.yml @@ -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 diff --git a/config/routes/project.rb b/config/routes/project.rb index ec399573f57..7f9b2cc4fbf 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -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 diff --git a/doc/administration/postgresql/index.md b/doc/administration/postgresql/index.md index fb0fa9ae7b6..15425a6d9f2 100644 --- a/doc/administration/postgresql/index.md +++ b/doc/administration/postgresql/index.md @@ -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) diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index bdffde4df60..27f482ee2ba 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.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). diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 21238068743..72837b8ec22 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -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 diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb index 544c54d8aa8..a00c1da97ea 100644 --- a/lib/gitlab/ci/variables/collection.rb +++ b/lib/gitlab/ci/variables/collection.rb @@ -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? diff --git a/lib/gitlab/diff/position_tracer/line_strategy.rb b/lib/gitlab/diff/position_tracer/line_strategy.rb index 8bacc781f61..0f0b8f0c4f3 100644 --- a/lib/gitlab/diff/position_tracer/line_strategy.rb +++ b/lib/gitlab/diff/position_tracer/line_strategy.rb @@ -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 diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 6faf3575bc1..027551df15d 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -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. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 16d4c22a261..25673097f38 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index d92862f0ca3..66af546b113 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -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 diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb index 1f34c4ed17c..f65e85b4cb6 100644 --- a/spec/features/admin/admin_disables_two_factor_spec.rb +++ b/spec/features/admin/admin_disables_two_factor_spec.rb @@ -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) diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 8315b8f44b0..8d4e7a7442c 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -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) diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index a501efd82ed..32e4d18227e 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -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 diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index 08d81906d9f..65de1160cfd 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -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 diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb index ed8ea84fbf8..6643ebe82e6 100644 --- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -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" } diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index 0e448446085..c13313609b5 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -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 diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb index 624bfde7359..73477fb93dd 100644 --- a/spec/features/admin/users/user_spec.rb +++ b/spec/features/admin/users/user_spec.rb @@ -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 diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index 119b01ff552..3968b055819 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -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 diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 9a5b5bbfc34..2f21961d1fc 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -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) diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb index b73313745e9..e6bf1ffc2f7 100644 --- a/spec/features/groups/members/leave_group_spec.rb +++ b/spec/features/groups/members/leave_group_spec.rb @@ -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 diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 54c3fe738d2..f9b554c5ed2 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -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) diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index c339a7d9976..dcd289c7627 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -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 diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index 83d9388914b..0416474218f 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -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 diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index 90cdc28d1bd..64cd5aa2bb1 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -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 diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index 873cc0a89c6..345404cc28f 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -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) diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index 9a261c6d9c8..7d935298f38 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -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 diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb index fd64704b7c8..a515c7b1c1f 100644 --- a/spec/features/profiles/active_sessions_spec.rb +++ b/spec/features/profiles/active_sessions_spec.rb @@ -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) diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb index 2735f601307..6827dff5434 100644 --- a/spec/features/profiles/oauth_applications_spec.rb +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -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 diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 8f44299b18f..74505633cae 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -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 diff --git a/spec/features/projects/branches/user_deletes_branch_spec.rb b/spec/features/projects/branches/user_deletes_branch_spec.rb index 3b8f49accc5..8fc5c3d2e1b 100644 --- a/spec/features/projects/branches/user_deletes_branch_spec.rb +++ b/spec/features/projects/branches/user_deletes_branch_spec.rb @@ -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 diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index 0a79719f14a..2725c6a91be 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -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') diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb index 431cbb4ffbb..67d3276fc14 100644 --- a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb +++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb @@ -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) diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb index 6997c2d8338..b0be6edb245 100644 --- a/spec/features/projects/commit/user_comments_on_commit_spec.rb +++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb @@ -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) diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 2404fbf01ca..3b83c25b629 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -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) diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index 5d7a761cb17..12e88bbf6a5 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -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) diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb index c4bd0b81dc0..4881a7bdf1a 100644 --- a/spec/features/projects/members/member_leaves_project_spec.rb +++ b/spec/features/projects/members/member_leaves_project_spec.rb @@ -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 diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 113ba692497..dcaef5f4ef0 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -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 diff --git a/spec/features/projects/pages/user_adds_domain_spec.rb b/spec/features/projects/pages/user_adds_domain_spec.rb index de9effe3dc7..06f130ae69c 100644 --- a/spec/features/projects/pages/user_adds_domain_spec.rb +++ b/spec/features/projects/pages/user_adds_domain_spec.rb @@ -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 diff --git a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb index cf8438d5e6f..a3fc5804e13 100644 --- a/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb +++ b/spec/features/projects/pages/user_edits_lets_encrypt_settings_spec.rb @@ -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) diff --git a/spec/features/projects/pages/user_edits_settings_spec.rb b/spec/features/projects/pages/user_edits_settings_spec.rb index 71d4cce2784..1226e1dc2ed 100644 --- a/spec/features/projects/pages/user_edits_settings_spec.rb +++ b/spec/features/projects/pages/user_edits_settings_spec.rb @@ -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 diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index 94e3331b173..9df430c0f78 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -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 diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 3a60087818d..e38c4989f26 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -317,6 +317,7 @@ RSpec.describe 'Pipelines', :js do end before do + stub_feature_flags(bootstrap_confirmation_modals: false) visit_project_pipelines end diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb index 4941b936c0c..d8de9e0449e 100644 --- a/spec/features/projects/settings/access_tokens_spec.rb +++ b/spec/features/projects/settings/access_tokens_spec.rb @@ -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 diff --git a/spec/features/projects/settings/user_searches_in_settings_spec.rb b/spec/features/projects/settings/user_searches_in_settings_spec.rb index 7ed96d01189..44b5464a1b0 100644 --- a/spec/features/projects/settings/user_searches_in_settings_spec.rb +++ b/spec/features/projects/settings/user_searches_in_settings_spec.rb @@ -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 diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index fc88cd9205c..6bd31d7314c 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -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) diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index ca050daa62a..82fe895d397 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -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 diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 6fa805d8c74..2ddd86dd807 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -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 diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js new file mode 100644 index 00000000000..d19f9352bbc --- /dev/null +++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js @@ -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'); + }); + }); +}); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js index 082a8977710..9d510b3d231 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -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 () => { diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js index a5807ac588c..0fd3e7446da 100644 --- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js @@ -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', () => { diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js index dda318f8153..bd05d4b2cfe 100644 --- a/spec/frontend/runner/components/runner_tag_spec.js +++ b/spec/frontend/runner/components/runner_tag_spec.js @@ -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); + }, + ); }); diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js index b6487ade0d6..da89a659432 100644 --- a/spec/frontend/runner/components/runner_tags_spec.js +++ b/spec/frontend/runner/components/runner_tags_spec.js @@ -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', () => { diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js index 48dacc50923..65f79bab005 100644 --- a/spec/frontend/vue_shared/components/notes/system_note_spec.js +++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js @@ -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); + }); }); diff --git a/spec/graphql/resolvers/concerns/resolves_groups_spec.rb b/spec/graphql/resolvers/concerns/resolves_groups_spec.rb new file mode 100644 index 00000000000..bfbbae29e92 --- /dev/null +++ b/spec/graphql/resolvers/concerns/resolves_groups_spec.rb @@ -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 diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 6e3ed27bb44..26c560565e0 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -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) diff --git a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb index bdeaabec1f1..b646cf38178 100644 --- a/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer/line_strategy_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 2467ef541b2..ae6513a3bc1 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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) } diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb index ddc03e178ba..94c91556ea7 100644 --- a/spec/support/shared_examples/features/2fa_shared_examples.rb +++ b/spec/support/shared_examples/features/2fa_shared_examples.rb @@ -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 |