Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-11 15:10:41 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-11 15:10:41 +0300
commit1c7411c597334e20d2e92cc948f0699d339d2710 (patch)
treee88e76f1f563b71a4b9113373f24849bc5d1d79e /app
parent3e9023894d319cf56b7b844910953df19ca010b1 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue47
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js47
-rw-r--r--app/assets/javascripts/lib/utils/rails_ujs.js38
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue14
-rw-r--r--app/assets/javascripts/pages/shared/wikis/constants.js5
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue20
-rw-r--r--app/assets/javascripts/runner/components/runner_tag.vue35
-rw-r--r--app/assets/javascripts/runner/components/runner_tags.vue10
-rw-r--r--app/assets/javascripts/runner/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue65
-rw-r--r--app/controllers/projects/notes_controller.rb8
-rw-r--r--app/graphql/resolvers/concerns/resolves_groups.rb34
-rw-r--r--app/graphql/resolvers/groups_resolver.rb12
-rw-r--r--app/graphql/resolvers/users/groups_resolver.rb15
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/models/group.rb11
-rw-r--r--app/presenters/ci/build_runner_presenter.rb2
-rw-r--r--app/serializers/note_entity.rb4
-rw-r--r--app/services/merge_requests/outdated_discussion_diff_lines_service.rb55
19 files changed, 391 insertions, 39 deletions
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
+ >
&middot;
<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