diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-16 12:13:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-16 12:13:21 +0300 |
commit | 2c90b9b579fbfe3db191a032d2cb176761605a02 (patch) | |
tree | d9819280a1ec64ff82c31ce6081e00745a9648b4 /app | |
parent | ccca6cec346d169fa2521c390760af9bd885ea77 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |