Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-04 12:11:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-04 12:11:20 +0300
commit266aad4e70f3c642583ab60894b27b2622095cd8 (patch)
treeca0959c1c5bf9a11d0ce2ae6f736504b4a48ebbb
parent87e82d6f2cc282a2c70535b4a7fb44b5a6dc8bf0 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue16
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue6
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue4
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue11
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue19
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js34
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js37
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue (renamed from app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue)37
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js19
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/components/design_management/design_version_dropdown.scss3
-rw-r--r--app/assets/stylesheets/components/project_list_item.scss24
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/banner.scss40
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss54
-rw-r--r--doc/ci/pipelines/multi_project_pipelines.md2
-rw-r--r--doc/ci/yaml/index.md2
-rw-r--r--locale/gitlab.pot5
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js12
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap4
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js24
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js5
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js29
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js33
-rw-r--r--spec/frontend/members/mock_data.js3
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js116
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js43
-rw-r--r--spec/frontend/vue_shared/oncall_schedules_list_spec.js87
35 files changed, 405 insertions, 314 deletions
diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue
index a0f4a4bf382..e6dde5898e7 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete.vue
@@ -14,7 +14,7 @@ export default {
type: Object,
required: true,
},
- oncallSchedules: {
+ userDeletionObstacles: {
type: Array,
required: false,
default: () => [],
@@ -29,7 +29,7 @@ export default {
:username="username"
:paths="paths"
:delete-path="paths.delete"
- :oncall-schedules="oncallSchedules"
+ :user-deletion-obstacles="userDeletionObstacles"
>
<slot></slot>
</shared-delete-action>
diff --git a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
index 02fd3efafa1..bd920a91516 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
@@ -14,7 +14,7 @@ export default {
type: Object,
required: true,
},
- oncallSchedules: {
+ userDeletionObstacles: {
type: Array,
required: false,
default: () => [],
@@ -29,7 +29,7 @@ export default {
:username="username"
:paths="paths"
:delete-path="paths.deleteWithContributions"
- :oncall-schedules="oncallSchedules"
+ :user-deletion-obstacles="userDeletionObstacles"
>
<slot></slot>
</shared-delete-action>
diff --git a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
index a1589c9d46d..c9f29b55dbf 100644
--- a/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
+++ b/app/assets/javascripts/admin/users/components/actions/shared/shared_delete_action.vue
@@ -22,7 +22,7 @@ export default {
type: String,
required: true,
},
- oncallSchedules: {
+ userDeletionObstacles: {
type: Array,
required: true,
},
@@ -34,7 +34,7 @@ export default {
'data-delete-user-url': this.deletePath,
'data-gl-modal-action': this.modalType,
'data-username': this.username,
- 'data-oncall-schedules': JSON.stringify(this.oncallSchedules),
+ 'data-user-deletion-obstacles': JSON.stringify(this.userDeletionObstacles),
};
},
},
diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
index aaf2ff313a0..ed90343777d 100644
--- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
+++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
@@ -2,7 +2,7 @@
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__, sprintf } from '~/locale';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
export default {
components: {
@@ -10,7 +10,7 @@ export default {
GlButton,
GlFormInput,
GlSprintf,
- OncallSchedulesList,
+ UserDeletionObstaclesList,
},
props: {
title: {
@@ -45,7 +45,7 @@ export default {
type: String,
required: true,
},
- oncallSchedules: {
+ userDeletionObstacles: {
type: String,
required: false,
default: '[]',
@@ -66,9 +66,9 @@ export default {
canSubmit() {
return this.enteredUsername === this.username;
},
- schedules() {
+ obstacles() {
try {
- return JSON.parse(this.oncallSchedules);
+ return JSON.parse(this.userDeletionObstacles);
} catch (e) {
Sentry.captureException(e);
}
@@ -112,7 +112,11 @@ export default {
</gl-sprintf>
</p>
- <oncall-schedules-list v-if="schedules.length" :schedules="schedules" :user-name="username" />
+ <user-deletion-obstacles-list
+ v-if="obstacles.length"
+ :obstacles="obstacles"
+ :user-name="username"
+ />
<p>
<gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index c076e0bedf0..4f4e2947341 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -9,6 +9,7 @@ import {
} from '@gitlab/ui';
import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { I18N_USER_ACTIONS } from '../constants';
import { generateUserPaths } from '../utils';
import Actions from './actions';
@@ -72,6 +73,9 @@ export default {
href: this.userPaths.edit,
};
},
+ obstaclesForUserDeletion() {
+ return parseUserDeletionObstacles(this.user);
+ },
},
methods: {
isLdapAction(action) {
@@ -141,7 +145,7 @@ export default {
:key="action"
:paths="userPaths"
:username="user.name"
- :oncall-schedules="user.oncallSchedules"
+ :user-deletion-obstacles="obstaclesForUserDeletion"
:data-testid="`delete-${action}`"
>
{{ $options.i18n[action] }}
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index 665e8ee69f7..69137ce615b 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -42,7 +42,7 @@ export default {
required: false,
default: false,
},
- oncallSchedules: {
+ userDeletionObstacles: {
type: Object,
required: false,
default: () => ({}),
@@ -61,7 +61,7 @@ export default {
memberPath: this.memberPath.replace(':id', this.memberId),
memberType: this.memberType,
message: this.message,
- oncallSchedules: this.oncallSchedules,
+ userDeletionObstacles: this.userDeletionObstacles,
};
},
},
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 0c20f935d50..44d658c90a0 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -1,5 +1,6 @@
<script>
import { s__, sprintf } from '~/locale';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import ActionButtonGroup from './action_button_group.vue';
import LeaveButton from './leave_button.vue';
import RemoveMemberButton from './remove_member_button.vue';
@@ -49,9 +50,11 @@ export default {
},
);
},
- oncallScheduleUserData() {
- const { user: { name, oncallSchedules: schedules } = {} } = this.member;
- return { name, schedules };
+ userDeletionObstaclesUserData() {
+ return {
+ name: this.member.user?.name,
+ obstacles: parseUserDeletionObstacles(this.member.user),
+ };
},
},
};
@@ -65,7 +68,7 @@ export default {
v-else
:member-id="member.id"
:member-type="member.type"
- :oncall-schedules="oncallScheduleUserData"
+ :user-deletion-obstacles="userDeletionObstaclesUserData"
:message="message"
:title="s__('Member|Remove member')"
/>
diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index 44178981136..e39669e17dd 100644
--- a/app/assets/javascripts/members/components/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -3,7 +3,8 @@ import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { LEAVE_MODAL_ID } from '../../constants';
export default {
@@ -20,7 +21,7 @@ export default {
csrf,
modalId: LEAVE_MODAL_ID,
modalContent: s__('Members|Are you sure you want to leave "%{source}"?'),
- components: { GlModal, GlForm, GlSprintf, OncallSchedulesList },
+ components: { GlModal, GlForm, GlSprintf, UserDeletionObstaclesList },
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -43,11 +44,11 @@ export default {
modalTitle() {
return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName });
},
- schedules() {
- return this.member.user?.oncallSchedules;
+ obstacles() {
+ return parseUserDeletionObstacles(this.member.user);
},
- isPartOfOnCallSchedules() {
- return this.schedules?.length;
+ hasObstaclesToUserDeletion() {
+ return this.obstacles?.length;
},
},
methods: {
@@ -74,9 +75,9 @@ export default {
</gl-sprintf>
</p>
- <oncall-schedules-list
- v-if="isPartOfOnCallSchedules"
- :schedules="schedules"
+ <user-deletion-obstacles-list
+ v-if="hasObstaclesToUserDeletion"
+ :obstacles="obstacles"
:is-current-user="true"
/>
diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
index 00b6ebf9a73..b82fb0030ff 100644
--- a/app/assets/javascripts/members/components/modals/remove_member_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -3,7 +3,7 @@ import { GlFormCheckbox, GlModal } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { s__, __ } from '~/locale';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
export default {
actionCancel: {
@@ -13,7 +13,7 @@ export default {
components: {
GlFormCheckbox,
GlModal,
- OncallSchedulesList,
+ UserDeletionObstaclesList,
},
inject: ['namespace'],
computed: {
@@ -33,8 +33,8 @@ export default {
message(state) {
return state[this.namespace].removeMemberModalData.message;
},
- oncallSchedules(state) {
- return state[this.namespace].removeMemberModalData.oncallSchedules ?? {};
+ userDeletionObstacles(state) {
+ return state[this.namespace].removeMemberModalData.userDeletionObstacles ?? {};
},
removeMemberModalVisible(state) {
return state[this.namespace].removeMemberModalVisible;
@@ -60,11 +60,11 @@ export default {
},
};
},
- showUnassignIssuablesCheckbox() {
+ hasWorkspaceAccess() {
return !this.isAccessRequest && !this.isInvite;
},
- isPartOfOncallSchedules() {
- return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
+ hasObstaclesToUserDeletion() {
+ return this.hasWorkspaceAccess && this.userDeletionObstacles.obstacles?.length;
},
},
methods: {
@@ -95,10 +95,10 @@ export default {
<form ref="form" :action="memberPath" method="post">
<p>{{ message }}</p>
- <oncall-schedules-list
- v-if="isPartOfOncallSchedules"
- :schedules="oncallSchedules.schedules"
- :user-name="oncallSchedules.name"
+ <user-deletion-obstacles-list
+ v-if="hasObstaclesToUserDeletion"
+ :obstacles="userDeletionObstacles.obstacles"
+ :user-name="userDeletionObstacles.name"
/>
<input ref="method" type="hidden" name="_method" value="delete" />
@@ -106,7 +106,7 @@ export default {
<gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
{{ __('Also remove direct user membership from subgroups and projects') }}
</gl-form-checkbox>
- <gl-form-checkbox v-if="showUnassignIssuablesCheckbox" name="unassign_issuables">
+ <gl-form-checkbox v-if="hasWorkspaceAccess" name="unassign_issuables">
{{ __('Also unassign this user from related issues and merge requests') }}
</gl-form-checkbox>
</form>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js
new file mode 100644
index 00000000000..9700117a3da
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.stories.js
@@ -0,0 +1,34 @@
+import ProjectListItem from './project_list_item.vue';
+
+export default {
+ component: ProjectListItem,
+ title: 'vue_shared/components/project_selector/project_list_item',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ProjectListItem },
+ props: Object.keys(argTypes),
+ template: '<project-list-item v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ project: {
+ id: '1',
+ name: 'MyProject',
+ name_with_namespace: 'path / to / MyProject',
+ },
+ selected: false,
+};
+
+export const SelectedProject = Template.bind({});
+SelectedProject.args = {
+ ...Default.args,
+ selected: true,
+};
+
+export const MatchedProject = Template.bind({});
+MatchedProject.args = {
+ ...Default.args,
+ matcher: 'proj',
+};
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 36d3696ec36..0bd57c84018 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { isString } from 'lodash';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -8,6 +8,7 @@ import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/def
export default {
name: 'ProjectListItem',
components: { GlIcon, ProjectAvatar, GlButton },
+ directives: { SafeHtml },
props: {
project: {
type: Object,
@@ -58,9 +59,9 @@ export default {
<span v-if="truncatedNamespace" class="text-secondary">/&nbsp;</span>
</div>
<div
+ v-safe-html="highlightedProjectName"
:title="project.name"
class="js-project-name text-truncate"
- v-html="highlightedProjectName /* eslint-disable-line vue/no-v-html */"
></div>
</div>
</gl-button>
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js
new file mode 100644
index 00000000000..256db2ea1ce
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/constants.js
@@ -0,0 +1,5 @@
+// Types of obstacles to user deletion
+export const OBSTACLE_TYPES = Object.freeze({
+ oncallSchedules: 'ONCALL_SCHEDULE',
+ escalationPolicies: 'ESCALATION_POLICY',
+});
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
new file mode 100644
index 00000000000..d2030c14029
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
@@ -0,0 +1,37 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import { OBSTACLE_TYPES } from './constants';
+import UserDeletionObstaclesList from './user_deletion_obstacles_list.vue';
+
+export default {
+ component: UserDeletionObstaclesList,
+ title: 'vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { UserDeletionObstaclesList },
+ props: Object.keys(argTypes),
+ template: '<user-deletion-obstacles-list v-bind="$props" v-on="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ obstacles: [
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'APAC',
+ url: 'https://domain.com/group/main-application/oncall_schedules',
+ projectName: 'main-application',
+ projectUrl: 'https://domain.com/group/main-application',
+ },
+ {
+ type: OBSTACLE_TYPES.escalationPolicies,
+ name: 'Engineering On-call',
+ url: 'https://domain.com/group/microservice-backend/escalation_policies',
+ projectName: 'Microservice Backend',
+ projectUrl: 'https://domain.com/group/microservice-backend',
+ },
+ ],
+ userName: 'Thomspon Smith',
+ isCurrentUser: false,
+};
diff --git a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
index e37a663ace3..1eea660d527 100644
--- a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue
@@ -1,6 +1,16 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
+import { OBSTACLE_TYPES } from './constants';
+
+const OBSTACLE_TEXT = {
+ [OBSTACLE_TYPES.oncallSchedules]: s__(
+ 'OnCallSchedules|On-call schedule %{obstacle} in Project %{project}',
+ ),
+ [OBSTACLE_TYPES.escalationPolicies]: s__(
+ 'EscalationPolicies|Escalation policy %{obstacle} in Project %{project}',
+ ),
+};
export default {
components: {
@@ -8,7 +18,7 @@ export default {
GlLink,
},
props: {
- schedules: {
+ obstacles: {
type: Array,
required: true,
},
@@ -45,6 +55,15 @@ export default {
);
},
},
+ methods: {
+ textForObstacle(obstacle) {
+ return OBSTACLE_TEXT[obstacle.type];
+ },
+ urlForObstacle(obstacle) {
+ // Fallback to scheduleUrl for backwards compatibility
+ return obstacle.url || obstacle.scheduleUrl;
+ },
+ },
};
</script>
@@ -52,17 +71,15 @@ export default {
<div>
<p data-testid="title">{{ title }}</p>
- <ul data-testid="schedules-list">
- <li v-for="(schedule, index) in schedules" :key="`${schedule.name}-${index}`">
- <gl-sprintf
- :message="s__('OnCallSchedules|On-call schedule %{schedule} in Project %{project}')"
- >
- <template #schedule>
- <gl-link :href="schedule.scheduleUrl" target="_blank">{{ schedule.name }}</gl-link>
+ <ul data-testid="obstacles-list">
+ <li v-for="(obstacle, index) in obstacles" :key="`${obstacle.name}-${index}`">
+ <gl-sprintf :message="textForObstacle(obstacle)">
+ <template #obstacle>
+ <gl-link :href="urlForObstacle(obstacle)" target="_blank">{{ obstacle.name }}</gl-link>
</template>
<template #project>
- <gl-link :href="schedule.projectUrl" target="_blank">{{
- schedule.projectName
+ <gl-link :href="obstacle.projectUrl" target="_blank">{{
+ obstacle.projectName
}}</gl-link>
</template>
</gl-sprintf>
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js
new file mode 100644
index 00000000000..502302a1ef2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/utils.js
@@ -0,0 +1,19 @@
+import { OBSTACLE_TYPES } from './constants';
+
+const addTypeToObstacles = (obstacles, type) => {
+ if (!obstacles) return [];
+
+ return obstacles?.map((obstacle) => ({ type, ...obstacle }));
+};
+
+// For use with user objects formatted via internal REST API.
+// If the removal/deletion of a user could cause critical
+// problems, return a single array containing all affected
+// associations including their type.
+export const parseUserDeletionObstacles = (user) => {
+ if (!user) return [];
+
+ return Object.keys(OBSTACLE_TYPES).flatMap((type) => {
+ return addTypeToObstacles(user[type], OBSTACLE_TYPES[type]);
+ });
+};
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index dfbdee8e1c8..8f3b5b3b7cc 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -1,5 +1,4 @@
@import './pages/branches';
-@import './pages/ci_projects';
@import './pages/clusters';
@import './pages/commits';
@import './pages/deploy_keys';
diff --git a/app/assets/stylesheets/components/design_management/design_version_dropdown.scss b/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
deleted file mode 100644
index f79d672e238..00000000000
--- a/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.design-version-dropdown > button {
- background: inherit;
-}
diff --git a/app/assets/stylesheets/components/project_list_item.scss b/app/assets/stylesheets/components/project_list_item.scss
deleted file mode 100644
index 8e7c2c4398c..00000000000
--- a/app/assets/stylesheets/components/project_list_item.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.project-list-item {
- &:not(:disabled):not(.disabled) {
- &:focus,
- &:active,
- &:focus:active {
- outline: none;
- box-shadow: none;
- }
- }
-}
-
-// When housed inside a modal, the edge of each item
-// should extend to the edge of the modal.
-.modal-body {
- .project-list-item {
- border-radius: 0;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
-
- .project-namespace-name-container {
- overflow: hidden;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 804cc205279..06a8694eb3d 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -9,7 +9,6 @@
@import 'framework/animations';
@import 'framework/vue_transitions';
-@import 'framework/banner';
@import 'framework/blocks';
@import 'framework/buttons';
@import 'framework/badges';
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
deleted file mode 100644
index 71bbab2065d..00000000000
--- a/app/assets/stylesheets/framework/banner.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-.banner-callout {
- display: flex;
- position: relative;
- align-items: start;
-
- .banner-close {
- position: absolute;
- top: 10px;
- right: 10px;
- opacity: 1;
-
- .dismiss-icon {
- color: $gl-text-color;
- font-size: $gl-font-size;
- }
- }
-
- .banner-graphic {
- margin: 0 $gl-padding $gl-padding 0;
- }
-
- &.banner-non-empty-state {
- border-bottom: 1px solid $border-color;
- }
-
- @include media-breakpoint-down(xs) {
- justify-content: center;
- flex-direction: column;
- align-items: center;
-
- .banner-title,
- .banner-buttons {
- text-align: center;
- }
-
- .banner-graphic {
- margin-left: $gl-padding;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
deleted file mode 100644
index fbe1f3081a0..00000000000
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-.ci-body {
- .project-title {
- margin: 0;
- color: $common-gray-dark;
- font-size: 20px;
- line-height: 1.5;
- }
-
- .builds,
- .projects-table {
- .light {
- border-color: $border-color;
- }
-
- th,
- td {
- padding: 10px $gl-padding;
- }
-
- td {
- color: $gl-text-color;
- vertical-align: middle !important;
-
- a {
- font-weight: $gl-font-weight-normal;
- text-decoration: none;
- }
- }
- }
-
- .commit-info {
- .attr-name {
- margin-right: 5px;
- }
-
- pre.commit-message {
- background: none;
- padding: 0;
- border: 0;
- margin: 20px 0;
- border-radius: 0;
- }
- }
-
- .loading {
- font-size: 20px;
- }
-
- .ci-charts {
- fieldset {
- margin-bottom: 16px;
- }
- }
-}
diff --git a/doc/ci/pipelines/multi_project_pipelines.md b/doc/ci/pipelines/multi_project_pipelines.md
index d31ddcf736e..8390e85d57b 100644
--- a/doc/ci/pipelines/multi_project_pipelines.md
+++ b/doc/ci/pipelines/multi_project_pipelines.md
@@ -88,7 +88,7 @@ The keywords available for use in trigger jobs are:
- [`only` and `except`](../yaml/index.md#only--except)
- [`when`](../yaml/index.md#when) (only with a value of `on_success`, `on_failure`, or `always`)
- [`extends`](../yaml/index.md#extends)
-- [`needs`](../yaml/index.md#needs)
+- [`needs`](../yaml/index.md#needs), but not [cross project artifact downloads with `needs`](../yaml/index.md#cross-project-artifact-downloads-with-needs)
#### Specify a downstream pipeline branch
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index ffc64c4ef70..39a1d9b035e 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -1703,6 +1703,8 @@ same group or namespace, you can omit them from the `project:` keyword. For exam
The user running the pipeline must have at least `reporter` access to the group or project, or the group/project must have public visibility.
+You cannot use cross project artifact downloads in the same job as [`trigger`](#trigger).
+
##### Artifact downloads between pipelines in the same project
Use `needs` to download artifacts from different pipelines in the current project.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index af7be268904..1189431d298 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13558,6 +13558,9 @@ msgstr ""
msgid "EscalationPolicies|Escalation policies"
msgstr ""
+msgid "EscalationPolicies|Escalation policy %{obstacle} in Project %{project}"
+msgstr ""
+
msgid "EscalationPolicies|Escalation rules"
msgstr ""
@@ -23570,7 +23573,7 @@ msgstr ""
msgid "OnCallSchedules|For this rotation, on-call will be:"
msgstr ""
-msgid "OnCallSchedules|On-call schedule %{schedule} in Project %{project}"
+msgid "OnCallSchedules|On-call schedule %{obstacle} in Project %{project}"
msgstr ""
msgid "OnCallSchedules|On-call schedules"
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index fd05b08a3fb..67dcf5c6149 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -5,6 +5,7 @@ import { nextTick } from 'vue';
import Actions from '~/admin/users/components/actions';
import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
import { CONFIRMATION_ACTIONS, DELETE_ACTIONS } from '../../constants';
import { paths } from '../../mock_data';
@@ -46,7 +47,10 @@ describe('Action components', () => {
});
describe('DELETE_ACTION_COMPONENTS', () => {
- const oncallSchedules = [{ name: 'schedule1' }, { name: 'schedule2' }];
+ const userDeletionObstacles = [
+ { name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
+ { name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
+ ];
it.each(DELETE_ACTIONS.map((action) => [action, paths[action]]))(
'renders a dropdown item for "%s"',
@@ -56,7 +60,7 @@ describe('Action components', () => {
props: {
username: 'John Doe',
paths,
- oncallSchedules,
+ userDeletionObstacles,
},
stubs: { SharedDeleteAction },
});
@@ -69,8 +73,8 @@ describe('Action components', () => {
expect(sharedAction.attributes('data-delete-user-url')).toBe(expectedPath);
expect(sharedAction.attributes('data-gl-modal-action')).toBe(kebabCase(action));
expect(sharedAction.attributes('data-username')).toBe('John Doe');
- expect(sharedAction.attributes('data-oncall-schedules')).toBe(
- JSON.stringify(oncallSchedules),
+ expect(sharedAction.attributes('data-user-deletion-obstacles')).toBe(
+ JSON.stringify(userDeletionObstacles),
);
expect(findDropdownItem().exists()).toBe(true);
},
diff --git a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
index 5e367891337..472158a9b10 100644
--- a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
@@ -8,8 +8,8 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
/>
</p>
- <oncall-schedules-list-stub
- schedules="schedule1,schedule2"
+ <user-deletion-obstacles-list-stub
+ obstacles="schedule1,policy1"
username="username"
/>
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index fee74764645..82307c9e3b3 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import ModalStub from './stubs/modal_stub';
const TEST_DELETE_USER_URL = 'delete-url';
@@ -25,7 +25,7 @@ describe('User Operation confirmation modal', () => {
const getUsername = () => findUsernameInput().attributes('value');
const getMethodParam = () => new FormData(findForm().element).get('_method');
const getFormAction = () => findForm().attributes('action');
- const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
+ const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
const setUsername = (username) => {
findUsernameInput().vm.$emit('input', username);
@@ -33,7 +33,7 @@ describe('User Operation confirmation modal', () => {
const username = 'username';
const badUsername = 'bad_username';
- const oncallSchedules = '["schedule1", "schedule2"]';
+ const userDeletionObstacles = '["schedule1", "policy1"]';
const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteUserModal, {
@@ -46,7 +46,7 @@ describe('User Operation confirmation modal', () => {
deleteUserUrl: TEST_DELETE_USER_URL,
blockUserUrl: TEST_BLOCK_USER_URL,
csrfToken: TEST_CSRF,
- oncallSchedules,
+ userDeletionObstacles,
...props,
},
stubs: {
@@ -150,18 +150,18 @@ describe('User Operation confirmation modal', () => {
});
});
- describe('Related oncall-schedules list', () => {
- it('does NOT render the list when user has no related schedules', () => {
- createComponent({ oncallSchedules: '[]' });
- expect(findOnCallSchedulesList().exists()).toBe(false);
+ describe('Related user-deletion-obstacles list', () => {
+ it('does NOT render the list when user has no related obstacles', () => {
+ createComponent({ userDeletionObstacles: '[]' });
+ expect(findUserDeletionObstaclesList().exists()).toBe(false);
});
- it('renders the list when user has related schedules', () => {
+ it('renders the list when user has related obstalces', () => {
createComponent();
- const schedules = findOnCallSchedulesList();
- expect(schedules.exists()).toBe(true);
- expect(schedules.props('schedules')).toEqual(JSON.parse(oncallSchedules));
+ const obstacles = findUserDeletionObstaclesList();
+ expect(obstacles.exists()).toBe(true);
+ expect(obstacles.props('obstacles')).toEqual(JSON.parse(userDeletionObstacles));
});
});
});
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index d8453d453e7..7eb0ea37fe6 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -45,7 +45,7 @@ describe('RemoveMemberButton', () => {
title: 'Remove member',
isAccessRequest: true,
isInvite: true,
- oncallSchedules: { name: 'user', schedules: [] },
+ userDeletionObstacles: { name: 'user', obstacles: [] },
...propsData,
},
directives: {
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
index 0aa3780f030..10e451376c8 100644
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { member, orphanedMember } from '../../mock_data';
describe('UserActionButtons', () => {
@@ -45,9 +46,9 @@ describe('UserActionButtons', () => {
isAccessRequest: false,
isInvite: false,
icon: 'remove',
- oncallSchedules: {
+ userDeletionObstacles: {
name: member.user.name,
- schedules: member.user.oncallSchedules,
+ obstacles: parseUserDeletionObstacles(member.user),
},
});
});
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index 1dc913e5c78..f755f08dbf2 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -6,7 +6,8 @@ import { nextTick } from 'vue';
import Vuex from 'vuex';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { member } from '../../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -51,7 +52,7 @@ describe('LeaveModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => findModal().findComponent(GlForm);
- const findOncallSchedulesList = () => findModal().findComponent(OncallSchedulesList);
+ const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList);
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
@@ -89,25 +90,27 @@ describe('LeaveModal', () => {
);
});
- describe('On-call schedules list', () => {
- it("displays oncall schedules list when member's user is part of on-call schedules ", () => {
- const schedulesList = findOncallSchedulesList();
- expect(schedulesList.exists()).toBe(true);
- expect(schedulesList.props()).toMatchObject({
+ describe('User deletion obstacles list', () => {
+ it("displays obstacles list when member's user is part of on-call management", () => {
+ const obstaclesList = findUserDeletionObstaclesList();
+ expect(obstaclesList.exists()).toBe(true);
+ expect(obstaclesList.props()).toMatchObject({
isCurrentUser: true,
- schedules: member.user.oncallSchedules,
+ obstacles: parseUserDeletionObstacles(member.user),
});
});
- it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", async () => {
+ it("does NOT display obstacles list when member's user is NOT a part of on-call management", async () => {
wrapper.destroy();
- const memberWithoutOncallSchedules = cloneDeep(member);
- delete memberWithoutOncallSchedules.user.oncallSchedules;
- createComponent({ member: memberWithoutOncallSchedules });
+ const memberWithoutOncall = cloneDeep(member);
+ delete memberWithoutOncall.user.oncallSchedules;
+ delete memberWithoutOncall.user.escalationPolicies;
+
+ createComponent({ member: memberWithoutOncall });
await nextTick();
- expect(findOncallSchedulesList().exists()).toBe(false);
+ expect(findUserDeletionObstaclesList().exists()).toBe(false);
});
});
diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js
index 1dc41582c12..1d39c4b3175 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -4,15 +4,19 @@ import Vue from 'vue';
import Vuex from 'vuex';
import RemoveMemberModal from '~/members/components/modals/remove_member_modal.vue';
import { MEMBER_TYPES } from '~/members/constants';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
Vue.use(Vuex);
describe('RemoveMemberModal', () => {
const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
- const mockSchedules = {
+ const mockObstacles = {
name: 'User1',
- schedules: [{ id: 1, name: 'Schedule 1' }],
+ obstacles: [
+ { name: 'Schedule 1', type: OBSTACLE_TYPES.oncallSchedules },
+ { name: 'Policy 1', type: OBSTACLE_TYPES.escalationPolicies },
+ ],
};
let wrapper;
@@ -44,18 +48,18 @@ describe('RemoveMemberModal', () => {
const findForm = () => wrapper.find({ ref: 'form' });
const findGlModal = () => wrapper.findComponent(GlModal);
- const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
+ const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
afterEach(() => {
wrapper.destroy();
});
describe.each`
- state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules
- ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}}
- ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
- ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}}
- ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
+ state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
+ ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
+ ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${true}
+ ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} | ${false}
+ ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${false}
`(
'when $state',
({
@@ -66,7 +70,8 @@ describe('RemoveMemberModal', () => {
message,
removeSubMembershipsCheckboxExpected,
unassignIssuablesCheckboxExpected,
- onCallSchedules,
+ userDeletionObstacles,
+ isPartOfOncall,
}) => {
beforeEach(() => {
createComponent({
@@ -75,12 +80,10 @@ describe('RemoveMemberModal', () => {
message,
memberPath,
memberType,
- onCallSchedules,
+ userDeletionObstacles,
});
});
- const isPartOfOncallSchedules = Boolean(isAccessRequest && onCallSchedules.schedules?.length);
-
it(`has the title ${actionText}`, () => {
expect(findGlModal().attributes('title')).toBe(actionText);
});
@@ -109,8 +112,8 @@ describe('RemoveMemberModal', () => {
);
});
- it(`shows ${isPartOfOncallSchedules ? 'all' : 'no'} related on-call schedules`, () => {
- expect(findOnCallSchedulesList().exists()).toBe(isPartOfOncallSchedules);
+ it(`shows ${isPartOfOncall ? 'all' : 'no'} related on-call schedules or policies`, () => {
+ expect(findUserDeletionObstaclesList().exists()).toBe(isPartOfOncall);
});
it('submits the form when the modal is submitted', () => {
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index eb9f905fea2..3afbe57a1aa 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -23,6 +23,7 @@ export const member = {
blocked: false,
twoFactorEnabled: false,
oncallSchedules: [{ name: 'schedule 1' }],
+ escalationPolicies: [{ name: 'policy 1' }],
},
id: 238,
createdAt: '2020-07-17T16:22:46.923Z',
@@ -63,7 +64,7 @@ export const modalData = {
memberPath: '/groups/foo-bar/-/group_members/1',
memberType: 'GroupMember',
message: 'Are you sure you want to remove John Smith?',
- oncallSchedules: { name: 'user', schedules: [] },
+ userDeletionObstacles: { name: 'user', obstacles: [] },
};
const { user, ...memberNoUser } = member;
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
new file mode 100644
index 00000000000..a92f058f311
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -0,0 +1,116 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+
+const mockSchedules = [
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'Schedule 1',
+ url: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
+ projectName: 'Shell',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
+ },
+ {
+ type: OBSTACLE_TYPES.oncallSchedules,
+ name: 'Schedule 2',
+ url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
+ projectName: 'UI',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
+ },
+];
+const mockPolicies = [
+ {
+ type: OBSTACLE_TYPES.escalationPolicies,
+ name: 'Policy 1',
+ url: 'http://gitlab.com/gitlab-org/gitlab-ui/-/escalation-policies',
+ projectName: 'UI',
+ projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
+ },
+];
+const mockObstacles = mockSchedules.concat(mockPolicies);
+
+const userName = "O'User";
+
+describe('User deletion obstacles list', () => {
+ let wrapper;
+
+ function createComponent(props) {
+ wrapper = extendedWrapper(
+ shallowMount(UserDeletionObstaclesList, {
+ propsData: {
+ obstacles: mockObstacles,
+ userName,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ }),
+ );
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findFooter = () => wrapper.findByTestId('footer');
+ const findObstacles = () => wrapper.findByTestId('obstacles-list');
+
+ describe.each`
+ isCurrentUser | titleText | footerText
+ ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
+ ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
+ `('when current user', ({ isCurrentUser, titleText, footerText }) => {
+ it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, async () => {
+ createComponent({
+ isCurrentUser,
+ });
+
+ expect(findTitle().text()).toBe(titleText);
+ expect(findFooter().text()).toBe(footerText);
+ });
+ });
+
+ describe.each(mockObstacles)(
+ 'renders all obstacles',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the project name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`in Project ${projectName}`);
+ expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
+ });
+ },
+ );
+
+ describe.each(mockSchedules)(
+ 'renders on-call schedules',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the schedule name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`On-call schedule ${name}`);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+ },
+ );
+
+ describe.each(mockPolicies)(
+ 'renders escalation policies',
+ ({ type, name, url, projectName, projectUrl }) => {
+ it(`includes the policy name and link for ${name}`, () => {
+ createComponent({ obstacles: [{ type, name, url, projectName, projectUrl }] });
+ const msg = findObstacles().text();
+
+ expect(msg).toContain(`Escalation policy ${name}`);
+ expect(findLinks().at(0).attributes('href')).toBe(url);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js
new file mode 100644
index 00000000000..99f739098f7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/utils_spec.js
@@ -0,0 +1,43 @@
+import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
+
+describe('parseUserDeletionObstacles', () => {
+ const mockObstacles = [{ name: 'Obstacle' }];
+ const expectedSchedule = { name: 'Obstacle', type: OBSTACLE_TYPES.oncallSchedules };
+ const expectedPolicy = { name: 'Obstacle', type: OBSTACLE_TYPES.escalationPolicies };
+
+ it('is undefined when user is not available', () => {
+ expect(parseUserDeletionObstacles()).toHaveLength(0);
+ });
+
+ it('is empty when obstacles are not available for user', () => {
+ expect(parseUserDeletionObstacles({})).toHaveLength(0);
+ });
+
+ it('is empty when user has no obstacles to deletion', () => {
+ const input = { oncallSchedules: [], escalationPolicies: [] };
+
+ expect(parseUserDeletionObstacles(input)).toHaveLength(0);
+ });
+
+ it('returns obstacles with type when user is part of on-call schedules', () => {
+ const input = { oncallSchedules: mockObstacles, escalationPolicies: [] };
+ const expectedOutput = [expectedSchedule];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+
+ it('returns obstacles with type when user is part of escalation policies', () => {
+ const input = { oncallSchedules: [], escalationPolicies: mockObstacles };
+ const expectedOutput = [expectedPolicy];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+
+ it('returns obstacles with type when user have every obstacle type', () => {
+ const input = { oncallSchedules: mockObstacles, escalationPolicies: mockObstacles };
+ const expectedOutput = [expectedSchedule, expectedPolicy];
+
+ expect(parseUserDeletionObstacles(input)).toEqual(expectedOutput);
+ });
+});
diff --git a/spec/frontend/vue_shared/oncall_schedules_list_spec.js b/spec/frontend/vue_shared/oncall_schedules_list_spec.js
deleted file mode 100644
index f83a5187b8b..00000000000
--- a/spec/frontend/vue_shared/oncall_schedules_list_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
-
-const mockSchedules = [
- {
- name: 'Schedule 1',
- scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
- projectName: 'Shell',
- projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
- },
- {
- name: 'Schedule 2',
- scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
- projectName: 'UI',
- projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
- },
-];
-
-const userName = "O'User";
-
-describe('On-call schedules list', () => {
- let wrapper;
-
- function createComponent(props) {
- wrapper = extendedWrapper(
- shallowMount(OncallSchedulesList, {
- propsData: {
- schedules: mockSchedules,
- userName,
- ...props,
- },
- stubs: {
- GlSprintf,
- },
- }),
- );
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findLinks = () => wrapper.findAllComponents(GlLink);
- const findTitle = () => wrapper.findByTestId('title');
- const findFooter = () => wrapper.findByTestId('footer');
- const findSchedules = () => wrapper.findByTestId('schedules-list');
-
- describe.each`
- isCurrentUser | titleText | footerText
- ${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
- ${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
- `('when current user ', ({ isCurrentUser, titleText, footerText }) => {
- it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call schedule`, async () => {
- createComponent({
- isCurrentUser,
- });
-
- expect(findTitle().text()).toBe(titleText);
- expect(findFooter().text()).toBe(footerText);
- });
- });
-
- describe.each(mockSchedules)(
- 'renders each on-call schedule data',
- ({ name, scheduleUrl, projectName, projectUrl }) => {
- beforeEach(() => {
- createComponent({ schedules: [{ name, scheduleUrl, projectName, projectUrl }] });
- });
-
- it(`renders schedule ${name}'s name and link`, () => {
- const msg = findSchedules().text();
-
- expect(msg).toContain(`On-call schedule ${name}`);
- expect(findLinks().at(0).attributes('href')).toBe(scheduleUrl);
- });
-
- it(`renders project ${projectName}'s name and link`, () => {
- const msg = findSchedules().text();
-
- expect(msg).toContain(`in Project ${projectName}`);
- expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
- });
- },
- );
-});