diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-01 06:13:34 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-01 06:13:34 +0300 |
commit | a3e0d4c59ff43ee406d2fee12a29339b95a0787d (patch) | |
tree | 22105026897baded7ae5fe2ea5384bdfe6febc69 | |
parent | 08775893a80e4024d9b30bd1a17caff7ecb274f9 (diff) |
Add latest changes from gitlab-org/gitlab@master
49 files changed, 544 insertions, 95 deletions
@@ -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">·</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 Binary files differnew file mode 100644 index 00000000000..0d7d5bb3062 --- /dev/null +++ b/doc/ci/testing/img/code_quality_inline_indicator_v16_7.png 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> 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 Binary files differnew file mode 100644 index 00000000000..c86f536afc4 --- /dev/null +++ b/doc/user/application_security/sast/img/sast_inline_indicator_v16_7.png 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 Binary files differnew file mode 100644 index 00000000000..199f8b6d322 --- /dev/null +++ b/doc/user/application_security/sast/img/sast_mr_widget_v16_7.png 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 |