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-16 12:13:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-16 12:13:21 +0300
commit2c90b9b579fbfe3db191a032d2cb176761605a02 (patch)
treed9819280a1ec64ff82c31ce6081e00745a9648b4 /app
parentccca6cec346d169fa2521c390760af9bd885ea77 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue21
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue35
-rw-r--r--app/assets/javascripts/boards/index.js4
-rw-r--r--app/assets/javascripts/boards/mount_filtered_search_issue_boards.js3
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/stores/actions.js41
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js41
-rw-r--r--app/controllers/concerns/issuable_actions.rb25
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/helpers/notes_helper.rb6
-rw-r--r--app/models/concerns/noteable.rb33
-rw-r--r--app/models/note.rb1
-rw-r--r--app/presenters/projects/security/configuration_presenter.rb100
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb12
-rw-r--r--app/services/resource_events/synthetic_label_notes_builder_service.rb4
-rw-r--r--app/services/resource_events/synthetic_milestone_notes_builder_service.rb4
-rw-r--r--app/services/resource_events/synthetic_state_notes_builder_service.rb4
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml4
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
21 files changed, 220 insertions, 131 deletions
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 86f512a5117..6e6ada2d109 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -42,6 +42,7 @@ export default {
types,
weight,
epicId,
+ myReactionEmoji,
} = this.filterParams;
const filteredSearchValue = [];
@@ -89,6 +90,13 @@ export default {
});
}
+ if (myReactionEmoji) {
+ filteredSearchValue.push({
+ type: 'my_reaction_emoji',
+ value: { data: myReactionEmoji, operator: '=' },
+ });
+ }
+
if (epicId) {
filteredSearchValue.push({
type: 'epic_id',
@@ -147,6 +155,13 @@ export default {
});
}
+ if (this.filterParams['not[myReactionEmoji]']) {
+ filteredSearchValue.push({
+ type: 'my_reaction_emoji',
+ value: { data: this.filterParams['not[myReactionEmoji]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
@@ -163,6 +178,7 @@ export default {
types,
weight,
epicId,
+ myReactionEmoji,
} = this.filterParams;
let notParams = {};
@@ -177,6 +193,7 @@ export default {
'not[milestone_title]': this.filterParams.not.milestoneTitle,
'not[weight]': this.filterParams.not.weight,
'not[epic_id]': this.filterParams.not.epicId,
+ 'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
},
undefined,
);
@@ -192,6 +209,7 @@ export default {
types,
weight,
epic_id: getIdFromGraphQLId(epicId),
+ my_reaction_emoji: myReactionEmoji,
};
},
},
@@ -249,6 +267,9 @@ export default {
case 'epic_id':
filterParams.epicId = filter.value.data;
break;
+ case 'my_reaction_emoji':
+ filterParams.myReactionEmoji = filter.value.data;
+ break;
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index 7af0de2f231..dd4b2b23efc 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -1,14 +1,20 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapActions } from 'vuex';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import { BoardType } from '~/boards/constants';
+import axios from '~/lib/utils/axios_utils';
import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ DEFAULT_MILESTONES_GRAPHQL,
+ TOKEN_TITLE_MY_REACTION,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
@@ -33,6 +39,7 @@ export default {
isNot: __('is not'),
},
components: { BoardFilteredSearch },
+ inject: ['isSignedIn'],
props: {
fullPath: {
type: String,
@@ -113,6 +120,32 @@ export default {
symbol: '~',
fetchLabels,
},
+ ...(this.isSignedIn
+ ? [
+ {
+ type: 'my_reaction_emoji',
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ fetchEmojis: (search = '') => {
+ // TODO: Switch to GraphQL query when backend is ready: https://gitlab.com/gitlab-org/gitlab/-/issues/339694
+ return axios
+ .get(`${gon.relative_url_root || ''}/-/autocomplete/award_emojis`)
+ .then(({ data }) => {
+ if (search) {
+ return {
+ data: fuzzaldrinPlus.filter(data, search, {
+ key: ['name'],
+ }),
+ };
+ }
+ return { data };
+ });
+ },
+ },
+ ]
+ : []),
{
type: 'milestone_title',
title: milestone,
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 66b0562d02d..6fa8dd63245 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -13,7 +13,7 @@ import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
import toggleFocusMode from '~/boards/toggle_focus';
-import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
+import { NavigationType, isLoggedIn, parseBoolean } from '~/lib/utils/common_utils';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
import initNewBoard from './new_board';
@@ -110,7 +110,7 @@ export default () => {
});
if (gon?.features?.issueBoardsFilteredSearch) {
- initBoardsFilteredSearch(apolloProvider, parseBoolean($boardApp.dataset.epicFeatureAvailable));
+ initBoardsFilteredSearch(apolloProvider, isLoggedIn());
}
mountBoardApp($boardApp);
diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
index d55cd4566aa..1ea74d5685c 100644
--- a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
+++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
@@ -4,7 +4,7 @@ import store from '~/boards/stores';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
-export default (apolloProvider) => {
+export default (apolloProvider, isSignedIn) => {
const el = document.getElementById('js-issue-board-filtered-search');
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
@@ -20,6 +20,7 @@ export default (apolloProvider) => {
el,
provide: {
initialFilterParams,
+ isSignedIn,
},
store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
apolloProvider,
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 9b17ad8e178..3ab3e7a20d4 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -98,15 +98,17 @@ export default {
return this.noteableData.noteableType;
},
allDiscussions() {
+ let skeletonNotes = [];
+
if (this.renderSkeleton || this.isLoading) {
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
- return new Array(prerenderedNotesCount).fill({
+ skeletonNotes = new Array(prerenderedNotesCount).fill({
isSkeletonNote: true,
});
}
- return this.discussions;
+ return this.discussions.concat(skeletonNotes);
},
canReply() {
return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled;
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 935494dc193..c862a29ad9c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -70,7 +70,7 @@ export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, dat
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, discussions) =>
- commit(types.SET_INITIAL_DISCUSSIONS, discussions);
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, discussions);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
@@ -89,14 +89,51 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi
? { params: { notes_filter: filter, persist_filter: persistFilter } }
: null;
+ if (window.gon?.features?.paginatedIssueDiscussions) {
+ return dispatch('fetchDiscussionsBatch', { path, config, perPage: 20 });
+ }
+
return axios.get(path, config).then(({ data }) => {
- commit(types.SET_INITIAL_DISCUSSIONS, data);
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
commit(types.SET_FETCHING_DISCUSSIONS, false);
dispatch('updateResolvableDiscussionsCounts');
});
};
+export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, cursor, perPage }) => {
+ const params = { ...config?.params, per_page: perPage };
+
+ if (cursor) {
+ params.cursor = cursor;
+ }
+
+ return axios.get(path, { params }).then(({ data, headers }) => {
+ commit(types.ADD_OR_UPDATE_DISCUSSIONS, data);
+
+ if (headers['x-next-page-cursor']) {
+ const nextConfig = { ...config };
+
+ if (config?.params?.persist_filter) {
+ delete nextConfig.params.notes_filter;
+ delete nextConfig.params.persist_filter;
+ }
+
+ return dispatch('fetchDiscussionsBatch', {
+ path,
+ config: nextConfig,
+ cursor: headers['x-next-page-cursor'],
+ perPage: Math.min(Math.round(perPage * 1.5), 100),
+ });
+ }
+
+ commit(types.SET_FETCHING_DISCUSSIONS, false);
+ dispatch('updateResolvableDiscussionsCounts');
+
+ return undefined;
+ });
+};
+
export const updateDiscussion = ({ commit, state }, discussion) => {
commit(types.UPDATE_DISCUSSION, discussion);
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 2e8b728e013..fcd2846ff0d 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -1,11 +1,11 @@
export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
+export const ADD_OR_UPDATE_DISCUSSIONS = 'ADD_OR_UPDATE_DISCUSSIONS';
export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA';
-export const SET_INITIAL_DISCUSSIONS = 'SET_INITIAL_DISCUSSIONS';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index c5fa34dfedd..1a99750ddb3 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -129,8 +129,8 @@ export default {
Object.assign(state, { userData: data });
},
- [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) {
- const discussions = discussionsData.reduce((acc, d) => {
+ [types.ADD_OR_UPDATE_DISCUSSIONS](state, discussionsData) {
+ discussionsData.forEach((d) => {
const discussion = { ...d };
const diffData = {};
@@ -145,27 +145,38 @@ export default {
// To support legacy notes, should be very rare case.
if (discussion.individual_note && discussion.notes.length > 1) {
discussion.notes.forEach((n) => {
- acc.push({
+ const newDiscussion = {
...discussion,
...diffData,
notes: [n], // override notes array to only have one item to mimick individual_note
- });
+ };
+ const oldDiscussion = state.discussions.find(
+ (existingDiscussion) =>
+ existingDiscussion.id === discussion.id && existingDiscussion.notes[0].id === n.id,
+ );
+
+ if (oldDiscussion) {
+ state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, newDiscussion);
+ } else {
+ state.discussions.push(newDiscussion);
+ }
});
} else {
- const oldNote = utils.findNoteObjectById(state.discussions, discussion.id);
+ const oldDiscussion = utils.findNoteObjectById(state.discussions, discussion.id);
- acc.push({
- ...discussion,
- ...diffData,
- expanded: oldNote ? oldNote.expanded : discussion.expanded,
- });
+ if (oldDiscussion) {
+ state.discussions.splice(state.discussions.indexOf(oldDiscussion), 1, {
+ ...discussion,
+ ...diffData,
+ expanded: oldDiscussion.expanded,
+ });
+ } else {
+ state.discussions.push({ ...discussion, ...diffData });
+ }
}
-
- return acc;
- }, []);
-
- Object.assign(state, { discussions });
+ });
},
+
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt });
},
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 8a9b66045b8..2d7fbb78209 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -149,8 +149,20 @@ module IssuableActions
.includes(:noteable)
.fresh
+ if paginated_discussions
+ paginated_discussions_by_type = paginated_discussions.records.group_by(&:table_name)
+
+ notes = if paginated_discussions_by_type['notes'].present?
+ notes.with_discussion_ids(paginated_discussions_by_type['notes'].map(&:discussion_id))
+ else
+ notes.none
+ end
+
+ response.headers['X-Next-Page-Cursor'] = paginated_discussions.cursor_for_next_page if paginated_discussions.has_next_page?
+ end
+
if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
- notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
+ notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user, paginated_notes: paginated_discussions_by_type).execute(notes)
end
notes = prepare_notes_for_rendering(notes)
@@ -170,6 +182,17 @@ module IssuableActions
private
+ def paginated_discussions
+ return if params[:per_page].blank?
+ return unless issuable.instance_of?(Issue) && Feature.enabled?(:paginated_issue_discussions, project, default_enabled: :yaml)
+
+ strong_memoize(:paginated_discussions) do
+ issuable
+ .discussion_root_note_ids(notes_filter: notes_filter)
+ .keyset_paginate(cursor: params[:cursor], per_page: params[:per_page].to_i)
+ end
+ end
+
def notes_filter
strong_memoize(:notes_filter) do
notes_filter_param = params[:notes_filter]&.to_i
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 1968fa81b29..e607346b40e 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -66,7 +66,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
render 'create'
else
- @error = _('Invalid pin code')
+ @error = { message: _('Invalid pin code.') }
@qr_code = build_qr_code
if Feature.enabled?(:webauthn)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 781c63e32f5..853e9c7ccdd 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
experiment_instance.exclude! unless helpers.can_admin_project_member?(@project)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 003c971af8d..2dadaa0be0a 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -167,11 +167,11 @@ module NotesHelper
}
end
- def discussions_path(issuable)
+ def discussions_path(issuable, **params)
if issuable.is_a?(Issue)
- discussions_project_issue_path(@project, issuable, format: :json)
+ discussions_project_issue_path(@project, issuable, params.merge(format: :json))
else
- discussions_project_merge_request_path(@project, issuable, format: :json)
+ discussions_project_merge_request_path(@project, issuable, params.merge(format: :json))
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index f6d4e5bd27b..ea4fe5b27dc 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -98,6 +98,27 @@ module Noteable
.order('MIN(created_at), MIN(id)')
end
+ # This does not consider OutOfContextDiscussions in MRs
+ # where notes from commits are overriden so that they have
+ # the same discussion_id
+ def discussion_root_note_ids(notes_filter:)
+ relations = []
+
+ relations << discussion_notes.select(
+ "'notes' AS table_name",
+ 'discussion_id',
+ 'MIN(id) AS id',
+ 'MIN(created_at) AS created_at'
+ ).with_notes_filter(notes_filter)
+ .group(:discussion_id)
+
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ relations += synthetic_note_ids_relations
+ end
+
+ Note.from_union(relations, remove_duplicates: false).fresh
+ end
+
def capped_notes_count(max)
notes.limit(max).count
end
@@ -179,6 +200,18 @@ module Noteable
project_email.sub('@', "-#{iid}@")
end
+
+ private
+
+ # Synthetic system notes don't have discussion IDs because these are generated dynamically
+ # in Ruby. These are always root notes anyway so we don't need to group by discussion ID.
+ def synthetic_note_ids_relations
+ [
+ resource_label_events.select("'resource_label_events'", "'NULL'", :id, :created_at),
+ resource_milestone_events.select("'resource_milestone_events'", "'NULL'", :id, :created_at),
+ resource_state_events.select("'resource_state_events'", "'NULL'", :id, :created_at)
+ ]
+ end
end
Noteable.extend(Noteable::ClassMethods)
diff --git a/app/models/note.rb b/app/models/note.rb
index 37473518892..cb285028203 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -114,6 +114,7 @@ class Note < ApplicationRecord
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
scope :with_updated_at, ->(time) { where(updated_at: time) }
+ scope :with_discussion_ids, ->(discussion_ids) { where(discussion_id: discussion_ids) }
scope :with_suggestions, -> { joins(:suggestions) }
scope :inc_author, -> { includes(:author) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
deleted file mode 100644
index 8f8f5bea0b6..00000000000
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-module Projects
- module Security
- class ConfigurationPresenter < Gitlab::View::Presenter::Delegated
- include AutoDevopsHelper
- include ::Security::LatestPipelineInformation
-
- delegator_override_with Gitlab::Utils::StrongMemoize
-
- presents ::Project, as: :project
-
- def to_h
- {
- auto_devops_enabled: auto_devops_source?,
- auto_devops_help_page_path: help_page_path('topics/autodevops/index'),
- auto_devops_path: auto_devops_settings_path(project),
- can_enable_auto_devops: can_enable_auto_devops?,
- features: features,
- help_page_path: help_page_path('user/application_security/index'),
- latest_pipeline_path: latest_pipeline_path,
- # TODO: gitlab_ci_present will incorrectly report `false` if the CI/CD configuration file name
- # has been customized and a file with the given custom name exists in the repo. This edge case
- # will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/342465
- gitlab_ci_present: project.repository.gitlab_ci_yml.present?,
- gitlab_ci_history_path: gitlab_ci_history_path,
- auto_fix_enabled: autofix_enabled,
- can_toggle_auto_fix_settings: can_toggle_autofix,
- auto_fix_user_path: auto_fix_user_path
- }
- end
-
- def to_html_data_attribute
- data = to_h
- data[:features] = data[:features].to_json
- data[:auto_fix_enabled] = data[:auto_fix_enabled].to_json
-
- data
- end
-
- private
-
- def autofix_enabled; end
-
- def auto_fix_user_path; end
-
- def can_enable_auto_devops?
- feature_available?(:builds, current_user) &&
- can?(current_user, :admin_project, self) &&
- !archived?
- end
-
- def can_toggle_autofix; end
-
- def gitlab_ci_history_path
- return '' if project.empty_repo?
-
- gitlab_ci = ::Gitlab::FileDetector::PATTERNS[:gitlab_ci]
- ::Gitlab::Routing.url_helpers.project_blame_path(project, File.join(project.default_branch_or_main, gitlab_ci))
- end
-
- def features
- scans = scan_types.map do |scan_type|
- scan(scan_type, configured: scanner_enabled?(scan_type))
- end
-
- # These scans are "fake" (non job) entries. Add them manually.
- scans << scan(:corpus_management, configured: true)
- scans << scan(:dast_profiles, configured: true)
- end
-
- def latest_pipeline_path
- return help_page_path('ci/pipelines') unless latest_default_branch_pipeline
-
- project_pipeline_path(self, latest_default_branch_pipeline)
- end
-
- def scan(type, configured: false)
- scan = ::Gitlab::Security::ScanConfiguration.new(project: project, type: type, configured: configured)
-
- {
- type: scan.type,
- configured: scan.configured?,
- configuration_path: scan.configuration_path,
- available: scan.available?
- }
- end
-
- def scan_types
- ::Security::SecurityJobsFinder.allowed_job_types + ::Security::LicenseComplianceJobsFinder.allowed_job_types
- end
-
- def project_settings
- project.security_setting
- end
- end
- end
-end
-
-Projects::Security::ConfigurationPresenter.prepend_mod_with('Projects::Security::ConfigurationPresenter')
diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb
index 5939b9d2f9c..192d40129a3 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -24,10 +24,18 @@ module ResourceEvents
private
def apply_common_filters(events)
+ events = apply_pagination(events)
events = apply_last_fetched_at(events)
apply_fetch_until(events)
end
+ def apply_pagination(events)
+ return events if params[:paginated_notes].nil?
+ return events.none if params[:paginated_notes][table_name].blank?
+
+ events.id_in(params[:paginated_notes][table_name].map(&:id))
+ end
+
def apply_last_fetched_at(events)
return events unless params[:last_fetched_at].present?
@@ -47,5 +55,9 @@ module ResourceEvents
resource.project || resource.group
end
end
+
+ def table_name
+ raise NotImplementedError
+ end
end
end
diff --git a/app/services/resource_events/synthetic_label_notes_builder_service.rb b/app/services/resource_events/synthetic_label_notes_builder_service.rb
index 5915ea938cf..0e5d945d13c 100644
--- a/app/services/resource_events/synthetic_label_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_label_notes_builder_service.rb
@@ -23,5 +23,9 @@ module ResourceEvents
events.group_by { |event| event.discussion_id }
end
+
+ def table_name
+ 'resource_label_events'
+ end
end
end
diff --git a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
index 10acf94e22b..0e2b171e192 100644
--- a/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_milestone_notes_builder_service.rb
@@ -21,5 +21,9 @@ module ResourceEvents
events = resource.resource_milestone_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
apply_common_filters(events)
end
+
+ def table_name
+ 'resource_milestone_events'
+ end
end
end
diff --git a/app/services/resource_events/synthetic_state_notes_builder_service.rb b/app/services/resource_events/synthetic_state_notes_builder_service.rb
index 71d40200365..e17882b00de 100644
--- a/app/services/resource_events/synthetic_state_notes_builder_service.rb
+++ b/app/services/resource_events/synthetic_state_notes_builder_service.rb
@@ -16,5 +16,9 @@ module ResourceEvents
events = resource.resource_state_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
apply_common_filters(events)
end
+
+ def table_name
+ 'resource_state_events'
+ end
end
end
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 00df8608957..0eae3c95bf6 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -43,7 +43,9 @@
.gl-alert.gl-alert-danger.gl-mb-5
.gl-alert-container
.gl-alert-content
- = @error
+ %p.gl-alert-body.gl-md-0
+ = @error[:message]
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 86cac7c8376..f1c19756474 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,4 +1,4 @@
-- add_page_startup_api_call discussions_path(@issue)
+- add_page_startup_api_call Feature.enabled?(:paginated_issue_discussions, @project, default_enabled: :yaml) ? discussions_path(@issue, per_page: 20) : discussions_path(@issue)
- @gfm_form = true