Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0a412bceb9
commit
2e2c1a521c
|
@ -199,6 +199,7 @@ Style/FormatString:
|
|||
- 'ee/app/components/namespaces/free_user_cap/personal_usage_quota_limitations_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/preview_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/usage_quota_alert_component.rb'
|
||||
- 'ee/app/components/namespaces/free_user_cap/usage_quota_trial_alert_component.rb'
|
||||
- 'ee/app/controllers/admin/elasticsearch_controller.rb'
|
||||
- 'ee/app/controllers/admin/geo/application_controller.rb'
|
||||
- 'ee/app/controllers/admin/geo/projects_controller.rb'
|
||||
|
|
|
@ -470,7 +470,7 @@ Style/IfUnlessModifier:
|
|||
- 'db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb'
|
||||
- 'db/post_migrate/20220131000001_schedule_trace_expiry_removal.rb'
|
||||
- 'db/post_migrate/20220523171107_drop_deploy_tokens_token_column.rb'
|
||||
- 'ee/app/components/namespaces/storage/limit_alert.rb'
|
||||
- 'ee/app/components/namespaces/storage/limit_alert_component.rb'
|
||||
- 'ee/app/controllers/admin/elasticsearch_controller.rb'
|
||||
- 'ee/app/controllers/admin/emails_controller.rb'
|
||||
- 'ee/app/controllers/admin/geo/application_controller.rb'
|
||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
|||
>
|
||||
<gl-avatar
|
||||
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
|
||||
:entity-id="group.id"
|
||||
:entity-name="group.name"
|
||||
:src="group.avatarUrl"
|
||||
:alt="group.name"
|
||||
|
|
|
@ -73,7 +73,9 @@ export default {
|
|||
<template>
|
||||
<div ref="milestoneDetails" class="issue-milestone-details">
|
||||
<gl-icon :size="16" class="gl-mr-2 flex-shrink-0" name="clock" />
|
||||
<span class="milestone-title d-inline-block">{{ milestone.title }}</span>
|
||||
<span class="milestone-title gl-display-inline-block gl-text-truncate">{{
|
||||
milestone.title
|
||||
}}</span>
|
||||
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
|
||||
<span class="bold">{{ __('Milestone') }}</span> <br />
|
||||
<span>{{ milestone.title }}</span> <br />
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<script>
|
||||
import { GlPopover, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { GlIcon, GlPopover, GlSkeletonLoader, GlTooltipDirective } from '@gitlab/ui';
|
||||
import query from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
|
||||
import IssueDueDate from '~/boards/components/issue_due_date.vue';
|
||||
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
|
||||
import StatusBox from '~/issuable/components/status_box.vue';
|
||||
import { IssuableStatus } from '~/issues/constants';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import query from '../queries/issue.query.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlPopover,
|
||||
GlSkeletonLoader,
|
||||
IssueDueDate,
|
||||
IssueMilestone,
|
||||
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
|
||||
StatusBox,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
target: {
|
||||
|
@ -44,6 +54,9 @@ export default {
|
|||
showDetails() {
|
||||
return Object.keys(this.issue).length > 0;
|
||||
},
|
||||
isIssueClosed() {
|
||||
return this.issue?.state === IssuableStatus.Closed;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
issue: {
|
||||
|
@ -69,6 +82,14 @@ export default {
|
|||
</gl-skeleton-loader>
|
||||
<div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
|
||||
<status-box issuable-type="issue" :initial-state="issue.state" />
|
||||
<gl-icon
|
||||
v-if="issue.confidential"
|
||||
v-gl-tooltip
|
||||
name="eye-slash"
|
||||
:title="__('Confidential')"
|
||||
class="gl-text-orange-500 gl-mr-2"
|
||||
:aria-label="__('Confidential')"
|
||||
/>
|
||||
<span class="gl-text-secondary">
|
||||
{{ __('Opened') }} <time :datetime="issue.createdAt">{{ formattedTime }}</time>
|
||||
</span>
|
||||
|
@ -79,5 +100,27 @@ export default {
|
|||
{{ `${projectPath}#${iid}` }}
|
||||
</div>
|
||||
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
|
||||
|
||||
<div v-if="!$apollo.queries.issue.loading" class="gl-display-flex gl-text-secondary gl-mt-2">
|
||||
<issue-due-date
|
||||
v-if="issue.dueDate"
|
||||
:date="issue.dueDate.toString()"
|
||||
:closed="isIssueClosed"
|
||||
tooltip-placement="top"
|
||||
class="gl-mr-4"
|
||||
css-class="gl-display-flex gl-white-space-nowrap"
|
||||
/>
|
||||
<issue-weight
|
||||
v-if="issue.weight"
|
||||
:weight="issue.weight"
|
||||
tag-name="span"
|
||||
class="gl-display-flex gl-mr-4"
|
||||
/>
|
||||
<issue-milestone
|
||||
v-if="issue.milestone"
|
||||
:milestone="issue.milestone"
|
||||
class="gl-display-flex gl-overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
</gl-popover>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,14 @@ query issue($projectPath: ID!, $iid: String!) {
|
|||
title
|
||||
createdAt
|
||||
state
|
||||
confidential
|
||||
dueDate
|
||||
milestone {
|
||||
id
|
||||
title
|
||||
startDate
|
||||
dueDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ $avatar-sizes: (
|
|||
),
|
||||
60: (
|
||||
font-size: 32px,
|
||||
line-height: 58px,
|
||||
line-height: 60px,
|
||||
border-radius: $border-radius-large
|
||||
),
|
||||
64: (
|
||||
|
@ -47,7 +47,7 @@ $avatar-sizes: (
|
|||
),
|
||||
90: (
|
||||
font-size: 36px,
|
||||
line-height: 88px,
|
||||
line-height: 90px,
|
||||
border-radius: $border-radius-large
|
||||
),
|
||||
96: (
|
||||
|
@ -72,7 +72,6 @@ $avatar-sizes: (
|
|||
float: left;
|
||||
margin-right: $gl-padding;
|
||||
border-radius: $avatar-radius;
|
||||
border: 1px solid $t-gray-a-08;
|
||||
|
||||
@each $size, $size-config in $avatar-sizes {
|
||||
&.s#{$size} {
|
||||
|
@ -83,13 +82,12 @@ $avatar-sizes: (
|
|||
|
||||
.avatar {
|
||||
transition-property: none;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
background: $gray-lightest;
|
||||
overflow: hidden;
|
||||
border-color: rgba($black, $gl-avatar-border-opacity);
|
||||
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
|
||||
|
||||
&.avatar-inline {
|
||||
float: none;
|
||||
|
@ -180,6 +178,10 @@ $avatar-sizes: (
|
|||
@each $size, $size-config in $avatar-sizes {
|
||||
&.s#{$size} {
|
||||
border-radius: map-get($size-config, 'border-radius');
|
||||
|
||||
.avatar {
|
||||
border-radius: map-get($size-config, 'border-radius');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,16 +134,6 @@
|
|||
.avatar-container {
|
||||
@include gl-font-weight-normal;
|
||||
flex: none;
|
||||
box-shadow: $avatar-box-shadow;
|
||||
|
||||
&.rect-avatar {
|
||||
@include gl-border-none;
|
||||
|
||||
.avatar.s32 {
|
||||
border-radius: $border-radius-default;
|
||||
box-shadow: $avatar-box-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
|
||||
.avatar-container {
|
||||
flex: 0 0 40px;
|
||||
flex: 0 0 32px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
|
|
|
@ -1043,7 +1043,7 @@ kbd {
|
|||
text-align: left;
|
||||
}
|
||||
.context-header .avatar-container {
|
||||
flex: 0 0 40px;
|
||||
flex: 0 0 32px;
|
||||
background-color: #333;
|
||||
}
|
||||
.context-header .sidebar-context-title {
|
||||
|
@ -1376,18 +1376,6 @@ kbd {
|
|||
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
flex: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
|
||||
border-style: none;
|
||||
}
|
||||
.nav-sidebar-inner-scroll
|
||||
> div.context-header
|
||||
a
|
||||
.avatar-container.rect-avatar
|
||||
.avatar.s32 {
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items {
|
||||
margin-bottom: 60px;
|
||||
|
@ -1400,18 +1388,6 @@ kbd {
|
|||
.sidebar-top-level-items .context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
flex: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
|
||||
border-style: none;
|
||||
}
|
||||
.sidebar-top-level-items
|
||||
.context-header
|
||||
a
|
||||
.avatar-container.rect-avatar
|
||||
.avatar.s32 {
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items
|
||||
> li.active
|
||||
|
@ -1628,7 +1604,6 @@ svg.s16 {
|
|||
float: left;
|
||||
margin-right: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.avatar.s16,
|
||||
.avatar-container.s16 {
|
||||
|
@ -1649,7 +1624,7 @@ svg.s16 {
|
|||
padding: 0;
|
||||
background: #222;
|
||||
overflow: hidden;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.avatar.avatar-tile {
|
||||
border-radius: 0;
|
||||
|
@ -1714,9 +1689,15 @@ svg.s16 {
|
|||
.rect-avatar.s16 {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.rect-avatar.s16 .avatar {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.rect-avatar.s32 {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.rect-avatar.s32 .avatar {
|
||||
border-radius: 4px;
|
||||
}
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
@ -1817,6 +1798,10 @@ body.gl-dark {
|
|||
background-color: #262626;
|
||||
border-right: 1px solid #303030;
|
||||
}
|
||||
.avatar-container,
|
||||
.avatar {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.nav-sidebar li a {
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
|
|
@ -1022,7 +1022,7 @@ kbd {
|
|||
text-align: left;
|
||||
}
|
||||
.context-header .avatar-container {
|
||||
flex: 0 0 40px;
|
||||
flex: 0 0 32px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.context-header .sidebar-context-title {
|
||||
|
@ -1355,18 +1355,6 @@ kbd {
|
|||
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
flex: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
|
||||
border-style: none;
|
||||
}
|
||||
.nav-sidebar-inner-scroll
|
||||
> div.context-header
|
||||
a
|
||||
.avatar-container.rect-avatar
|
||||
.avatar.s32 {
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items {
|
||||
margin-bottom: 60px;
|
||||
|
@ -1379,18 +1367,6 @@ kbd {
|
|||
.sidebar-top-level-items .context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
flex: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
|
||||
border-style: none;
|
||||
}
|
||||
.sidebar-top-level-items
|
||||
.context-header
|
||||
a
|
||||
.avatar-container.rect-avatar
|
||||
.avatar.s32 {
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items
|
||||
> li.active
|
||||
|
@ -1607,7 +1583,6 @@ svg.s16 {
|
|||
float: left;
|
||||
margin-right: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.avatar.s16,
|
||||
.avatar-container.s16 {
|
||||
|
@ -1628,7 +1603,7 @@ svg.s16 {
|
|||
padding: 0;
|
||||
background: #fdfdfd;
|
||||
overflow: hidden;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 0 0 1px rgba(31, 31, 31, 0.1);
|
||||
}
|
||||
.avatar.avatar-tile {
|
||||
border-radius: 0;
|
||||
|
@ -1693,9 +1668,15 @@ svg.s16 {
|
|||
.rect-avatar.s16 {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.rect-avatar.s16 .avatar {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.rect-avatar.s32 {
|
||||
border-radius: 4px;
|
||||
}
|
||||
.rect-avatar.s32 .avatar {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab-width-8 {
|
||||
tab-size: 8;
|
||||
|
|
|
@ -48,6 +48,17 @@
|
|||
border-right: 1px solid $gray-50;
|
||||
}
|
||||
|
||||
.gl-avatar:not(.gl-avatar-identicon),
|
||||
.avatar-container,
|
||||
.avatar {
|
||||
background: rgba($gray-950, 0.04);
|
||||
}
|
||||
|
||||
.gl-avatar {
|
||||
@include gl-border-none;
|
||||
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
li {
|
||||
a {
|
||||
|
|
|
@ -4,14 +4,10 @@ module Integrations
|
|||
module BaseDataFields
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
LEGACY_FOREIGN_KEY_NAME = %w(
|
||||
Integrations::IssueTrackerData
|
||||
).freeze
|
||||
|
||||
included do
|
||||
# TODO: Once we rename the tables we can't rely on `table_name` anymore.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/331953
|
||||
belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: foreign_key_name
|
||||
belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :integration_id
|
||||
|
||||
validates :integration, presence: true
|
||||
end
|
||||
|
@ -25,16 +21,6 @@ module Integrations
|
|||
algorithm: 'aes-256-gcm'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Older data field models use the `service_id` foreign key for the
|
||||
# integration association.
|
||||
def foreign_key_name
|
||||
return :service_id if self.name.in?(LEGACY_FOREIGN_KEY_NAME)
|
||||
|
||||
:integration_id
|
||||
end
|
||||
end
|
||||
|
||||
def activated?
|
||||
|
|
|
@ -44,7 +44,7 @@ module Integrations
|
|||
end
|
||||
|
||||
included do
|
||||
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
|
||||
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::IssueTrackerData'
|
||||
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::JiraTrackerData'
|
||||
has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.context-header
|
||||
= link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
|
||||
%span{ class: ['avatar-container', 'settings-avatar', 's32'] }
|
||||
= image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
|
||||
= image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32', 'gl-rounded-full!'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
|
||||
%span.sidebar-context-title= _('User Settings')
|
||||
%ul.sidebar-top-level-items
|
||||
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: pull_mirror_bulk_branches
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93211
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368797
|
||||
milestone: '15.2'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RenameIssueTrackerDataServiceIdToIntegrationId < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
rename_column_concurrently :issue_tracker_data, :service_id, :integration_id
|
||||
end
|
||||
|
||||
def down
|
||||
undo_rename_column_concurrently :issue_tracker_data, :service_id, :integration_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CleanupIssueTrackerDataServiceId < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
cleanup_concurrent_column_rename :issue_tracker_data, :service_id, :integration_id
|
||||
end
|
||||
|
||||
def down
|
||||
undo_cleanup_concurrent_column_rename :issue_tracker_data, :service_id, :integration_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
4ee9f603c04284cbc0fcb6aa47ecc0f0fe238b4d68083a51f5f170edca19608b
|
|
@ -0,0 +1 @@
|
|||
b4ff0087acba9b91182219ea49a5a7d1bfd5b55391f0174ea62a2bfa14af03ce
|
|
@ -16597,7 +16597,6 @@ ALTER SEQUENCE issue_metrics_id_seq OWNED BY issue_metrics.id;
|
|||
|
||||
CREATE TABLE issue_tracker_data (
|
||||
id bigint NOT NULL,
|
||||
service_id integer NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
encrypted_project_url character varying,
|
||||
|
@ -16605,7 +16604,9 @@ CREATE TABLE issue_tracker_data (
|
|||
encrypted_issues_url character varying,
|
||||
encrypted_issues_url_iv character varying,
|
||||
encrypted_new_issue_url character varying,
|
||||
encrypted_new_issue_url_iv character varying
|
||||
encrypted_new_issue_url_iv character varying,
|
||||
integration_id integer,
|
||||
CONSTRAINT check_7ca00cd891 CHECK ((integration_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE issue_tracker_data_id_seq
|
||||
|
@ -28671,7 +28672,7 @@ CREATE INDEX index_issue_metrics_on_issue_id_and_timestamps ON issue_metrics USI
|
|||
|
||||
CREATE INDEX index_issue_on_project_id_state_id_and_blocking_issues_count ON issues USING btree (project_id, state_id, blocking_issues_count);
|
||||
|
||||
CREATE INDEX index_issue_tracker_data_on_service_id ON issue_tracker_data USING btree (service_id);
|
||||
CREATE INDEX index_issue_tracker_data_on_integration_id ON issue_tracker_data USING btree (integration_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_issue_user_mentions_on_note_id ON issue_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
|
||||
|
||||
|
@ -32039,6 +32040,9 @@ ALTER TABLE ONLY approvals
|
|||
ALTER TABLE ONLY namespaces
|
||||
ADD CONSTRAINT fk_319256d87a FOREIGN KEY (file_template_project_id) REFERENCES projects(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY issue_tracker_data
|
||||
ADD CONSTRAINT fk_33921c0ee1 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY namespaces
|
||||
ADD CONSTRAINT fk_3448c97865 FOREIGN KEY (push_rule_id) REFERENCES push_rules(id) ON DELETE SET NULL;
|
||||
|
||||
|
@ -34055,9 +34059,6 @@ ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
|
|||
ALTER TABLE ONLY operations_strategies_user_lists
|
||||
ADD CONSTRAINT fk_rails_ccb7e4bc0b FOREIGN KEY (user_list_id) REFERENCES operations_user_lists(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY issue_tracker_data
|
||||
ADD CONSTRAINT fk_rails_ccc0840427 FOREIGN KEY (service_id) REFERENCES integrations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY resource_milestone_events
|
||||
ADD CONSTRAINT fk_rails_cedf8cce4d FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||
|
||||
|
|
|
@ -1125,12 +1125,6 @@ Geo secondary sites continue to replicate and verify data, and the secondary sit
|
|||
|
||||
This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983).
|
||||
|
||||
### GitLab Pages return 404 errors after promoting
|
||||
|
||||
This is due to [Pages data not being managed by Geo](datatypes.md#limitations-on-replicationverification).
|
||||
Find advice to resolve those error messages in the
|
||||
[Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node).
|
||||
|
||||
### Primary site returns 500 error when accessing `/admin/geo/replication/projects`
|
||||
|
||||
Navigating to **Admin > Geo > Replication** (or `/admin/geo/replication/projects`) on a primary Geo site, shows a 500 error, while that same link on the secondary works fine. The primary's `production.log` has a similar entry to the following:
|
||||
|
|
|
@ -1319,25 +1319,6 @@ and in your Pages log shows this error:
|
|||
sudo gitlab-ctl restart gitlab-pages
|
||||
```
|
||||
|
||||
### 404 error after promoting a Geo secondary to a primary node
|
||||
|
||||
Pages files are not among the
|
||||
[supported data types](../geo/replication/datatypes.md#limitations-on-replicationverification) for replication in Geo. After a secondary node is promoted to a primary node, attempts to access a Pages site result in a `404 Not Found` error.
|
||||
|
||||
It is possible to copy the subfolders and files in the [Pages path](#change-storage-path)
|
||||
to the new primary node to resolve this.
|
||||
For example, you can adapt the `rsync` strategy from the
|
||||
[moving repositories documentation](../operations/moving_repositories.md).
|
||||
Alternatively, run the CI pipelines of those projects that contain a `pages` job again.
|
||||
|
||||
### 404 or 500 error when accessing GitLab Pages in a Geo setup
|
||||
|
||||
Pages sites are only available on the primary Geo site, while the codebase of the project is available on all sites.
|
||||
|
||||
If you try to access a Pages page on a secondary site, a 404 or 500 HTTP code is returned depending on the access control.
|
||||
|
||||
Read more which [features don't support Geo replication/verification](../geo/replication/datatypes.md#limitations-on-replicationverification).
|
||||
|
||||
### Failed to connect to the internal GitLab API
|
||||
|
||||
If you see the following error:
|
||||
|
|
|
@ -188,6 +188,24 @@ Refer to [`strong_memoize.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/maste
|
|||
end
|
||||
```
|
||||
|
||||
Alternatively, use the `strong_memoize_attr` helper to memoize the method for you:
|
||||
|
||||
```ruby
|
||||
class Find
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def result
|
||||
search
|
||||
end
|
||||
strong_memoize_attr :result
|
||||
|
||||
strong_memoize_attr :enabled?, :enabled
|
||||
def enabled?
|
||||
Feature.enabled?(:some_feature)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
- Clear memoization
|
||||
|
||||
```ruby
|
||||
|
|
|
@ -297,9 +297,6 @@ A 404 can also be related to incorrect permissions. If [Pages Access Control](pa
|
|||
navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site.
|
||||
To fix this, verify that the user is a member of the project.
|
||||
|
||||
For Geo instances, 404 errors on Pages occur after promoting a secondary to a primary.
|
||||
Find more details in the [Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node)
|
||||
|
||||
### Cannot play media content on Safari
|
||||
|
||||
Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6)
|
||||
|
|
|
@ -21,6 +21,20 @@ module Gitlab
|
|||
# end
|
||||
# end
|
||||
#
|
||||
# Or like:
|
||||
#
|
||||
# include Gitlab::Utils::StrongMemoize
|
||||
#
|
||||
# def trigger_from_token
|
||||
# Ci::Trigger.find_by_token(params[:token].to_s)
|
||||
# end
|
||||
# strong_memoize_attr :trigger_from_token
|
||||
#
|
||||
# strong_memoize_attr :enabled?, :enabled
|
||||
# def enabled?
|
||||
# Feature.enabled?(:some_feature)
|
||||
# end
|
||||
#
|
||||
def strong_memoize(name)
|
||||
key = ivar(name)
|
||||
|
||||
|
@ -40,6 +54,34 @@ module Gitlab
|
|||
remove_instance_variable(key) if instance_variable_defined?(key)
|
||||
end
|
||||
|
||||
module StrongMemoizeClassMethods
|
||||
def strong_memoize_attr(method_name, member_name = nil)
|
||||
member_name ||= method_name
|
||||
|
||||
if method_defined?(method_name) || private_method_defined?(method_name)
|
||||
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
|
||||
:do_strong_memoize, self, method_name, member_name)
|
||||
else
|
||||
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
|
||||
:queue_strong_memoize, self, method_name, member_name)
|
||||
end
|
||||
end
|
||||
|
||||
def method_added(method_name)
|
||||
super
|
||||
|
||||
if member_name = StrongMemoize
|
||||
.send(:strong_memoize_queue, self).delete(method_name) # rubocop:disable GitlabSecurity/PublicSend
|
||||
StrongMemoize.send( # rubocop:disable GitlabSecurity/PublicSend
|
||||
:do_strong_memoize, self, method_name, member_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.singleton_class.prepend(StrongMemoizeClassMethods)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Convert `"name"`/`:name` into `:@name`
|
||||
|
@ -54,6 +96,37 @@ module Gitlab
|
|||
raise ArgumentError, "Invalid type of '#{name}'"
|
||||
end
|
||||
end
|
||||
|
||||
class <<self
|
||||
private
|
||||
|
||||
def strong_memoize_queue(klass)
|
||||
klass.instance_variable_get(:@strong_memoize_queue) || klass.instance_variable_set(:@strong_memoize_queue, {})
|
||||
end
|
||||
|
||||
def queue_strong_memoize(klass, method_name, member_name)
|
||||
strong_memoize_queue(klass)[method_name] = member_name
|
||||
end
|
||||
|
||||
def do_strong_memoize(klass, method_name, member_name)
|
||||
method = klass.instance_method(method_name)
|
||||
|
||||
# Methods defined within a class method are already public by default, so we don't need to
|
||||
# explicitly make them public.
|
||||
scope = %i(private protected).find do |scope|
|
||||
klass.send("#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend
|
||||
.include? method_name
|
||||
end
|
||||
|
||||
klass.define_method(method_name) do |*args, &block|
|
||||
strong_memoize(member_name) do
|
||||
method.bind_call(self, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -106,3 +106,41 @@ RSpec.describe API::Issues, '(JavaScript fixtures)', type: :request do
|
|||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe GraphQL::Query, type: :request do
|
||||
include ApiHelpers
|
||||
include GraphqlHelpers
|
||||
include JavaScriptFixturesHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
before_all do
|
||||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
issue_popover_query_path = 'issuable/popover/queries/issue.query.graphql'
|
||||
|
||||
it "graphql/#{issue_popover_query_path}.json" do
|
||||
query = get_graphql_query_as_string(issue_popover_query_path, ee: Gitlab.ee?)
|
||||
|
||||
issue = create(
|
||||
:issue,
|
||||
project: project,
|
||||
confidential: true,
|
||||
created_at: Time.parse('2020-07-01T04:08:01Z'),
|
||||
due_date: Date.new(2020, 7, 5),
|
||||
milestone: create(
|
||||
:milestone,
|
||||
project: project,
|
||||
title: '15.2',
|
||||
start_date: Date.new(2020, 7, 1),
|
||||
due_date: Date.new(2020, 7, 30)
|
||||
)
|
||||
)
|
||||
|
||||
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s })
|
||||
|
||||
expect_graphql_errors_to_be_empty
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,34 +1,21 @@
|
|||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import issueQueryResponse from 'test_fixtures/graphql/issuable/popover/queries/issue.query.graphql.json';
|
||||
import issueQuery from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import IssueDueDate from '~/boards/components/issue_due_date.vue';
|
||||
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
|
||||
import StatusBox from '~/issuable/components/status_box.vue';
|
||||
import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
|
||||
import issueQuery from '~/issuable/popover/queries/issue.query.graphql';
|
||||
|
||||
describe('Issue Popover', () => {
|
||||
let wrapper;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const issueQueryResponse = {
|
||||
data: {
|
||||
project: {
|
||||
__typename: 'Project',
|
||||
id: '1',
|
||||
issue: {
|
||||
__typename: 'Issue',
|
||||
id: 'gid://gitlab/Issue/1',
|
||||
createdAt: '2020-07-01T04:08:01Z',
|
||||
state: 'opened',
|
||||
title: 'Issue title',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mountComponent = ({
|
||||
queryResponse = jest.fn().mockResolvedValue(issueQueryResponse),
|
||||
} = {}) => {
|
||||
|
@ -77,5 +64,31 @@ describe('Issue Popover', () => {
|
|||
it('shows reference', () => {
|
||||
expect(wrapper.text()).toContain('foo/bar#1');
|
||||
});
|
||||
|
||||
it('shows confidential icon', () => {
|
||||
const icon = wrapper.findComponent(GlIcon);
|
||||
|
||||
expect(icon.exists()).toBe(true);
|
||||
expect(icon.props('name')).toBe('eye-slash');
|
||||
});
|
||||
|
||||
it('shows due date', () => {
|
||||
const component = wrapper.findComponent(IssueDueDate);
|
||||
|
||||
expect(component.exists()).toBe(true);
|
||||
expect(component.props('date')).toBe('2020-07-05');
|
||||
expect(component.props('closed')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows milestone', () => {
|
||||
const component = wrapper.findComponent(IssueMilestone);
|
||||
|
||||
expect(component.exists()).toBe(true);
|
||||
expect(component.props('milestone')).toMatchObject({
|
||||
title: '15.2',
|
||||
startDate: '2020-07-01',
|
||||
dueDate: '2020-07-30',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
require 'rspec-benchmark'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include RSpec::Benchmark::Matchers
|
||||
end
|
||||
|
||||
RSpec.describe Gitlab::Utils::StrongMemoize do
|
||||
let(:klass) do
|
||||
struct = Struct.new(:value) do
|
||||
strong_memoize_class = described_class
|
||||
|
||||
Struct.new(:value) do
|
||||
include strong_memoize_class
|
||||
|
||||
def self.method_added_list
|
||||
@method_added_list ||= []
|
||||
end
|
||||
|
||||
def self.method_added(name)
|
||||
method_added_list << name
|
||||
end
|
||||
|
||||
def method_name
|
||||
strong_memoize(:method_name) do
|
||||
trace << value
|
||||
|
@ -12,21 +29,56 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
|
|||
end
|
||||
end
|
||||
|
||||
def method_name_attr
|
||||
trace << value
|
||||
value
|
||||
end
|
||||
strong_memoize_attr :method_name_attr
|
||||
|
||||
strong_memoize_attr :different_method_name_attr, :different_member_name_attr
|
||||
def different_method_name_attr
|
||||
trace << value
|
||||
value
|
||||
end
|
||||
|
||||
strong_memoize_attr :enabled?
|
||||
def enabled?
|
||||
true
|
||||
end
|
||||
|
||||
def trace
|
||||
@trace ||= []
|
||||
end
|
||||
end
|
||||
|
||||
struct.include(described_class)
|
||||
struct
|
||||
protected
|
||||
|
||||
def private_method
|
||||
end
|
||||
private :private_method
|
||||
strong_memoize_attr :private_method
|
||||
|
||||
public
|
||||
|
||||
def protected_method
|
||||
end
|
||||
protected :protected_method
|
||||
strong_memoize_attr :protected_method
|
||||
|
||||
private
|
||||
|
||||
def public_method
|
||||
end
|
||||
public :public_method
|
||||
strong_memoize_attr :public_method
|
||||
end
|
||||
end
|
||||
|
||||
subject(:object) { klass.new(value) }
|
||||
|
||||
shared_examples 'caching the value' do
|
||||
it 'only calls the block once' do
|
||||
value0 = object.method_name
|
||||
value1 = object.method_name
|
||||
value0 = object.send(method_name)
|
||||
value1 = object.send(method_name)
|
||||
|
||||
expect(value0).to eq(value)
|
||||
expect(value1).to eq(value)
|
||||
|
@ -34,8 +86,8 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
|
|||
end
|
||||
|
||||
it 'returns and defines the instance variable for the exact value' do
|
||||
returned_value = object.method_name
|
||||
memoized_value = object.instance_variable_get(:@method_name)
|
||||
returned_value = object.send(method_name)
|
||||
memoized_value = object.instance_variable_get(:"@#{member_name}")
|
||||
|
||||
expect(returned_value).to eql(value)
|
||||
expect(memoized_value).to eql(value)
|
||||
|
@ -46,12 +98,19 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
|
|||
[nil, false, true, 'value', 0, [0]].each do |value|
|
||||
context "with value #{value}" do
|
||||
let(:value) { value }
|
||||
let(:method_name) { :method_name }
|
||||
let(:member_name) { :method_name }
|
||||
|
||||
it_behaves_like 'caching the value'
|
||||
|
||||
it 'raises exception for invalid key' do
|
||||
it 'raises exception for invalid type as key' do
|
||||
expect { object.strong_memoize(10) { 20 } }.to raise_error /Invalid type of '10'/
|
||||
end
|
||||
|
||||
it 'raises exception for invalid characters in key' do
|
||||
expect { object.strong_memoize(:enabled?) { 20 } }
|
||||
.to raise_error /is not allowed as an instance variable name/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -109,4 +168,64 @@ RSpec.describe Gitlab::Utils::StrongMemoize do
|
|||
expect(object.instance_variable_defined?(:@method_name)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.strong_memoize_attr' do
|
||||
[nil, false, true, 'value', 0, [0]].each do |value|
|
||||
let(:value) { value }
|
||||
|
||||
context "memoized after method definition with value #{value}" do
|
||||
let(:method_name) { :method_name_attr }
|
||||
let(:member_name) { :method_name_attr }
|
||||
|
||||
it_behaves_like 'caching the value'
|
||||
|
||||
it 'calls the existing .method_added' do
|
||||
expect(klass.method_added_list).to include(:method_name_attr)
|
||||
end
|
||||
end
|
||||
|
||||
context "memoized before method definition with different member name and value #{value}" do
|
||||
let(:method_name) { :different_method_name_attr }
|
||||
let(:member_name) { :different_member_name_attr }
|
||||
|
||||
it_behaves_like 'caching the value'
|
||||
|
||||
it 'calls the existing .method_added' do
|
||||
expect(klass.method_added_list).to include(:different_method_name_attr)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid method name' do
|
||||
let(:method_name) { :enabled? }
|
||||
|
||||
context 'with invalid member name' do
|
||||
let(:member_name) { :enabled? }
|
||||
|
||||
it 'is invalid' do
|
||||
expect { object.send(method_name) { value } }.to raise_error /is not allowed as an instance variable name/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'method visibility' do
|
||||
it 'sets private visibility' do
|
||||
expect(klass.private_instance_methods).to include(:private_method)
|
||||
expect(klass.protected_instance_methods).not_to include(:private_method)
|
||||
expect(klass.public_instance_methods).not_to include(:private_method)
|
||||
end
|
||||
|
||||
it 'sets protected visibility' do
|
||||
expect(klass.private_instance_methods).not_to include(:protected_method)
|
||||
expect(klass.protected_instance_methods).to include(:protected_method)
|
||||
expect(klass.public_instance_methods).not_to include(:protected_method)
|
||||
end
|
||||
|
||||
it 'sets public visibility' do
|
||||
expect(klass.private_instance_methods).not_to include(:public_method)
|
||||
expect(klass.protected_instance_methods).not_to include(:public_method)
|
||||
expect(klass.public_instance_methods).to include(:public_method)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe Integration do
|
|||
describe "Associations" do
|
||||
it { is_expected.to belong_to(:project).inverse_of(:integrations) }
|
||||
it { is_expected.to belong_to(:group).inverse_of(:integrations) }
|
||||
it { is_expected.to have_one(:issue_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:service_id).class_name('Integrations::IssueTrackerData') }
|
||||
it { is_expected.to have_one(:issue_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::IssueTrackerData') }
|
||||
it { is_expected.to have_one(:jira_tracker_data).autosave(true).inverse_of(:integration).with_foreign_key(:integration_id).class_name('Integrations::JiraTrackerData') }
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe IssuePolicy do
|
||||
include_context 'ProjectPolicyTable context'
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include ProjectHelpers
|
||||
|
||||
let(:guest) { create(:user) }
|
||||
let(:author) { create(:user) }
|
||||
|
@ -50,6 +52,19 @@ RSpec.describe IssuePolicy do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'grants the expected permissions' do |policy|
|
||||
specify do
|
||||
enable_admin_mode!(user) if admin_mode
|
||||
update_feature_access_level(project, feature_access_level)
|
||||
|
||||
if expected_count == 1
|
||||
expect(permissions(user, issue)).to be_allowed(policy)
|
||||
else
|
||||
expect(permissions(user, issue)).to be_disallowed(policy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'a private project' do
|
||||
let(:project) { create(:project, :private) }
|
||||
let(:issue) { create(:issue, project: project, assignees: [assignee], author: author) }
|
||||
|
@ -85,7 +100,6 @@ RSpec.describe IssuePolicy do
|
|||
|
||||
it 'allows reporters from group links to read, update, and admin issues' do
|
||||
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
|
||||
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
|
||||
expect(permissions(reporter_from_group_link, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
|
||||
end
|
||||
|
||||
|
@ -217,7 +231,7 @@ RSpec.describe IssuePolicy do
|
|||
|
||||
it 'allows reporters from group links to read, update, reopen and admin issues' do
|
||||
expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
|
||||
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :reopen_issue, :set_issue_metadata, :set_confidentiality)
|
||||
expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:reopen_issue)
|
||||
expect(permissions(reporter_from_group_link, issue_locked)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue, :set_issue_metadata, :set_confidentiality)
|
||||
expect(permissions(reporter_from_group_link, issue_locked)).to be_disallowed(:reopen_issue)
|
||||
expect(permissions(reporter, new_issue)).to be_allowed(:create_issue, :set_issue_metadata, :set_confidentiality)
|
||||
|
@ -454,7 +468,7 @@ RSpec.describe IssuePolicy do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when peronsal namespace' do
|
||||
context 'when personal namespace' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
it 'is disallowed' do
|
||||
|
@ -465,4 +479,34 @@ RSpec.describe IssuePolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is an inherited member from the group' do
|
||||
let(:user) { create_user_from_membership(group, membership) }
|
||||
let(:project) { create(:project, project_level, group: group) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
|
||||
context 'and policy allows guest access' do
|
||||
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
|
||||
permission_table_for_guest_feature_access
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'grants the expected permissions', :read_issue
|
||||
it_behaves_like 'grants the expected permissions', :read_issue_iid
|
||||
end
|
||||
end
|
||||
|
||||
context 'and policy allows reporter access' do
|
||||
where(:project_level, :feature_access_level, :membership, :admin_mode, :expected_count) do
|
||||
permission_table_for_reporter_issue_access
|
||||
end
|
||||
|
||||
with_them do
|
||||
it_behaves_like 'grants the expected permissions', :update_issue
|
||||
it_behaves_like 'grants the expected permissions', :admin_issue
|
||||
it_behaves_like 'grants the expected permissions', :set_issue_metadata
|
||||
it_behaves_like 'grants the expected permissions', :set_confidentiality
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,8 @@ RSpec.describe API::Issues do
|
|||
create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace, merge_requests_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
|
||||
let(:user2) { create(:user) }
|
||||
let(:non_member) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
|
@ -85,6 +87,8 @@ RSpec.describe API::Issues do
|
|||
end
|
||||
|
||||
before_all do
|
||||
group.add_reporter(user)
|
||||
group.add_guest(guest)
|
||||
project.add_reporter(user)
|
||||
project.add_guest(guest)
|
||||
private_mrs_project.add_reporter(user)
|
||||
|
@ -107,6 +111,22 @@ RSpec.describe API::Issues do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'returns project issues without confidential issues for guests' do
|
||||
specify do
|
||||
get api(api_url, guest)
|
||||
|
||||
expect_paginated_array_response_contain_exactly(open_issue.id, closed_issue.id)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'returns all project issues for reporters' do
|
||||
specify do
|
||||
get api(api_url, user)
|
||||
|
||||
expect_paginated_array_response_contain_exactly(open_issue.id, confidential_issue.id, closed_issue.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /projects/:id/issues" do
|
||||
let(:base_url) { "/projects/#{project.id}" }
|
||||
|
||||
|
@ -183,6 +203,30 @@ RSpec.describe API::Issues do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when user is an inherited member from the group' do
|
||||
let!(:open_issue) { create(:issue, project: group_project) }
|
||||
let!(:confidential_issue) { create(:issue, :confidential, project: group_project) }
|
||||
let!(:closed_issue) { create(:issue, state: :closed, project: group_project) }
|
||||
|
||||
let!(:api_url) { "/projects/#{group_project.id}/issues" }
|
||||
|
||||
context 'and group project is public and issues are private' do
|
||||
let_it_be(:group_project) do
|
||||
create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group)
|
||||
end
|
||||
|
||||
it_behaves_like 'returns project issues without confidential issues for guests'
|
||||
it_behaves_like 'returns all project issues for reporters'
|
||||
end
|
||||
|
||||
context 'and group project is private' do
|
||||
let_it_be(:group_project) { create(:project, :private, group: group) }
|
||||
|
||||
it_behaves_like 'returns project issues without confidential issues for guests'
|
||||
it_behaves_like 'returns all project issues for reporters'
|
||||
end
|
||||
end
|
||||
|
||||
it 'avoids N+1 queries' do
|
||||
get api("/projects/#{project.id}/issues", user)
|
||||
|
||||
|
|
|
@ -66,6 +66,13 @@ module ApiHelpers
|
|||
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
|
||||
end
|
||||
|
||||
def expect_paginated_array_response_contain_exactly(*items)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.map { |item| item['id'] }).to contain_exactly(*items)
|
||||
end
|
||||
|
||||
def stub_last_activity_update
|
||||
allow_any_instance_of(Users::ActivityService).to receive(:execute)
|
||||
end
|
||||
|
|
|
@ -545,5 +545,62 @@ RSpec.shared_context 'ProjectPolicyTable context' do
|
|||
:private | :non_member | nil | 0
|
||||
:private | :anonymous | nil | 0
|
||||
end
|
||||
|
||||
# Based on the permission_table_for_reporter_feature_access table, but for issue
|
||||
# features where public and internal projects with issues enabled only allow
|
||||
# access to reporters and above (excluding admins if admin mode is disabled)
|
||||
#
|
||||
# project_level, :feature_access_level, :membership, :admin_mode, :expected_count
|
||||
def permission_table_for_reporter_issue_access
|
||||
:public | :enabled | :admin | true | 1
|
||||
:public | :enabled | :admin | false | 0
|
||||
:public | :enabled | :reporter | nil | 1
|
||||
:public | :enabled | :guest | nil | 0
|
||||
:public | :enabled | :non_member | nil | 0
|
||||
:public | :enabled | :anonymous | nil | 0
|
||||
|
||||
:public | :private | :admin | true | 1
|
||||
:public | :private | :admin | false | 0
|
||||
:public | :private | :reporter | nil | 1
|
||||
:public | :private | :guest | nil | 0
|
||||
:public | :private | :non_member | nil | 0
|
||||
:public | :private | :anonymous | nil | 0
|
||||
|
||||
:public | :disabled | :reporter | nil | 0
|
||||
:public | :disabled | :guest | nil | 0
|
||||
:public | :disabled | :non_member | nil | 0
|
||||
:public | :disabled | :anonymous | nil | 0
|
||||
|
||||
:internal | :enabled | :admin | true | 1
|
||||
:internal | :enabled | :admin | false | 0
|
||||
:internal | :enabled | :reporter | nil | 1
|
||||
:internal | :enabled | :guest | nil | 0
|
||||
:internal | :enabled | :non_member | nil | 0
|
||||
:internal | :enabled | :anonymous | nil | 0
|
||||
|
||||
:internal | :private | :admin | true | 1
|
||||
:internal | :private | :admin | false | 0
|
||||
:internal | :private | :reporter | nil | 1
|
||||
:internal | :private | :guest | nil | 0
|
||||
:internal | :private | :non_member | nil | 0
|
||||
:internal | :private | :anonymous | nil | 0
|
||||
|
||||
:internal | :disabled | :reporter | nil | 0
|
||||
:internal | :disabled | :guest | nil | 0
|
||||
:internal | :disabled | :non_member | nil | 0
|
||||
:internal | :disabled | :anonymous | nil | 0
|
||||
|
||||
:private | :private | :admin | true | 1
|
||||
:private | :private | :admin | false | 0
|
||||
:private | :private | :reporter | nil | 1
|
||||
:private | :private | :guest | nil | 0
|
||||
:private | :private | :non_member | nil | 0
|
||||
:private | :private | :anonymous | nil | 0
|
||||
|
||||
:private | :disabled | :reporter | nil | 0
|
||||
:private | :disabled | :guest | nil | 0
|
||||
:private | :disabled | :non_member | nil | 0
|
||||
:private | :disabled | :anonymous | nil | 0
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue