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
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-01 06:13:34 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-01 06:13:34 +0300
commita3e0d4c59ff43ee406d2fee12a29339b95a0787d (patch)
tree22105026897baded7ae5fe2ea5384bdfe6febc69
parent08775893a80e4024d9b30bd1a17caff7ecb274f9 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue15
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue15
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue18
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue27
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js2
-rw-r--r--app/finders/issuable_finder.rb6
-rw-r--r--app/finders/issues_finder.rb21
-rw-r--r--app/finders/work_items/namespace_work_items_finder.rb4
-rw-r--r--app/graphql/resolvers/group_issues_resolver.rb2
-rw-r--r--app/graphql/types/issue_type.rb10
-rw-r--r--app/graphql/types/issue_type_enum.rb4
-rw-r--r--app/helpers/issues_helper.rb3
-rw-r--r--app/models/issue.rb11
-rw-r--r--app/models/users/phone_number_validation.rb7
-rw-r--r--app/models/work_item.rb1
-rw-r--r--app/presenters/issue_presenter.rb8
-rw-r--r--db/post_migrate/20231129192345_drop_projects_on_path_and_id_index.rb18
-rw-r--r--db/post_migrate/20231130200216_drop_projects_on_created_at_and_id_index.rb18
-rw-r--r--db/schema_migrations/202311291923451
-rw-r--r--db/schema_migrations/202311302002161
-rw-r--r--db/structure.sql4
-rw-r--r--doc/api/graphql/reference/index.md9
-rw-r--r--doc/ci/testing/code_quality.md4
-rw-r--r--doc/ci/testing/img/code_quality_inline_indicator_v16_7.pngbin0 -> 53078 bytes
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/index.md15
-rw-r--r--doc/user/application_security/sast/img/sast_inline_indicator_v16_7.pngbin0 -> 89080 bytes
-rw-r--r--doc/user/application_security/sast/img/sast_mr_widget_v16_7.pngbin0 -> 39147 bytes
-rw-r--r--doc/user/application_security/sast/index.md31
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js20
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js2
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js54
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js46
-rw-r--r--spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js29
-rw-r--r--spec/frontend/issues/dashboard/components/index_spec.js18
-rw-r--r--spec/graphql/types/issue_type_enum_spec.rb2
-rw-r--r--spec/helpers/issues_helper_spec.rb2
-rw-r--r--spec/models/issue_spec.rb10
-rw-r--r--spec/models/users/phone_number_validation_spec.rb24
-rw-r--r--spec/models/work_item_spec.rb10
-rw-r--r--spec/presenters/issue_presenter_spec.rb20
-rw-r--r--spec/requests/api/graphql/group/issues_spec.rb36
-rw-r--r--spec/requests/api/project_events_spec.rb45
-rw-r--r--spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb5
-rw-r--r--spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb19
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb28
49 files changed, 544 insertions, 95 deletions
diff --git a/Gemfile b/Gemfile
index 8fa6b6911d2..44894d6a62c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -197,7 +197,7 @@ gem 'seed-fu', '~> 2.3.7' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-model', '~> 7.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentation' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'elasticsearch-api', '7.13.3' # rubocop:todo Gemfile/MissingFeatureCategory
-gem 'aws-sdk-core', '~> 3.189.0' # rubocop:todo Gemfile/MissingFeatureCategory
+gem 'aws-sdk-core', '~> 3.190.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'aws-sdk-s3', '~> 1.141.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'faraday_middleware-aws-sigv4', '~>0.3.0' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 125d58c49e8..031a8943ef8 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -37,7 +37,7 @@
{"name":"aws-eventstream","version":"1.3.0","platform":"ruby","checksum":"f1434cc03ab2248756eb02cfa45e900e59a061d7fbdc4a9fd82a5dd23d796d3f"},
{"name":"aws-partitions","version":"1.761.0","platform":"ruby","checksum":"291e444e1edfc92c5521a6dbdd1236ccc3f122b3520163b2be6ec5b6ef350ef2"},
{"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"},
-{"name":"aws-sdk-core","version":"3.189.0","platform":"ruby","checksum":"a2b5d55c2f3827c8e453a228b011ac5c0074a09c301e289774151225114c7ecf"},
+{"name":"aws-sdk-core","version":"3.190.0","platform":"ruby","checksum":"a3455fb3fc1691dd5331282ff16cb0b2ef136a5b63ed68b77e9fda447ea7cfa6"},
{"name":"aws-sdk-kms","version":"1.64.0","platform":"ruby","checksum":"40de596c95047bfc6e1aacea24f3df6241aa716b6f7ce08ac4c5f7e3120395ad"},
{"name":"aws-sdk-s3","version":"1.141.0","platform":"ruby","checksum":"cadb88497af6736e86a4a1fc8eb42333fb27ae85901686334252c50862bdd02e"},
{"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 55a11ad9272..50e152536dc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -291,7 +291,7 @@ GEM
aws-sdk-cloudformation (1.41.0)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
- aws-sdk-core (3.189.0)
+ aws-sdk-core (3.190.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
@@ -1792,7 +1792,7 @@ DEPENDENCIES
autoprefixer-rails (= 10.2.5.1)
awesome_print
aws-sdk-cloudformation (~> 1)
- aws-sdk-core (~> 3.189.0)
+ aws-sdk-core (~> 3.190.0)
aws-sdk-s3 (~> 1.141.0)
axe-core-rspec
babosa (~> 2.0)
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
index 3c709fc565f..d13d7611143 100644
--- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
+ showCommentForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -60,6 +65,16 @@ export default {
: '';
},
},
+ watch: {
+ showCommentForm: {
+ immediate: true,
+ handler(focus) {
+ if (focus) {
+ this.isEditing = true;
+ }
+ },
+ },
+ },
methods: {
async addNote({ commentText }) {
this.isSubmitting = true;
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
index 49ba777f697..4f28ec65e87 100644
--- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
@@ -28,6 +28,7 @@ export default {
data() {
return {
isExpanded: true,
+ showCommentForm: false,
};
},
computed: {
@@ -54,16 +55,24 @@ export default {
toggleDiscussion() {
this.isExpanded = !this.isExpanded;
},
+ startReplying() {
+ this.showCommentForm = true;
+ },
+ stopReplying() {
+ this.showCommentForm = false;
+ },
},
};
</script>
<template>
<abuse-report-note
- v-if="!hasReplies"
+ v-if="!hasReplies && !showCommentForm"
:note="note"
:abuse-report-id="abuseReportId"
+ show-reply-button
class="gl-mb-4"
+ @startReplying="startReplying"
/>
<timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
<div class="timeline-content">
@@ -76,7 +85,9 @@ export default {
:note="note"
:discussion-id="discussionId"
:abuse-report-id="abuseReportId"
+ show-reply-button
class="gl-mb-4"
+ @startReplying="startReplying"
/>
<discussion-notes-replies-wrapper>
<toggle-replies-widget
@@ -97,7 +108,9 @@ export default {
<abuse-report-add-note
:discussion-id="discussionId"
:is-new-discussion="false"
+ :show-comment-form="showCommentForm"
:abuse-report-id="abuseReportId"
+ @cancelEditing="stopReplying"
/>
</template>
</discussion-notes-replies-wrapper>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
index 6da3017e11e..213a7c85fe2 100644
--- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
@@ -5,6 +5,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import NoteBody from './abuse_report_note_body.vue';
+import AbuseReportNoteActions from './abuse_report_note_actions.vue';
export default {
name: 'AbuseReportNote',
@@ -17,6 +18,7 @@ export default {
TimelineEntryItem,
NoteHeader,
NoteBody,
+ AbuseReportNoteActions,
},
props: {
abuseReportId: {
@@ -27,6 +29,11 @@ export default {
type: Object,
required: true,
},
+ showReplyButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
noteAnchorId() {
@@ -39,6 +46,11 @@ export default {
return getIdFromGraphQLId(this.author.id);
},
},
+ methods: {
+ startReplying() {
+ this.$emit('startReplying');
+ },
+ },
};
</script>
@@ -70,6 +82,12 @@ export default {
>
<span v-if="note.createdAt" class="d-none d-sm-inline">&middot;</span>
</note-header>
+ <div class="gl-display-inline-flex">
+ <abuse-report-note-actions
+ :show-reply-button="showReplyButton"
+ @startReplying="startReplying"
+ />
+ </div>
</div>
<div class="timeline-discussion-body">
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue
new file mode 100644
index 00000000000..91bfaf60f38
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue
@@ -0,0 +1,27 @@
+<script>
+import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+
+export default {
+ name: 'AbuseReportNoteActions',
+ components: {
+ ReplyButton,
+ },
+ props: {
+ showReplyButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="note-actions">
+ <reply-button
+ v-if="showReplyButton"
+ ref="replyButton"
+ @startReplying="$emit('startReplying')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index 74633b251b2..30cc1c5b822 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -23,6 +23,7 @@ export async function mountIssuesDashboardApp() {
emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
+ hasIssueDateFilterFeature,
hasIssueWeightsFeature,
hasScopedLabelsFeature,
initialSort,
@@ -47,6 +48,7 @@ export async function mountIssuesDashboardApp() {
emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueDateFilterFeature: parseBoolean(hasIssueDateFilterFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 93b7292bb69..2b4e4592020 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -130,9 +130,7 @@ class IssuableFinder
end
def filter_items(items)
- # Selection by group is already covered by `by_project` and `projects` for project-based issuables
- # Group-based issuables have their own group filter methods
- items = by_project(items)
+ items = by_parent(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
@@ -313,7 +311,7 @@ class IssuableFinder
end
# rubocop: disable CodeReuse/ActiveRecord
- def by_project(items)
+ def by_parent(items)
# When finding issues for multiple projects it's more efficient
# to use a JOIN instead of running a sub-query
# See https://gitlab.com/gitlab-org/gitlab/-/commit/8591cc02be6b12ed60f763a5e0147f2cbbca99e1
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 0ba93a76342..2297c0569d9 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -109,6 +109,21 @@ class IssuesFinder < IssuableFinder
super.with_projects_matching_search_data
end
+ override :by_parent
+ def by_parent(items)
+ return super unless include_namespace_level_work_items?
+
+ items.in_namespaces(
+ Namespace.from_union(
+ [
+ Group.id_in(params.group).select(:id),
+ params.projects.select(:project_namespace_id)
+ ],
+ remove_duplicates: false
+ )
+ )
+ end
+
def by_confidential(items)
return items if params[:confidential].nil?
@@ -157,6 +172,12 @@ class IssuesFinder < IssuableFinder
def model_class
Issue
end
+
+ def include_namespace_level_work_items?
+ params.group? &&
+ Array(params[:issue_types]).map(&:to_s).include?('epic') &&
+ Feature.enabled?(:namespace_level_work_items, params.group)
+ end
end
IssuesFinder.prepend_mod_with('IssuesFinder')
diff --git a/app/finders/work_items/namespace_work_items_finder.rb b/app/finders/work_items/namespace_work_items_finder.rb
index da6437e0907..95075e0ca0d 100644
--- a/app/finders/work_items/namespace_work_items_finder.rb
+++ b/app/finders/work_items/namespace_work_items_finder.rb
@@ -54,8 +54,8 @@ module WorkItems
end
strong_memoize_attr :namespace
- override :by_project
- def by_project(items)
+ override :by_parent
+ def by_parent(items)
items
end
end
diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb
index 5a6a3d678b9..360781806a4 100644
--- a/app/graphql/resolvers/group_issues_resolver.rb
+++ b/app/graphql/resolvers/group_issues_resolver.rb
@@ -10,7 +10,7 @@ module Resolvers
include GroupIssuableResolver
before_connection_authorization do |nodes, _|
- projects = nodes.map(&:project)
+ projects = nodes.filter_map(&:project)
ActiveRecord::Associations::Preloader.new(records: projects, associations: project_associations).call
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 1c8a654a841..2c1c9e5a3bd 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -91,13 +91,13 @@ module Types
description: 'Web URL of the issue.'
field :emails_disabled, GraphQL::Types::Boolean, null: false,
- method: :project_emails_disabled?,
- description: 'Indicates if a project has email notifications disabled: `true` if email notifications are disabled.',
+ method: :parent_emails_disabled?,
+ description: 'Indicates if the parent project or group has email notifications disabled: `true` if email notifications are disabled.',
deprecated: { reason: 'Use `emails_enabled`', milestone: '16.3' }
field :emails_enabled, GraphQL::Types::Boolean, null: false,
- method: :project_emails_enabled?,
- description: 'Indicates if a project has email notifications disabled: `false` if email notifications are disabled.'
+ method: :parent_emails_enabled?,
+ description: 'Indicates if the parent project or group has email notifications disabled: `false` if email notifications are disabled.'
field :human_time_estimate, GraphQL::Types::String, null: true,
description: 'Human-readable time estimate of the issue.'
@@ -162,7 +162,7 @@ module Types
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the issue.'
- field :project_id, GraphQL::Types::Int, null: false, method: :project_id,
+ field :project_id, GraphQL::Types::Int, null: true, method: :project_id,
description: 'ID of the issue project.'
field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true,
diff --git a/app/graphql/types/issue_type_enum.rb b/app/graphql/types/issue_type_enum.rb
index d7f587ff03d..e1f0a69b90a 100644
--- a/app/graphql/types/issue_type_enum.rb
+++ b/app/graphql/types/issue_type_enum.rb
@@ -20,5 +20,9 @@ module Types
value 'KEY_RESULT', value: 'key_result',
description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.7' }
+ value 'EPIC', value: 'epic',
+ description: 'Epic issue type. ' \
+ 'Available only when feature flag `namespace_level_work_items` is enabled.',
+ alpha: { milestone: '16.7' }
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 4419b573701..42a4d63ed2f 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -166,7 +166,7 @@ module IssuesHelper
jira_integration_path: help_page_url('integration/jira/issues', anchor: 'view-jira-issues'),
rss_path: url_for(safe_params.merge(rss_url_options)),
sign_in_path: new_user_session_path,
- has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, namespace).to_s
+ has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, current_user).to_s
}
end
@@ -218,6 +218,7 @@ module IssuesHelper
dashboard_milestones_path: dashboard_milestones_path(format: :json),
empty_state_with_filter_svg_path: image_path('illustrations/empty-state/empty-issues-md.svg'),
empty_state_without_filter_svg_path: image_path('illustrations/issue-dashboard_results-without-filter.svg'),
+ has_issue_date_filter_feature: Feature.enabled?(:issue_date_filter, current_user).to_s,
initial_sort: current_user&.user_preference&.issues_sort,
is_public_visibility_restricted:
Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b207785021d..4d73db8caac 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -132,16 +132,16 @@ class Issue < ApplicationRecord
validate :due_date_after_start_date
validate :parent_link_confidentiality
- alias_method :issuing_parent, :project
- alias_attribute :issuing_parent_id, :project_id
-
alias_attribute :external_author, :service_desk_reply_to
pg_full_text_searchable columns: [{ name: 'title', weight: 'A' }, { name: 'description', weight: 'B' }]
+ scope :in_namespaces, ->(namespaces) { where(namespace: namespaces) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :not_in_projects, ->(project_ids) { where.not(project_id: project_ids) }
+ scope :non_archived, -> { left_joins(:project).where(project_id: nil).or(where(projects: { archived: false })) }
+
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
@@ -732,6 +732,11 @@ class Issue < ApplicationRecord
def resource_parent
project || namespace
end
+ alias_method :issuing_parent, :resource_parent
+
+ def issuing_parent_id
+ project_id.presence || namespace_id
+ end
# Persisted records will always have a work_item_type. This method is useful
# in places where we use a non persisted issue to perform feature checks
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index 2256eb8ddc4..072b75a1c90 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -35,10 +35,13 @@ module Users
scope :for_user, -> (user_id) { where(user_id: user_id) }
def self.related_to_banned_user?(international_dial_code, phone_number)
- joins(:banned_user).where(
+ joins(:banned_user)
+ .where(
international_dial_code: international_dial_code,
phone_number: phone_number
- ).exists?
+ )
+ .where.not(validated_at: nil)
+ .exists?
end
def self.by_reference_id(ref_id)
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 05c230628ce..d4cac3b6c44 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -23,7 +23,6 @@ class WorkItem < Issue
foreign_key: :work_item_id, source: :work_item
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
- scope :in_namespaces, ->(namespaces) { where(namespace: namespaces) }
scope :with_confidentiality_check, ->(user) {
confidential_query = <<~SQL
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index 9403dd0814b..f9bc8c1dfa6 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -12,12 +12,12 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
issue.subscribed?(current_user, issue.project)
end
- def project_emails_disabled?
- issue.project.emails_disabled?
+ def parent_emails_disabled?
+ issue.resource_parent.emails_disabled?
end
- def project_emails_enabled?
- issue.project.emails_enabled?
+ def parent_emails_enabled?
+ issue.resource_parent.emails_enabled?
end
delegator_override :service_desk_reply_to
diff --git a/db/post_migrate/20231129192345_drop_projects_on_path_and_id_index.rb b/db/post_migrate/20231129192345_drop_projects_on_path_and_id_index.rb
new file mode 100644
index 00000000000..825cb8bb8d2
--- /dev/null
+++ b/db/post_migrate/20231129192345_drop_projects_on_path_and_id_index.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class DropProjectsOnPathAndIdIndex < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :projects
+ INDEX_NAME = :index_projects_on_path_and_id
+
+ def up
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index TABLE_NAME, [:path, :id], name: INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20231130200216_drop_projects_on_created_at_and_id_index.rb b/db/post_migrate/20231130200216_drop_projects_on_created_at_and_id_index.rb
new file mode 100644
index 00000000000..ed033fa0f66
--- /dev/null
+++ b/db/post_migrate/20231130200216_drop_projects_on_created_at_and_id_index.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class DropProjectsOnCreatedAtAndIdIndex < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = :projects
+ INDEX_NAME = :index_projects_on_created_at_and_id
+
+ def up
+ remove_concurrent_index_by_name TABLE_NAME, INDEX_NAME
+ end
+
+ def down
+ add_concurrent_index TABLE_NAME, [:created_at, :id], name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20231129192345 b/db/schema_migrations/20231129192345
new file mode 100644
index 00000000000..611ad223f05
--- /dev/null
+++ b/db/schema_migrations/20231129192345
@@ -0,0 +1 @@
+4a5bf054f8bea3ec51060cc4cd3a18f12fb40e13edb8a5a8d99f9d25e631dd30 \ No newline at end of file
diff --git a/db/schema_migrations/20231130200216 b/db/schema_migrations/20231130200216
new file mode 100644
index 00000000000..cb7d0740994
--- /dev/null
+++ b/db/schema_migrations/20231130200216
@@ -0,0 +1 @@
+4267ce10078606ae7829e5b1afd27e64c7e15603d87dd0c1a52a683ae8fb9e28 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 0dc47575f9e..270c6508743 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -34158,8 +34158,6 @@ CREATE INDEX index_projects_id_for_aimed_for_deletion ON projects USING btree (i
CREATE INDEX index_projects_not_aimed_for_deletion ON projects USING btree (id) WHERE (marked_for_deletion_at IS NULL);
-CREATE INDEX index_projects_on_created_at_and_id ON projects USING btree (created_at, id);
-
CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON projects USING btree (creator_id, created_at, id);
CREATE INDEX index_projects_on_creator_id_and_id ON projects USING btree (creator_id, id);
@@ -34202,8 +34200,6 @@ CREATE INDEX index_projects_on_namespace_id_and_repository_size_limit ON project
CREATE INDEX index_projects_on_organization_id ON projects USING btree (organization_id);
-CREATE INDEX index_projects_on_path_and_id ON projects USING btree (path, id);
-
CREATE INDEX index_projects_on_path_trigram ON projects USING gin (path gin_trgm_ops);
CREATE INDEX index_projects_on_pending_delete ON projects USING btree (pending_delete);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 8ce59e74fb1..fc1c9c41f7f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -18123,7 +18123,7 @@ Relationship between an epic and an issue.
| <a id="epicissuedownvotes"></a>`downvotes` | [`Int!`](#int) | Number of downvotes the issue has received. |
| <a id="epicissueduedate"></a>`dueDate` | [`Time`](#time) | Due date of the issue. |
| <a id="epicissueemailsdisabled"></a>`emailsDisabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.3. Use `emails_enabled`. |
-| <a id="epicissueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `false` if email notifications are disabled. |
+| <a id="epicissueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if the parent project or group has email notifications disabled: `false` if email notifications are disabled. |
| <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. |
| <a id="epicissueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
@@ -18145,7 +18145,7 @@ Relationship between an epic and an issue.
| <a id="epicissuemoved"></a>`moved` | [`Boolean`](#boolean) | Indicates if issue got moved from other project. |
| <a id="epicissuemovedto"></a>`movedTo` | [`Issue`](#issue) | Updated Issue after it got moved to another project. |
| <a id="epicissueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
-| <a id="epicissueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
+| <a id="epicissueprojectid"></a>`projectId` | [`Int`](#int) | ID of the issue project. |
| <a id="epicissuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) |
| <a id="epicissuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) |
| <a id="epicissuerelationpath"></a>`relationPath` | [`String`](#string) | URI path of the epic-issue relation. |
@@ -20524,7 +20524,7 @@ Describes an issuable resource link for incident issues.
| <a id="issuedownvotes"></a>`downvotes` | [`Int!`](#int) | Number of downvotes the issue has received. |
| <a id="issueduedate"></a>`dueDate` | [`Time`](#time) | Due date of the issue. |
| <a id="issueemailsdisabled"></a>`emailsDisabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 16.3. Use `emails_enabled`. |
-| <a id="issueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `false` if email notifications are disabled. |
+| <a id="issueemailsenabled"></a>`emailsEnabled` | [`Boolean!`](#boolean) | Indicates if the parent project or group has email notifications disabled: `false` if email notifications are disabled. |
| <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. |
| <a id="issueescalationpolicy"></a>`escalationPolicy` | [`EscalationPolicyType`](#escalationpolicytype) | Escalation policy associated with the issue. Available for issues which support escalation. |
| <a id="issueescalationstatus"></a>`escalationStatus` | [`IssueEscalationStatus`](#issueescalationstatus) | Escalation status of the issue. |
@@ -20545,7 +20545,7 @@ Describes an issuable resource link for incident issues.
| <a id="issuemoved"></a>`moved` | [`Boolean`](#boolean) | Indicates if issue got moved from other project. |
| <a id="issuemovedto"></a>`movedTo` | [`Issue`](#issue) | Updated Issue after it got moved to another project. |
| <a id="issueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
-| <a id="issueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
+| <a id="issueprojectid"></a>`projectId` | [`Int`](#int) | ID of the issue project. |
| <a id="issuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) |
| <a id="issuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) |
| <a id="issuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). |
@@ -30050,6 +30050,7 @@ Issue type.
| Value | Description |
| ----- | ----------- |
+| <a id="issuetypeepic"></a>`EPIC` **{warning-solid}** | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Epic issue type. Available only when feature flag `namespace_level_work_items` is enabled. |
| <a id="issuetypeincident"></a>`INCIDENT` | Incident issue type. |
| <a id="issuetypeissue"></a>`ISSUE` | Issue issue type. |
| <a id="issuetypekey_result"></a>`KEY_RESULT` **{warning-solid}** | **Introduced** in 15.7. This feature is an Experiment. It can be changed or removed at any time. Key Result issue type. Available only when feature flag `okrs_mvc` is enabled. |
diff --git a/doc/ci/testing/code_quality.md b/doc/ci/testing/code_quality.md
index 94968d5dfe2..1819361ea86 100644
--- a/doc/ci/testing/code_quality.md
+++ b/doc/ci/testing/code_quality.md
@@ -65,9 +65,9 @@ full report available in the **Pipeline** details view.
> - [Inline annotation added](https://gitlab.com/gitlab-org/gitlab/-/issues/2526) and [feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284140) in GitLab 14.1.
Code Quality results display in the merge request **Changes** view. Lines containing Code Quality
-issues are marked by an indicator beside the gutter. Hover over the marker for details of the issue.
+issues are marked by a symbol beside the gutter. Select the symbol to see the list of issues, then select an issue to see its details.
-![Code Quality MR diff report](img//code_quality_mr_diff_report_v15_7.png)
+![Code Quality Inline Indicator](img/code_quality_inline_indicator_v16_7.png)
### Pipeline details view **(PREMIUM ALL)**
diff --git a/doc/ci/testing/img/code_quality_inline_indicator_v16_7.png b/doc/ci/testing/img/code_quality_inline_indicator_v16_7.png
new file mode 100644
index 00000000000..0d7d5bb3062
--- /dev/null
+++ b/doc/ci/testing/img/code_quality_inline_indicator_v16_7.png
Binary files differ
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
index 8c42a394106..68cd0ae3d53 100644
--- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md
+++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
@@ -7,20 +7,7 @@ type: howto
# Undo options in Git **(FREE ALL)**
-[Nothing in Git is deleted](https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery),
-so when you work in Git, you can undo your work.
-
-All version control systems have options for undoing work. However,
-because of the de-centralized nature of Git, these options are multiplied.
-The actions you take are based on the
-[stage of development](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository)
-you are in.
-
-For more information about working with Git and GitLab:
-
-- <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Learn why [North Western Mutual chose GitLab](https://youtu.be/kPNMyxKRRoM) for their enterprise source code management.
-- Learn how to [get started with Git](https://about.gitlab.com/resources/whitepaper-moving-to-git/).
-- For more advanced examples, refer to the [Git book](https://git-scm.com/book/en/v2).
+Git provides options for undoing changes. The method for undoing a change depends on whether the change is unstaged, staged, committed, or pushed.
## When you can undo changes
diff --git a/doc/user/application_security/sast/img/sast_inline_indicator_v16_7.png b/doc/user/application_security/sast/img/sast_inline_indicator_v16_7.png
new file mode 100644
index 00000000000..c86f536afc4
--- /dev/null
+++ b/doc/user/application_security/sast/img/sast_inline_indicator_v16_7.png
Binary files differ
diff --git a/doc/user/application_security/sast/img/sast_mr_widget_v16_7.png b/doc/user/application_security/sast/img/sast_mr_widget_v16_7.png
new file mode 100644
index 00000000000..199f8b6d322
--- /dev/null
+++ b/doc/user/application_security/sast/img/sast_mr_widget_v16_7.png
Binary files differ
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 00a12c7c8a4..3aa0ff8b1de 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -17,7 +17,6 @@ any GitLab tier. The analyzers output JSON-formatted reports as job artifacts.
With GitLab Ultimate, SAST results are also processed so you can:
-- See them in merge requests.
- Use them in approval workflows.
- Review them in the security dashboard.
@@ -222,6 +221,7 @@ as shown in the following table:
| [Customize SAST settings](#available-cicd-variables) | **{check-circle}** | **{check-circle}** |
| Download [JSON Report](#reports-json-format) | **{check-circle}** | **{check-circle}** |
| See new findings in merge request widget | **{dotted-circle}** | **{check-circle}** |
+| See new findings in merge request changes | **{dotted-circle}** | **{check-circle}** |
| [Manage vulnerabilities](../vulnerabilities/index.md) | **{dotted-circle}** | **{check-circle}** |
| [Access the Security Dashboard](../security_dashboard/index.md) | **{dotted-circle}** | **{check-circle}** |
| [Configure SAST by using the UI](#configure-sast-by-using-the-ui) | **{dotted-circle}** | **{check-circle}** |
@@ -229,6 +229,35 @@ as shown in the following table:
| [Detect False Positives](#false-positive-detection) | **{dotted-circle}** | **{check-circle}** |
| [Track moved vulnerabilities](#advanced-vulnerability-tracking) | **{dotted-circle}** | **{check-circle}** |
+## View SAST results
+
+SAST results are shown in the:
+
+- Merge request widget
+- Merge request changes view
+- Vulnerability Report
+
+### Merge request widget **(ULTIMATE ALL)**
+
+SAST results display in the merge request widget area if a report from the target
+branch is available for comparison. The merge request widget displays SAST findings and resolutions that
+were introduced by the changes made in the merge request.
+
+![Security Merge request widget](img/sast_mr_widget_v16_7.png)
+
+### Merge request changes view **(ULTIMATE ALL)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10959) in GitLab 16.6 with a [flag](../../../administration/feature_flags.md) named `sast_reports_in_inline_diff`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../../administration/feature_flags.md) named `sast_reports_in_inline_diff`.
+On GitLab.com, this feature is not available.
+
+SAST results display in the merge request **Changes** view. Lines containing SAST
+issues are marked by a symbol beside the gutter. Select the symbol to see the list of issues, then select an issue to see its details.
+
+![SAST Inline Indicator](img/sast_inline_indicator_v16_7.png)
+
## Contribute your scanner
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1f28887ff8c..095ebbead51 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -43191,6 +43191,9 @@ msgstr ""
msgid "SecurityOrchestration|%{fileName} loading failed. Please try again."
msgstr ""
+msgid "SecurityOrchestration|%{frameworkName} has %{projectLength} %{projects}"
+msgstr ""
+
msgid "SecurityOrchestration|%{licenses} and %{lastLicense}"
msgstr ""
@@ -43293,6 +43296,9 @@ msgstr ""
msgid "SecurityOrchestration|Compliance Framework ID(s) can only be set for group policies"
msgstr ""
+msgid "SecurityOrchestration|Compliance framework has no projects"
+msgstr ""
+
msgid "SecurityOrchestration|Create more robust vulnerability rules and apply them to all your projects."
msgstr ""
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js
index dd9dcf304ae..09193f8052a 100644
--- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_add_note_spec.js
@@ -19,6 +19,7 @@ describe('Abuse Report Add Note', () => {
let wrapper;
const mockAbuseReportId = mockAbuseReport.report.globalId;
+ const mockDiscussionId = 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a';
const mutationSuccessHandler = jest.fn().mockResolvedValue(createAbuseReportNoteResponse);
@@ -35,6 +36,7 @@ describe('Abuse Report Add Note', () => {
abuseReportId = mockAbuseReportId,
discussionId = '',
isNewDiscussion = true,
+ showCommentForm = false,
} = {}) => {
wrapper = shallowMountExtended(AbuseReportAddNote, {
apolloProvider: createMockApollo([[createNoteMutation, mutationHandler]]),
@@ -42,6 +44,7 @@ describe('Abuse Report Add Note', () => {
abuseReportId,
discussionId,
isNewDiscussion,
+ showCommentForm,
},
});
};
@@ -194,15 +197,30 @@ describe('Abuse Report Add Note', () => {
describe('Replying to a comment', () => {
beforeEach(() => {
createComponent({
- discussionId: 'gid://gitlab/Discussion/9c7228e06fb0339a3d1440fcda960acfd8baa43a',
+ discussionId: mockDiscussionId,
isNewDiscussion: false,
+ showCommentForm: false,
});
});
+ it('should not show the comment form', () => {
+ expect(findAbuseReportCommentForm().exists()).toBe(false);
+ });
+
it('should show comment form when reply textarea is clicked on', async () => {
await findReplyTextarea().trigger('click');
expect(findAbuseReportCommentForm().exists()).toBe(true);
});
+
+ it('should show comment form if `showCommentForm` is true', () => {
+ createComponent({
+ discussionId: mockDiscussionId,
+ isNewDiscussion: false,
+ showCommentForm: true,
+ });
+
+ expect(findAbuseReportCommentForm().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js
index 6f9ae8852b2..e521806f8f6 100644
--- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_comment_form_spec.js
@@ -80,7 +80,7 @@ describe('Abuse Report Comment Form', () => {
expect(findMarkdownEditor().props('value')).toBe('draft comment');
});
- it('should pass an empty string if both draft & initialValue are empty', () => {
+ it('should pass an empty string if both draft and initialValue are empty', () => {
jest.spyOn(autosave, 'getDraft').mockImplementation(() => '');
createComponent({ initialValue: '' });
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
index 5a8c7abe711..fdc049725a4 100644
--- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_discussion_spec.js
@@ -45,6 +45,7 @@ describe('Abuse Report Discussion', () => {
expect(findAbuseReportNote().props()).toMatchObject({
abuseReportId: mockAbuseReportId,
note: mockDiscussionWithNoReplies[0],
+ showReplyButton: true,
});
});
@@ -91,5 +92,58 @@ describe('Abuse Report Discussion', () => {
isNewDiscussion: false,
});
});
+
+ it('should show the reply button only for the main comment', () => {
+ expect(findAbuseReportNotes().at(0).props('showReplyButton')).toBe(true);
+
+ expect(findAbuseReportNotes().at(1).props('showReplyButton')).toBe(false);
+ expect(findAbuseReportNotes().at(2).props('showReplyButton')).toBe(false);
+ });
+ });
+
+ describe('Replying to a comment when it has no replies', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show comment form when `startReplying` is emitted', async () => {
+ expect(findAbuseReportAddNote().exists()).toBe(false);
+
+ findAbuseReportNote().vm.$emit('startReplying');
+ await nextTick();
+
+ expect(findAbuseReportAddNote().exists()).toBe(true);
+ expect(findAbuseReportAddNote().props('showCommentForm')).toBe(true);
+ });
+
+ it('should hide the comment form when `cancelEditing` is emitted', async () => {
+ findAbuseReportNote().vm.$emit('startReplying');
+ await nextTick();
+
+ findAbuseReportAddNote().vm.$emit('cancelEditing');
+ await nextTick();
+
+ expect(findAbuseReportAddNote().exists()).toBe(false);
+ });
+ });
+
+ describe('Replying to a comment with replies', () => {
+ beforeEach(() => {
+ createComponent({
+ discussion: mockDiscussionWithReplies,
+ });
+ });
+
+ it('should show reply textarea, but not comment form', () => {
+ expect(findAbuseReportAddNote().exists()).toBe(true);
+ expect(findAbuseReportAddNote().props('showCommentForm')).toBe(false);
+ });
+
+ it('should show comment form when reply button on main comment is clicked', async () => {
+ findAbuseReportNotes().at(0).vm.$emit('startReplying');
+ await nextTick();
+
+ expect(findAbuseReportAddNote().props('showCommentForm')).toBe(true);
+ });
});
});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js
new file mode 100644
index 00000000000..b24da749004
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_actions_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+import AbuseReportNoteActions from '~/admin/abuse_report/components/notes/abuse_report_note_actions.vue';
+
+describe('Abuse Report Note Actions', () => {
+ let wrapper;
+ const mockShowReplyButton = true;
+
+ const findReplyButton = () => wrapper.findComponent(ReplyButton);
+
+ const createComponent = ({ showReplyButton = mockShowReplyButton } = {}) => {
+ wrapper = shallowMount(AbuseReportNoteActions, {
+ propsData: {
+ showReplyButton,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should show reply button', () => {
+ expect(findReplyButton().exists()).toBe(true);
+ });
+
+ it('should emit `startReplying`', () => {
+ findReplyButton().vm.$emit('startReplying');
+
+ expect(wrapper.emitted('startReplying')).toHaveLength(1);
+ });
+ });
+
+ describe('When `showReplyButton` is false', () => {
+ beforeEach(() => {
+ createComponent({
+ showReplyButton: false,
+ });
+ });
+
+ it('should not show reply button', () => {
+ expect(findReplyButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
index b6908853e46..33be59898e9 100644
--- a/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
+++ b/spec/frontend/admin/abuse_report/components/notes/abuse_report_note_spec.js
@@ -2,7 +2,8 @@ import { shallowMount } from '@vue/test-utils';
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import AbuseReportNote from '~/admin/abuse_report/components/notes/abuse_report_note.vue';
import NoteHeader from '~/notes/components/note_header.vue';
-import NoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
+import AbuseReportNoteBody from '~/admin/abuse_report/components/notes/abuse_report_note_body.vue';
+import AbuseReportNoteActions from '~/admin/abuse_report/components/notes/abuse_report_note_actions.vue';
import { mockAbuseReport, mockDiscussionWithNoReplies } from '../../mock_data';
@@ -10,18 +11,25 @@ describe('Abuse Report Note', () => {
let wrapper;
const mockAbuseReportId = mockAbuseReport.report.globalId;
const mockNote = mockDiscussionWithNoReplies[0];
+ const mockShowReplyButton = true;
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
- const findNoteBody = () => wrapper.findComponent(NoteBody);
+ const findNoteBody = () => wrapper.findComponent(AbuseReportNoteBody);
+ const findNoteActions = () => wrapper.findComponent(AbuseReportNoteActions);
- const createComponent = ({ note = mockNote, abuseReportId = mockAbuseReportId } = {}) => {
+ const createComponent = ({
+ note = mockNote,
+ abuseReportId = mockAbuseReportId,
+ showReplyButton = mockShowReplyButton,
+ } = {}) => {
wrapper = shallowMount(AbuseReportNote, {
propsData: {
note,
abuseReportId,
+ showReplyButton,
},
});
};
@@ -77,4 +85,19 @@ describe('Abuse Report Note', () => {
});
});
});
+
+ describe('Actions', () => {
+ it('should show note actions', () => {
+ expect(findNoteActions().exists()).toBe(true);
+ expect(findNoteActions().props()).toMatchObject({
+ showReplyButton: mockShowReplyButton,
+ });
+ });
+
+ it('should emit `startReplying`', () => {
+ findNoteActions().vm.$emit('startReplying');
+
+ expect(wrapper.emitted('startReplying')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/issues/dashboard/components/index_spec.js b/spec/frontend/issues/dashboard/components/index_spec.js
new file mode 100644
index 00000000000..51cb5c0acf6
--- /dev/null
+++ b/spec/frontend/issues/dashboard/components/index_spec.js
@@ -0,0 +1,18 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { mountIssuesDashboardApp } from '~/issues/dashboard';
+
+describe('IssueDashboardRoot', () => {
+ beforeEach(() => {
+ setHTMLFixture(
+ '<div class="js-issues-dashboard" data-has-issue-date-filter-feature="true"></div>',
+ );
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('mounts without errors and vue warnings', async () => {
+ await expect(mountIssuesDashboardApp()).resolves.toBeTruthy();
+ });
+});
diff --git a/spec/graphql/types/issue_type_enum_spec.rb b/spec/graphql/types/issue_type_enum_spec.rb
index 5b1bc9c3d9c..f0370e275cd 100644
--- a/spec/graphql/types/issue_type_enum_spec.rb
+++ b/spec/graphql/types/issue_type_enum_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Types::IssueTypeEnum, feature_category: :team_planning do
it 'exposes all the existing issue type values except epic' do
expect(described_class.values.keys).to match_array(
- %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE KEY_RESULT]
+ %w[ISSUE INCIDENT TEST_CASE REQUIREMENT TASK OBJECTIVE KEY_RESULT EPIC]
)
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 62d94b59c2a..2bc6dff26fa 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -303,6 +303,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:image_path).and_return('#')
allow(helper).to receive(:url_for).and_return('#')
+ stub_feature_flags(issue_date_filter: false)
expected = {
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
@@ -311,6 +312,7 @@ RSpec.describe IssuesHelper, feature_category: :team_planning do
dashboard_milestones_path: dashboard_milestones_path(format: :json),
empty_state_with_filter_svg_path: '#',
empty_state_without_filter_svg_path: '#',
+ has_issue_date_filter_feature: 'false',
initial_sort: current_user&.user_preference&.issues_sort,
is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
is_signed_in: current_user.present?.to_s,
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 6c8603d7b4c..aa538de9f02 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -380,6 +380,16 @@ RSpec.describe Issue, feature_category: :team_planning do
end
end
+ describe '.in_namespaces' do
+ let(:group) { create(:group) }
+ let!(:group_work_item) { create(:issue, :group_level, namespace: group) }
+ let!(:project_work_item) { create(:issue, project: reusable_project) }
+
+ subject { described_class.in_namespaces(group) }
+
+ it { is_expected.to contain_exactly(group_work_item) }
+ end
+
describe '.with_issue_type' do
let_it_be(:issue) { create(:issue, project: reusable_project) }
let_it_be(:incident) { create(:incident, project: reusable_project) }
diff --git a/spec/models/users/phone_number_validation_spec.rb b/spec/models/users/phone_number_validation_spec.rb
index e41719d8ca3..15bbb507dee 100644
--- a/spec/models/users/phone_number_validation_spec.rb
+++ b/spec/models/users/phone_number_validation_spec.rb
@@ -39,16 +39,26 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
end
context 'when banned user has the same international dial code and phone number' do
- before do
- create(:phone_number_validation, user: banned_user)
+ context 'and the matching record has not been verified' do
+ before do
+ create(:phone_number_validation, user: banned_user)
+ end
+
+ it { is_expected.to eq(false) }
end
- it { is_expected.to eq(true) }
+ context 'and the matching record has been verified' do
+ before do
+ create(:phone_number_validation, :validated, user: banned_user)
+ end
+
+ it { is_expected.to eq(true) }
+ end
end
context 'when banned user has the same international dial code and phone number, but different country code' do
before do
- create(:phone_number_validation, user: banned_user, country: 'CA')
+ create(:phone_number_validation, :validated, user: banned_user, country: 'CA')
end
it { is_expected.to eq(true) }
@@ -56,7 +66,7 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
context 'when banned user does not have the same international dial code' do
before do
- create(:phone_number_validation, user: banned_user, international_dial_code: 61)
+ create(:phone_number_validation, :validated, user: banned_user, international_dial_code: 61)
end
it { is_expected.to eq(false) }
@@ -64,7 +74,7 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
context 'when banned user does not have the same phone number' do
before do
- create(:phone_number_validation, user: banned_user, phone_number: '666')
+ create(:phone_number_validation, :validated, user: banned_user, phone_number: '666')
end
it { is_expected.to eq(false) }
@@ -72,7 +82,7 @@ RSpec.describe Users::PhoneNumberValidation, feature_category: :instance_resilie
context 'when not-banned user has the same international dial code and phone number' do
before do
- create(:phone_number_validation, user: user)
+ create(:phone_number_validation, :validated, user: user)
end
it { is_expected.to eq(false) }
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 476d346db10..c70c1002eda 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -79,16 +79,6 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
- describe '.in_namespaces' do
- let(:group) { create(:group) }
- let!(:group_work_item) { create(:work_item, namespace: group) }
- let!(:project_work_item) { create(:work_item, project: reusable_project) }
-
- subject { described_class.in_namespaces(group) }
-
- it { is_expected.to contain_exactly(group_work_item) }
- end
-
describe '.with_confidentiality_check' do
let(:user) { create(:user) }
let!(:authored_work_item) { create(:work_item, :confidential, project: reusable_project, author: user) }
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
index 07a9f8015e9..6c971a55e74 100644
--- a/spec/presenters/issue_presenter_spec.rb
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -73,8 +73,8 @@ RSpec.describe IssuePresenter do
end
end
- describe '#project_emails_disabled?' do
- subject { presenter.project_emails_disabled? }
+ describe '#parent_emails_disabled?' do
+ subject { presenter.parent_emails_disabled? }
it 'returns false when emails notifications is enabled for project' do
is_expected.to be(false)
@@ -87,6 +87,22 @@ RSpec.describe IssuePresenter do
it { is_expected.to be(true) }
end
+
+ context 'for group-level issue' do
+ let(:presented_issue) { create(:issue, :group_level, namespace: group) }
+
+ it 'returns false when email notifications are enabled for group' do
+ is_expected.to be(false)
+ end
+
+ context 'when email notifications are disabled for group' do
+ before do
+ allow(group).to receive(:emails_disabled?).and_return(true)
+ end
+
+ it { is_expected.to be(true) }
+ end
+ end
end
describe '#service_desk_reply_to' do
diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb
index 95aeed32558..1da6abf3cac 100644
--- a/spec/requests/api/graphql/group/issues_spec.rb
+++ b/spec/requests/api/graphql/group/issues_spec.rb
@@ -15,6 +15,8 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan
let_it_be(:issue2) { create(:issue, project: project2) }
let_it_be(:issue3) { create(:issue, project: project3) }
+ let_it_be(:group_level_issue) { create(:issue, :epic, :group_level, namespace: group1) }
+
let(:issue1_gid) { issue1.to_global_id.to_s }
let(:issue2_gid) { issue2.to_global_id.to_s }
let(:issues_data) { graphql_data['group']['issues']['edges'] }
@@ -142,6 +144,40 @@ RSpec.describe 'getting an issue list for a group', feature_category: :team_plan
end
end
+ context 'when querying epic types' do
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group1.full_path },
+ "issues(types: [EPIC]) { #{fields} }"
+ )
+ end
+
+ before_all do
+ group1.add_developer(current_user)
+ end
+
+ it 'returns group-level epics' do
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ expect(issues_ids).to contain_exactly(group_level_issue.to_global_id.to_s)
+ end
+
+ context 'when namespace_level_work_items is disabled' do
+ before do
+ stub_feature_flags(namespace_level_work_items: false)
+ end
+
+ it 'returns no epics' do
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ expect(issues_ids).to be_empty
+ end
+ end
+ end
+
def issues_ids
graphql_dig_at(issues_data, :node, :id)
end
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
index f904cd8fd6c..52a6093c4c8 100644
--- a/spec/requests/api/project_events_spec.rb
+++ b/spec/requests/api/project_events_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe API::ProjectEvents, feature_category: :user_profile do
- let(:user) { create(:user) }
- let(:non_member) { create(:user) }
- let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
- let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
- let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: :closed, created_at: Date.new(2016, 12, 30)) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
+ let_it_be(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
+ let_it_be(:closed_issue_event) { create(:closed_issue_event, project: private_project, author: user, target: closed_issue, created_at: Date.new(2016, 12, 30)) }
describe 'GET /projects/:id/events' do
context 'when unauthenticated ' do
@@ -27,11 +27,11 @@ RSpec.describe API::ProjectEvents, feature_category: :user_profile do
end
context 'with inaccessible events' do
- let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
- let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
- let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: :closed) }
- let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
- let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: :closed) }
+ let_it_be(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
+ let_it_be(:confidential_issue) { create(:closed_issue, :confidential, project: public_project, author: user) }
+ let_it_be(:confidential_event) { create(:closed_issue_event, project: public_project, author: user, target: confidential_issue) }
+ let_it_be(:public_issue) { create(:closed_issue, project: public_project, author: user) }
+ let_it_be(:public_event) { create(:closed_issue_event, project: public_project, author: user, target: public_issue) }
it 'returns only accessible events' do
get api("/projects/#{public_project.id}/events", non_member)
@@ -124,23 +124,34 @@ RSpec.describe API::ProjectEvents, feature_category: :user_profile do
end
context 'when exists some events' do
- let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+ let_it_be(:merge_request1) { create(:closed_merge_request, author: user, assignees: [user], source_project: private_project) }
+ let_it_be(:merge_request2) { create(:closed_merge_request, author: user, assignees: [user], source_project: private_project) }
+
+ let_it_be(:token) { create(:personal_access_token, user: user) }
before do
create_event(merge_request1)
end
it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.count
+ # Warmup, e.g. users#last_activity_on
+ get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request }
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request }
+ end
create_event(merge_request2)
expect do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.not_to exceed_all_query_limit(control_count)
+ get api("/projects/#{private_project.id}/events", personal_access_token: token), params: { target_type: :merge_request }
+ end.to issue_same_number_of_queries_as(control).with_threshold(1)
+ # The extra threshold is because we need to fetch `project` for the 2nd
+ # event. This is because in `app/policies/issuable_policy.rb`, we fetch
+ # the `project` for the `target` for the `event`. It is non-trivial to
+ # re-use the original `project` object from `lib/api/project_events.rb`
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/432823
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
diff --git a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
index 16d23f63fd0..2ad2fcec39e 100644
--- a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
@@ -63,6 +63,11 @@ RSpec.shared_context 'IssuesFinder context' do
)
end
+ let_it_be(:group_level_item) { create(:issue, :epic, :group_level, namespace: group, author: user) }
+ let_it_be(:group_level_confidential_item) do
+ create(:issue, :confidential, :epic, :group_level, namespace: group, author: user2)
+ end
+
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
diff --git a/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb
index 1118039d164..39e2819235b 100644
--- a/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb
@@ -63,6 +63,25 @@ RSpec.shared_context 'WorkItemsFinder context' do
)
end
+ let_it_be(:group_level_item) do
+ create(
+ :work_item,
+ :epic,
+ namespace: group,
+ author: user
+ )
+ end
+
+ let_it_be(:group_level_confidential_item) do
+ create(
+ :work_item,
+ :confidential,
+ :epic,
+ namespace: group,
+ author: user2
+ )
+ end
+
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 043d6db66d3..a5fee9c5fed 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -269,6 +269,34 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
end
end
end
+
+ context 'when querying group-level items' do
+ let(:params) { { group_id: group.id, issue_types: %w[issue epic] } }
+
+ it 'includes group-level items' do
+ expect(items).to contain_exactly(item1, item5, group_level_item)
+ end
+
+ context 'when user has access to confidential items' do
+ before do
+ group.add_reporter(user)
+ end
+
+ it 'includes confidential group-level items' do
+ expect(items).to contain_exactly(item1, item5, group_level_item, group_level_confidential_item)
+ end
+ end
+
+ context 'when namespace_level_work_items is disabled' do
+ before do
+ stub_feature_flags(namespace_level_work_items: false)
+ end
+
+ it 'only returns project-level items' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
end
context 'filtering by author' do