Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
87e82d6f2c
commit
266aad4e70
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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}')">
|
||||
|
|
|
@ -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] }}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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')"
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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">/ </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>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Types of obstacles to user deletion
|
||||
export const OBSTACLE_TYPES = Object.freeze({
|
||||
oncallSchedules: 'ONCALL_SCHEDULE',
|
||||
escalationPolicies: 'ESCALATION_POLICY',
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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>
|
|
@ -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]);
|
||||
});
|
||||
};
|
|
@ -1,5 +1,4 @@
|
|||
@import './pages/branches';
|
||||
@import './pages/ci_projects';
|
||||
@import './pages/clusters';
|
||||
@import './pages/commits';
|
||||
@import './pages/deploy_keys';
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.design-version-dropdown > button {
|
||||
background: inherit;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
@import 'framework/animations';
|
||||
@import 'framework/vue_transitions';
|
||||
@import 'framework/banner';
|
||||
@import 'framework/blocks';
|
||||
@import 'framework/buttons';
|
||||
@import 'framework/badges';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ describe('RemoveMemberButton', () => {
|
|||
title: 'Remove member',
|
||||
isAccessRequest: true,
|
||||
isInvite: true,
|
||||
oncallSchedules: { name: 'user', schedules: [] },
|
||||
userDeletionObstacles: { name: 'user', obstacles: [] },
|
||||
...propsData,
|
||||
},
|
||||
directives: {
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue