Commit f1f34baf authored by Samantha Ming's avatar Samantha Ming Committed by Paul Slaughter

Improve UX multi assigness in MR

Add merge warning on avatar in:
- open view assigness
- collapsed view assigness
- dropdown (search) view assigness

Add can_merge option to MR sidebar entity
parent fd589be3
<script>
import { __, sprintf } from '~/locale';
export default {
props: {
user: {
type: Object,
required: true,
},
imgSize: {
type: Number,
required: true,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
computed: {
assigneeAlt() {
return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
},
avatarUrl() {
return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
},
isMergeRequest() {
return this.issuableType === 'merge_request';
},
hasMergeIcon() {
return this.isMergeRequest && !this.user.can_merge;
},
},
};
</script>
<template>
<span class="position-relative">
<img
:alt="assigneeAlt"
:src="avatarUrl"
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
/>
<i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
</span>
</template>
<script>
import { __, sprintf } from '~/locale';
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import AssigneeAvatar from './assignee_avatar.vue';
export default {
components: {
AssigneeAvatar,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
user: {
type: Object,
required: true,
},
rootPath: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
default: 'bottom',
required: false,
},
tooltipHasName: {
type: Boolean,
default: true,
required: false,
},
issuableType: {
type: String,
default: 'issue',
required: false,
},
},
computed: {
cannotMerge() {
return this.issuableType === 'merge_request' && !this.user.can_merge;
},
tooltipTitle() {
if (this.cannotMerge && this.tooltipHasName) {
return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
} else if (this.cannotMerge) {
return __('Cannot merge');
} else if (this.tooltipHasName) {
return this.user.name;
}
return '';
},
tooltipOption() {
return {
container: 'body',
placement: this.tooltipPlacement,
boundary: 'viewport',
};
},
assigneeUrl() {
return joinPaths(`${this.rootPath}`, `${this.user.username}`);
},
},
};
</script>
<template>
<gl-link v-gl-tooltip="tooltipOption" :href="assigneeUrl" :title="tooltipTitle" class="d-flex">
<assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot :user="user"></slot>
</gl-link>
</template>
<script> <script>
import { __, sprintf } from '~/locale'; import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue';
export default { export default {
// name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives // name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Assignees', name: 'Assignees',
directives: { components: {
tooltip, CollapsedAssigneeList,
UncollapsedAssigneeList,
}, },
props: { props: {
rootPath: { rootPath: {
...@@ -28,167 +29,30 @@ export default { ...@@ -28,167 +29,30 @@ export default {
default: 'issue', default: 'issue',
}, },
}, },
data() {
return {
defaultRenderCount: 5,
defaultMaxCounter: 99,
showLess: true,
};
},
computed: { computed: {
firstUser() {
return this.users[0];
},
hasMoreThanTwoAssignees() {
return this.users.length > 2;
},
hasMoreThanOneAssignee() {
return this.users.length > 1;
},
hasAssignees() {
return this.users.length > 0;
},
hasNoUsers() { hasNoUsers() {
return !this.users.length; return !this.users.length;
}, },
hasOneUser() { sortedAssigness() {
return this.users.length === 1; const canMergeUsers = this.users.filter(user => user.can_merge);
}, const canNotMergeUsers = this.users.filter(user => !user.can_merge);
renderShowMoreSection() {
return this.users.length > this.defaultRenderCount;
},
numberOfHiddenAssignees() {
return this.users.length - this.defaultRenderCount;
},
isHiddenAssignees() {
return this.numberOfHiddenAssignees > 0;
},
hiddenAssigneesLabel() {
const { numberOfHiddenAssignees } = this;
return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
},
collapsedTooltipTitle() {
const maxRender = Math.min(this.defaultRenderCount, this.users.length);
const renderUsers = this.users.slice(0, maxRender);
const names = renderUsers.map(u => u.name);
if (this.users.length > maxRender) {
names.push(`+ ${this.users.length - maxRender} more`);
}
if (!this.users.length) {
const emptyTooltipLabel = __('Assignee(s)');
names.push(emptyTooltipLabel);
}
return names.join(', ');
},
sidebarAvatarCounter() {
let counter = `+${this.users.length - 1}`;
if (this.users.length > this.defaultMaxCounter) {
counter = `${this.defaultMaxCounter}+`;
}
return counter; return [...canMergeUsers, ...canNotMergeUsers];
},
mergeNotAllowedTooltipMessage() {
const assigneesCount = this.users.length;
if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
return null;
}
const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
const canMergeCount = assigneesCount - cannotMergeCount;
if (canMergeCount === assigneesCount) {
// Everyone can merge
return null;
} else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
return __('No one can merge');
} else if (assigneesCount === 1) {
return __('Cannot merge');
}
return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), {
canMergeCount,
assigneesCount,
});
}, },
}, },
methods: { methods: {
assignSelf() { assignSelf() {
this.$emit('assign-self'); this.$emit('assign-self');
}, },
toggleShowLess() {
this.showLess = !this.showLess;
},
renderAssignee(index) {
return !this.showLess || (index < this.defaultRenderCount && this.showLess);
},
avatarUrl(user) {
return user.avatar || user.avatar_url || gon.default_avatar_url;
},
assigneeUrl(user) {
return `${this.rootPath}${user.username}`;
},
assigneeAlt(user) {
return sprintf(__("%{userName}'s avatar"), { userName: user.name });
},
assigneeUsername(user) {
return `@${user.username}`;
},
shouldRenderCollapsedAssignee(index) {
const firstTwo = this.users.length <= 2 && index <= 2;
return index === 0 || firstTwo;
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<div <collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" />
v-tooltip
:class="{ 'multiple-users': hasMoreThanOneAssignee }"
:title="collapsedTooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
data-container="body"
data-placement="left"
data-boundary="viewport"
>
<i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
<button
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
:key="user.id"
type="button"
class="btn-link"
>
<img
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
width="24"
class="avatar avatar-inline s24"
/>
<span class="author"> {{ user.name }} </span>
</button>
<button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
</button>
</div>
<div class="value hide-collapsed"> <div class="value hide-collapsed">
<span
v-if="mergeNotAllowedTooltipMessage"
v-tooltip
:title="mergeNotAllowedTooltipMessage"
data-placement="left"
class="float-right cannot-be-merged"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
</span>
<template v-if="hasNoUsers"> <template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself"> <span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }} {{ __('None') }}
...@@ -200,51 +64,13 @@ export default { ...@@ -200,51 +64,13 @@ export default {
</template> </template>
</span> </span>
</template> </template>
<template v-else-if="hasOneUser">
<a :href="assigneeUrl(firstUser)" class="author-link bold"> <uncollapsed-assignee-list
<img v-else
:alt="assigneeAlt(firstUser)" :users="sortedAssigness"
:src="avatarUrl(firstUser)" :root-path="rootPath"
width="32" :issuable-type="issuableType"
class="avatar avatar-inline s32" />
/>
<span class="author"> {{ firstUser.name }} </span>
<span class="username"> {{ assigneeUsername(firstUser) }} </span>
</a>
</template>
<template v-else>
<div class="user-list">
<div
v-for="(user, index) in users"
v-if="renderAssignee(index)"
:key="user.id"
class="user-item"
>
<a
:href="assigneeUrl(user)"
:data-title="user.name"
class="user-link has-tooltip"
data-container="body"
data-placement="bottom"
>
<img
:alt="assigneeAlt(user)"
:src="avatarUrl(user)"
width="32"
class="avatar avatar-inline s32"
/>
</a>
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button type="button" class="btn-link" @click="toggleShowLess">
<template v-if="showLess">
{{ hiddenAssigneesLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
</div>
</template>
</div> </div>
</div> </div>
</template> </template>
<script>
import AssigneeAvatar from './assignee_avatar.vue';
export default {
components: {
AssigneeAvatar,
},
props: {
user: {
type: Object,
required: true,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
};
</script>
<template>
<button type="button" class="btn-link">
<assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
<span class="author"> {{ user.name }} </span>
</button>
</template>
<script>
import { __, sprintf } from '~/locale';
import { GlTooltipDirective } from '@gitlab/ui';
import CollapsedAssignee from './collapsed_assignee.vue';
const DEFAULT_MAX_COUNTER = 99;
const DEFAULT_RENDER_COUNT = 5;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CollapsedAssignee,
},
props: {
users: {
type: Array,
required: true,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
computed: {
isMergeRequest() {
return this.issuableType === 'merge_request';
},
hasNoUsers() {
return !this.users.length;
},
hasMoreThanOneAssignee() {
return this.users.length > 1;
},
hasMoreThanTwoAssignees() {
return this.users.length > 2;
},
allAssigneesCanMerge() {
return this.users.every(user => user.can_merge);
},
sidebarAvatarCounter() {
if (this.users.length > DEFAULT_MAX_COUNTER) {
return `${DEFAULT_MAX_COUNTER}+`;
}
return `+${this.users.length - 1}`;
},
collapsedUsers() {
const collapsedLength = this.hasMoreThanTwoAssignees ? 1 : this.users.length;
return this.users.slice(0, collapsedLength);
},
tooltipTitleMergeStatus() {
if (!this.isMergeRequest) {
return '';
}
const mergeLength = this.users.filter(u => u.can_merge).length;
if (mergeLength === this.users.length) {
return '';
} else if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
});
}
return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
},
tooltipTitle() {
const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
const renderUsers = this.users.slice(0, maxRender);
const names = renderUsers.map(u => u.name);
if (!this.users.length) {
return __('Assignee(s)');
}
if (this.users.length > names.length) {
names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
}
const text = names.join(', ');
return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
},
tooltipOptions() {
return { container: 'body', placement: 'left', boundary: 'viewport' };
},
},
};
</script>
<template>
<div
v-gl-tooltip="tooltipOptions"
:class="{ 'multiple-users': hasMoreThanOneAssignee }"
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
<i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
<collapsed-assignee
v-for="user in collapsedUsers"
:key="user.id"
:user="user"
:issuable-type="issuableType"
/>
<button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
<i
v-if="isMergeRequest && !allAssigneesCanMerge"
aria-hidden="true"
class="fa fa-exclamation-triangle merge-icon"
></i>
</button>
</div>
</template>
<script>
import { __, sprintf } from '~/locale';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
AssigneeAvatarLink,
},
props: {
users: {
type: Array,
required: true,
},
rootPath: {
type: String,
required: true,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
data() {
return {
showLess: true,
};
},
computed: {
firstUser() {
return this.users[0];
},
hasOneUser() {
return this.users.length === 1;
},
hiddenAssigneesLabel() {
const { numberOfHiddenAssignees } = this;
return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
},
renderShowMoreSection() {
return this.users.length > DEFAULT_RENDER_COUNT;
},
numberOfHiddenAssignees() {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
},
username() {
return `@${this.firstUser.username}`;
},
},
methods: {
toggleShowLess() {
this.showLess = !this.showLess;
},
},
};
</script>
<template>
<assignee-avatar-link
v-if="hasOneUser"
v-slot="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="ml-2">
<span class="author"> {{ user.name }} </span>
<span class="username"> {{ username }} </span>
</div>
</assignee-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button type="button" class="btn-link" @click="toggleShowLess">
<template v-if="showLess">
{{ hiddenAssigneesLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
</div>
</div>
</template>
...@@ -62,6 +62,8 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -62,6 +62,8 @@ function UsersSelect(currentUser, els, options = {}) {
options.showCurrentUser = $dropdown.data('currentUser'); options.showCurrentUser = $dropdown.data('currentUser');
options.todoFilter = $dropdown.data('todoFilter'); options.todoFilter = $dropdown.data('todoFilter');
options.todoStateFilter = $dropdown.data('todoStateFilter'); options.todoStateFilter = $dropdown.data('todoStateFilter');
options.iid = $dropdown.data('iid');
options.issuableType = $dropdown.data('issuableType');
showNullUser = $dropdown.data('nullUser'); showNullUser = $dropdown.data('nullUser');
defaultNullUser = $dropdown.data('nullUserDefault'); defaultNullUser = $dropdown.data('nullUserDefault');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
...@@ -239,7 +241,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -239,7 +241,7 @@ function UsersSelect(currentUser, els, options = {}) {
'<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>', '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
); );
assigneeTemplate = _.template( assigneeTemplate = _.template(
`<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), { ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
openingTag: '<a href="#" class="js-assign-yourself">', openingTag: '<a href="#" class="js-assign-yourself">',
closingTag: '</a>', closingTag: '</a>',
...@@ -423,6 +425,8 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -423,6 +425,8 @@ function UsersSelect(currentUser, els, options = {}) {
const { $el, e, isMarking } = options; const { $el, e, isMarking } = options;
const user = options.selectedObj; const user = options.selectedObj;
$el.tooltip('dispose');
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
const isActive = $el.hasClass('is-active'); const isActive = $el.hasClass('is-active');
const previouslySelected = $dropdown const previouslySelected = $dropdown
...@@ -570,20 +574,11 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -570,20 +574,11 @@ function UsersSelect(currentUser, els, options = {}) {
user.name, user.name,
)}</a></li>`; )}</a></li>`;
} else { } else {
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; // 0 margin, because it's now handled by a wrapper
img = "<img src='" + avatar + "' class='avatar avatar-inline m-0' width='32' />";
} }
return ` return _this.renderRow(options.issuableType, user, selected, username, img);
<li data-user-id=${user.id}>
<a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
${img}
<strong class='dropdown-menu-user-full-name'>
${_.escape(user.name)}
</strong>
${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
</a>
</li>
`;
}, },
}); });
}; };
...@@ -764,6 +759,11 @@ UsersSelect.prototype.users = function(query, options, callback) { ...@@ -764,6 +759,11 @@ UsersSelect.prototype.users = function(query, options, callback) {
author_id: options.authorId || null, author_id: options.authorId || null,
skip_users: options.skipUsers || null, skip_users: options.skipUsers || null,
}; };
if (options.issuableType === 'merge_request') {
params.merge_request_iid = options.iid || null;
}
return axios.get(url, { params }).then(({ data }) => { return axios.get(url, { params }).then(({ data }) => {
callback(data); callback(data);
}); });
...@@ -776,4 +776,44 @@ UsersSelect.prototype.buildUrl = function(url) { ...@@ -776,4 +776,44 @@ UsersSelect.prototype.buildUrl = function(url) {
return url; return url;
}; };
UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) {
const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : '';
const tooltipClass = tooltip ? `has-tooltip` : '';
const selectedClass = selected === true ? 'is-active' : '';
const linkClasses = `${selectedClass} ${tooltipClass}`;
const tooltipAttributes = tooltip
? `data-container="body" data-placement="left" data-title="${tooltip}"`
: '';
return `
<li data-user-id=${user.id}>
<a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
${this.renderRowAvatar(issuableType, user, img)}
<span class="d-flex flex-column overflow-hidden">
<strong class="dropdown-menu-user-full-name">
${_.escape(user.name)}
</strong>
${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''}
</span>
</a>
</li>
`;
};
UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
if (user.beforeDivider) {
return img;
}
const mergeIcon =
issuableType === 'merge_request' && !user.can_merge
? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
: '';
return `<span class="position-relative mr-2">
${img}
${mergeIcon}
</span>`;
};
export default UsersSelect; export default UsersSelect;
...@@ -126,6 +126,16 @@ ...@@ -126,6 +126,16 @@
} }
} }
.assignee {
.merge-icon {
color: $orange-500;
position: absolute;
bottom: 0;
right: 0;
text-shadow: -1px -1px 0 $white-light, 1px -1px 0 $white-light, -1px 1px 0 $white-light, 1px 1px 0 $white-light;
}
}
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
top: $header-height; top: $header-height;
...@@ -202,7 +212,6 @@ ...@@ -202,7 +212,6 @@
&.assignee { &.assignee {
.author-link { .author-link {
display: block; display: block;
padding-left: 42px;
position: relative; position: relative;
&:hover { &:hover {
...@@ -210,12 +219,6 @@ ...@@ -210,12 +219,6 @@
text-decoration: underline; text-decoration: underline;
} }
} }
.avatar {
left: 0;
position: absolute;
top: 0;
}
} }
} }
} }
...@@ -354,13 +357,6 @@ ...@@ -354,13 +357,6 @@
margin-top: 0; margin-top: 0;
} }
.assignee .avatar {
float: left;
margin-right: 10px;
margin-bottom: 0;
margin-left: 0;
}
.assignee .user-list .avatar { .assignee .user-list .avatar {
margin: 0; margin: 0;
} }
...@@ -521,6 +517,10 @@ ...@@ -521,6 +517,10 @@
display: none; display: none;
} }
.merge-icon {
font-size: 10px;
}
.multiple-users { .multiple-users {
position: relative; position: relative;
height: 24px; height: 24px;
......
...@@ -4,6 +4,7 @@ class IssuableSidebarBasicEntity < Grape::Entity ...@@ -4,6 +4,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :id expose :id
expose :iid
expose :type do |issuable| expose :type do |issuable|
issuable.to_ability_name issuable.to_ability_name
end end
......
...@@ -8,7 +8,7 @@ class MergeRequestSerializer < BaseSerializer ...@@ -8,7 +8,7 @@ class MergeRequestSerializer < BaseSerializer
entity ||= entity ||=
case opts[:serializer] case opts[:serializer]
when 'sidebar' when 'sidebar'
IssuableSidebarBasicEntity MergeRequestSidebarBasicEntity
when 'sidebar_extras' when 'sidebar_extras'
MergeRequestSidebarExtrasEntity MergeRequestSidebarExtrasEntity
when 'basic' when 'basic'
......
# frozen_string_literal: true
class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
end
MergeRequestSidebarBasicEntity.prepend_if_ee('EE::MergeRequestSidebarBasicEntity')
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
placeholder: _('Search users'), placeholder: _('Search users'),
data: { first_user: issuable_sidebar.dig(:current_user, :username), data: { first_user: issuable_sidebar.dig(:current_user, :username),
current_user: true, current_user: true,
iid: issuable_sidebar[:iid],
issuable_type: issuable_type,
project_id: issuable_sidebar[:project_id], project_id: issuable_sidebar[:project_id],
author_id: issuable_sidebar[:author_id], author_id: issuable_sidebar[:author_id],
field_name: "#{issuable_type}[assignee_ids][]", field_name: "#{issuable_type}[assignee_ids][]",
......
...@@ -128,9 +128,6 @@ msgstr[1] "" ...@@ -128,9 +128,6 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}" msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr "" msgstr ""
msgid "%{canMergeCount}/%{assigneesCount} can merge"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}" msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr "" msgstr ""
...@@ -202,6 +199,9 @@ msgstr "" ...@@ -202,6 +199,9 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr "" msgstr ""
msgid "%{mergeLength}/%{usersLength} can merge"
msgstr ""
msgid "%{mrText}, this issue will be closed automatically." msgid "%{mrText}, this issue will be closed automatically."
msgstr "" msgstr ""
...@@ -279,6 +279,9 @@ msgstr "" ...@@ -279,6 +279,9 @@ msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr "" msgstr ""
msgid "%{userName} (cannot merge)"
msgstr ""
msgid "%{userName}'s avatar" msgid "%{userName}'s avatar"
msgstr "" msgstr ""
...@@ -306,6 +309,9 @@ msgstr "" ...@@ -306,6 +309,9 @@ msgstr ""
msgid "(external source)" msgid "(external source)"
msgstr "" msgstr ""
msgid "+ %{amount} more"
msgstr ""
msgid "+ %{count} more" msgid "+ %{count} more"
msgstr "" msgstr ""
...@@ -7439,9 +7445,6 @@ msgstr "" ...@@ -7439,9 +7445,6 @@ msgstr ""
msgid "No milestones to show" msgid "No milestones to show"
msgstr "" msgstr ""
msgid "No one can merge"
msgstr ""
msgid "No other labels with such name or description" msgid "No other labels with such name or description"
msgstr "" msgstr ""
...@@ -13452,6 +13455,9 @@ msgstr "" ...@@ -13452,6 +13455,9 @@ msgstr ""
msgid "cannot include leading slash or directory traversal." msgid "cannot include leading slash or directory traversal."
msgstr "" msgstr ""
msgid "cannot merge"
msgstr ""
msgid "comment" msgid "comment"
msgstr "" msgstr ""
...@@ -13879,6 +13885,9 @@ msgstr "" ...@@ -13879,6 +13885,9 @@ msgstr ""
msgid "no contributions" msgid "no contributions"
msgstr "" msgstr ""
msgid "no one can merge"
msgstr ""
msgid "none" msgid "none"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { joinPaths } from '~/lib/utils/url_utility';
import userDataMock from '../../user_data_mock';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
const TOOLTIP_PLACEMENT = 'bottom';
const { name: USER_NAME } = userDataMock();
describe('AssigneeAvatarLink component', () => {
let wrapper;
function createComponent(props = {}) {
const propsData = {
user: userDataMock(),
showLess: true,
rootPath: 'http://localhost:3000/',
tooltipPlacement: TOOLTIP_PLACEMENT,
singleUser: false,
issuableType: 'merge_request',
...props,
};
wrapper = shallowMount(AssigneeAvatarLink, {
propsData,
sync: false,
});
}
afterEach(() => {
wrapper.destroy();
});
const findTooltipText = () => wrapper.attributes('data-original-title');
it('user who cannot merge has "cannot merge" in tooltip', () => {
createComponent({
user: {
can_merge: false,
},
});
expect(findTooltipText().includes('cannot merge')).toBe(true);
});
it('has the root url present in the assigneeUrl method', () => {
createComponent();
const assigneeUrl = joinPaths(
`${wrapper.props('rootPath')}`,
`${wrapper.props('user').username}`,
);
expect(wrapper.attributes().href).toEqual(assigneeUrl);
});
describe.each`
issuableType | tooltipHasName | canMerge | expected
${'merge_request'} | ${true} | ${true} | ${USER_NAME}
${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`}
${'merge_request'} | ${false} | ${true} | ${''}
${'merge_request'} | ${false} | ${false} | ${'Cannot merge'}
${'issue'} | ${true} | ${true} | ${USER_NAME}
${'issue'} | ${true} | ${false} | ${USER_NAME}
${'issue'} | ${false} | ${true} | ${''}
${'issue'} | ${false} | ${false} | ${''}
`(
'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge',
({ issuableType, tooltipHasName, canMerge, expected }) => {
beforeEach(() => {
createComponent({
issuableType,
tooltipHasName,
user: {
...userDataMock(),
can_merge: canMerge,
},
});
});
it('sets tooltip', () => {
expect(findTooltipText()).toBe(expected);
});
},
);
});
import { shallowMount } from '@vue/test-utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import { TEST_HOST } from 'helpers/test_constants';
import userDataMock from '../../user_data_mock';
const TEST_AVATAR = `${TEST_HOST}/avatar.png`;
const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`;
describe('AssigneeAvatar', () => {
let origGon;
let wrapper;
function createComponent(props = {}) {
const propsData = {
user: userDataMock(),
imgSize: 24,
issuableType: 'merge_request',
...props,
};
wrapper = shallowMount(AssigneeAvatar, {
propsData,
sync: false,
});
}
beforeEach(() => {
origGon = window.gon;
window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL };
});
afterEach(() => {
window.gon = origGon;
wrapper.destroy();
});
const findImg = () => wrapper.find('img');
it('does not show warning icon if assignee can merge', () => {
createComponent();
expect(wrapper.element.querySelector('.merge-icon')).toBeNull();
});
it('shows warning icon if assignee cannot merge', () => {
createComponent({
user: {
can_merge: false,
},
});
expect(wrapper.element.querySelector('.merge-icon')).not.toBeNull();
});
it('does not show warning icon for issuableType = "issue"', () => {
createComponent({
issuableType: 'issue',
});
expect(wrapper.element.querySelector('.merge-icon')).toBeNull();
});
it.each`
avatar | avatar_url | expected | desc
${TEST_AVATAR} | ${null} | ${TEST_AVATAR} | ${'with avatar'}
${null} | ${TEST_AVATAR} | ${TEST_AVATAR} | ${'with avatar_url'}
${null} | ${null} | ${TEST_DEFAULT_AVATAR_URL} | ${'with no avatar'}
`('$desc', ({ avatar, avatar_url, expected }) => {
createComponent({
user: {
avatar,
avatar_url,
},
});
expect(findImg().attributes('src')).toEqual(expected);
});
});
import { shallowMount } from '@vue/test-utils';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
import UsersMockHelper from 'helpers/user_mock_data_helper';
const DEFAULT_MAX_COUNTER = 99;
describe('CollapsedAssigneeList component', () => {
let wrapper;
function createComponent(props = {}) {
const propsData = {
users: [],
issuableType: 'merge_request',
...props,
};
wrapper = shallowMount(CollapsedAssigneeList, {
propsData,
sync: false,
});
}
const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
const findAvatarCounter = () => wrapper.find('.avatar-counter');
const findAssignees = () => wrapper.findAll(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('data-original-title');
afterEach(() => {
wrapper.destroy();
});
describe('No assignees/users', () => {
beforeEach(() => {
createComponent({
users: [],
});
});
it('has no users', () => {
expect(findNoUsersIcon().exists()).toBe(true);
});
});
describe('One assignee/user', () => {
let users;
beforeEach(() => {
users = UsersMockHelper.createNumberRandomUsers(1);
});
it('should not show no users icon', () => {
createComponent({ users });
expect(findNoUsersIcon().exists()).toBe(false);
});
it('has correct "cannot merge" tooltip when user cannot merge', () => {
users[0].can_merge = false;
createComponent({ users });
expect(getTooltipTitle()).toContain('cannot merge');
});
it('does not have "merge" word in tooltip if user can merge', () => {
users[0].can_merge = true;
createComponent({ users });
expect(getTooltipTitle()).not.toContain('merge');
});
});
describe('More than one assignees/users', () => {
let users;
beforeEach(() => {
users = UsersMockHelper.createNumberRandomUsers(2);
});
describe('default', () => {
beforeEach(() => {
createComponent({ users });
});
it('has multiple-users class', () => {
expect(wrapper.classes('multiple-users')).toBe(true);
});
it('does not display an avatar count', () => {
expect(findAvatarCounter().exists()).toBe(false);
});
it('returns just two collapsed users', () => {
expect(findAssignees().length).toBe(2);
});
});
it('has correct "cannot merge" tooltip when no user can merge', () => {
users[0].can_merge = false;
users[1].can_merge = false;
createComponent({
users,
});
expect(getTooltipTitle()).toEqual(`${users[0].name}, ${users[1].name} (no one can merge)`);
});
it('does not have "merge" word in tooltip if everyone can merge', () => {
users[0].can_merge = true;
users[1].can_merge = true;
createComponent({
users,
});
expect(getTooltipTitle()).toEqual(`${users[0].name}, ${users[1].name}`);
});
});
describe('More than two assignees/users', () => {
let users;
beforeEach(() => {
users = UsersMockHelper.createNumberRandomUsers(3);
});
describe('default', () => {
beforeEach(() => {
createComponent({ users });
});
it('does display an avatar count', () => {
expect(findAvatarCounter().exists()).toBe(true);
expect(findAvatarCounter().text()).toEqual('+2');
});
it('returns one collapsed users', () => {
expect(findAssignees().length).toBe(1);
});
});
it('has correct "cannot merge" tooltip when one user can merge', () => {
users[0].can_merge = true;
users[1].can_merge = false;
users[2].can_merge = false;
createComponent({
users,
});
expect(getTooltipTitle()).toContain('1/3 can merge');
});
it('has correct "cannot merge" tooltip when more than one user can merge', () => {
users[0].can_merge = false;
users[1].can_merge = true;
users[2].can_merge = true;
createComponent({
users,
});
expect(getTooltipTitle()).toContain('2/3 can merge');
});
it('does not have "merge" in tooltip if everyone can merge', () => {
users[0].can_merge = true;
users[1].can_merge = true;
users[2].can_merge = true;
createComponent({
users,
});
expect(getTooltipTitle()).not.toContain('merge');
});
it('displays the correct avatar count via a computed property if less than default max counter', () => {
users = UsersMockHelper.createNumberRandomUsers(5);
createComponent({
users,
});
expect(findAvatarCounter().text()).toEqual(`+${users.length - 1}`);
});
it('displays the correct avatar count via a computed property if more than default max counter', () => {
users = UsersMockHelper.createNumberRandomUsers(100);
createComponent({
users,
});
expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`);
});
});
});
import { shallowMount } from '@vue/test-utils';
import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
import userDataMock from '../../user_data_mock';
describe('CollapsedAssignee assignee component', () => {
let wrapper;
function createComponent(props = {}) {
const propsData = {
user: userDataMock(),
...props,
};
wrapper = shallowMount(CollapsedAssignee, {
propsData,
sync: false,
});
}
afterEach(() => {
wrapper.destroy();
});
it('has author name', () => {
createComponent();
expect(
wrapper
.find('.author')
.text()
.trim(),
).toEqual(wrapper.vm.user.name);
});
});
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import { TEST_HOST } from 'helpers/test_constants';
import userDataMock from '../../user_data_mock';
import UsersMockHelper from '../../../helpers/user_mock_data_helper';
const DEFAULT_RENDER_COUNT = 5;
describe('UncollapsedAssigneeList component', () => {
let wrapper;
function createComponent(props = {}) {
const propsData = {
users: [],
rootPath: TEST_HOST,
...props,
};
wrapper = mount(UncollapsedAssigneeList, {
sync: false,
propsData,
});
}
afterEach(() => {
wrapper.destroy();
});
const findMoreButton = () => wrapper.find('.user-list-more button');
describe('One assignee/user', () => {
let user;
beforeEach(() => {
user = userDataMock();
createComponent({
users: [user],
});
});
it('only has one user', () => {
expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1);
});
it('calls the AssigneeAvatarLink with the proper props', () => {
expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true);
expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left');
});
it('Shows one user with avatar, username and author name', () => {
expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain(`@${user.username}`);
});
});
describe('Two or more assignees/users', () => {
beforeEach(() => {
createComponent({
users: UsersMockHelper.createNumberRandomUsers(3),
});
});
it('more than one user', () => {
expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(3);
});
it('shows the "show-less" assignees label', done => {
const users = UsersMockHelper.createNumberRandomUsers(6);
createComponent({
users,
});
expect(wrapper.vm.$el.querySelectorAll('.user-item').length).toEqual(DEFAULT_RENDER_COUNT);
expect(wrapper.vm.$el.querySelector('.user-list-more')).not.toBe(null);
const usersLabelExpectation = users.length - DEFAULT_RENDER_COUNT;
expect(wrapper.vm.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
`+${usersLabelExpectation} more`,
);
wrapper.vm.toggleShowLess();
Vue.nextTick(() => {
expect(wrapper.vm.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
'- show less',
);
done();
});
});
it('shows the "show-less" when "n+ more " label is clicked', done => {
createComponent({
users: UsersMockHelper.createNumberRandomUsers(6),
});
wrapper.vm.$el.querySelector('.user-list-more .btn-link').click();
Vue.nextTick(() => {
expect(wrapper.vm.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
'- show less',
);
done();
});
});
it('does not show n+ more label when less than render count', () => {
expect(findMoreButton().exists()).toBe(false);
});
});
describe('n+ more label', () => {
beforeEach(() => {
createComponent({
users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT + 1),
});
});
it('shows "+1 more" label', () => {
expect(findMoreButton().text()).toBe('+ 1 more');
expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT);
});
it('shows "show less" label', done => {
findMoreButton().trigger('click');
Vue.nextTick(() => {
expect(findMoreButton().text()).toBe('- show less');
expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1);
done();
});
});
});
});
export default () => ({
avatar_url: 'mock_path',
id: 1,
name: 'Root',
state: 'active',
username: 'root',
web_url: '',
can_merge: true,
});
...@@ -94,115 +94,9 @@ describe('Assignee component', () => { ...@@ -94,115 +94,9 @@ describe('Assignee component', () => {
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
}); });
it('Shows one user with avatar, username and author name', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users: [UsersMock.user],
editable: true,
},
}).$mount();
expect(component.$el.querySelector('.author-link')).not.toBeNull();
// The image
expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(
UsersMock.user.avatar,
);
// Author name
expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(
UsersMock.user.name,
);
// Username
expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(
`@${UsersMock.user.username}`,
);
});
it('has the root url present in the assigneeUrl method', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users: [UsersMock.user],
editable: true,
},
}).$mount();
expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(
-1,
);
});
it('has correct "cannot merge" tooltip when user cannot merge', () => {
const user = Object.assign({}, UsersMock.user, { can_merge: false });
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users: [user],
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
});
}); });
describe('Two or more assignees/users', () => { describe('Two or more assignees/users', () => {
it('has correct "cannot merge" tooltip when one user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = true;
users[1].can_merge = false;
users[2].can_merge = false;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
});
it('has correct "cannot merge" tooltip when no user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
users[0].can_merge = false;
users[1].can_merge = false;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
});
it('has correct "cannot merge" tooltip when more than one user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = true;
users[2].can_merge = true;
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000/',
users,
editable: true,
issuableType: 'merge_request',
},
}).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
});
it('has no "cannot merge" tooltip when every user can merge', () => { it('has no "cannot merge" tooltip when every user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(2); const users = UsersMockHelper.createNumberRandomUsers(2);
users[0].can_merge = true; users[0].can_merge = true;
...@@ -217,7 +111,7 @@ describe('Assignee component', () => { ...@@ -217,7 +111,7 @@ describe('Assignee component', () => {
}, },
}).$mount(); }).$mount();
expect(component.mergeNotAllowedTooltipMessage).toEqual(null); expect(component.collapsedTooltipTitle).not.toContain('cannot merge');
}); });
it('displays two assignee icons when collapsed', () => { it('displays two assignee icons when collapsed', () => {
...@@ -295,8 +189,12 @@ describe('Assignee component', () => { ...@@ -295,8 +189,12 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null); expect(component.$el.querySelector('.user-list-more')).toBe(null);
}); });
it('sets tooltip container to body', () => { it('shows sorted assignee where "can merge" users are sorted first', () => {
const users = UsersMockHelper.createNumberRandomUsers(2); const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
rootPath: 'http://localhost:3000', rootPath: 'http://localhost:3000',
...@@ -305,98 +203,48 @@ describe('Assignee component', () => { ...@@ -305,98 +203,48 @@ describe('Assignee component', () => {
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body'); expect(component.sortedAssigness[0].can_merge).toBe(true);
}); });
it('Shows the "show-less" assignees label', done => { it('passes the sorted assignees to the uncollapsed-assignee-list', () => {
const users = UsersMockHelper.createNumberRandomUsers(6); const users = UsersMockHelper.createNumberRandomUsers(3);
users[0].can_merge = false;
users[1].can_merge = false;
users[2].can_merge = true;
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
rootPath: 'http://localhost:3000', rootPath: 'http://localhost:3000',
users, users,
editable: true, editable: false,
}, },
}).$mount(); }).$mount();
expect(component.$el.querySelectorAll('.user-item').length).toEqual( const userItems = component.$el.querySelectorAll('.user-list .user-item a');
component.defaultRenderCount,
);
expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
const usersLabelExpectation = users.length - component.defaultRenderCount;
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe( expect(userItems.length).toBe(3);
`+${usersLabelExpectation} more`, expect(userItems[0].dataset.originalTitle).toBe(users[2].name);
); expect(userItems[0].dataset.originalTitle).not.toBe(users[0].name);
component.toggleShowLess();
Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
'- show less',
);
done();
});
}); });
it('Shows the "show-less" when "n+ more " label is clicked', done => { it('passes the sorted assignees to the collapsed-assignee-list', () => {
const users = UsersMockHelper.createNumberRandomUsers(6); const users = UsersMockHelper.createNumberRandomUsers(3);
component = new AssigneeComponent({ users[0].can_merge = false;
propsData: { users[1].can_merge = false;
rootPath: 'http://localhost:3000', users[2].can_merge = true;
users,
editable: true,
},
}).$mount();
component.$el.querySelector('.user-list-more .btn-link').click();
Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
'- show less',
);
done();
});
});
it('gets the count of avatar via a computed property ', () => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({ component = new AssigneeComponent({
propsData: { propsData: {
rootPath: 'http://localhost:3000', rootPath: 'http://localhost:3000',
users, users,
editable: true, editable: false,
}, },
}).$mount(); }).$mount();
expect(component.sidebarAvatarCounter).toEqual(`+${users.length - 1}`); const collapsedButton = component.$el.querySelector('.sidebar-collapsed-user button');
});
describe('n+ more label', () => { expect(collapsedButton.innerText.trim()).toBe(users[2].name);
beforeEach(() => { expect(collapsedButton.innerText.trim()).not.toBe(users[0].name);
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
editable: true,
},
}).$mount();
});
it('shows "+1 more" label', () => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
'+ 1 more',
);
});
it('shows "show less" label', done => {
component.toggleShowLess();
Vue.nextTick(() => {
expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
'- show less',
);
done();
});
});
}); });
}); });
}); });
export const userDataMock = {
avatar_url: 'mock_path',
id: 1,
name: 'Root',
state: 'active',
username: 'root',
web_url: '',
can_merge: true,
};
const RESPONSE_MAP = { const RESPONSE_MAP = {
GET: { GET: {
'/gitlab-org/gitlab-shell/issues/5.json': { '/gitlab-org/gitlab-shell/issues/5.json': {
......
# frozen_string_literal: true
require 'spec_helper'
describe MergeRequestSidebarBasicEntity do
let(:project) { create :project, :repository }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
let(:request) { double('request', current_user: user, project: project) }
let(:entity) { described_class.new(merge_request, request: request).as_json }
describe '#current_user' do
it 'contains attributes related to the current user' do
expect(entity[:current_user].keys).to contain_exactly(
:id, :name, :username, :state, :avatar_url, :web_url, :todo,
:can_edit, :can_move, :can_admin_label, :can_merge
)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment