Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
08775893a8
commit
a3e0d4c59f
2
Gemfile
2
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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
4a5bf054f8bea3ec51060cc4cd3a18f12fb40e13edb8a5a8d99f9d25e631dd30
|
||||
|
|
@ -0,0 +1 @@
|
|||
4267ce10078606ae7829e5b1afd27e64c7e15603d87dd0c1a52a683ae8fb9e28
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
### Pipeline details view **(PREMIUM ALL)**
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
## Contribute your scanner
|
||||
|
||||
The [Security Scanner Integration](../../../development/integrations/secure.md) documentation explains how to integrate other security scanners into GitLab.
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: '' });
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue