Commit b5cbf78a authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '235601-convert-group-members-list-view-from-haml-to-vue-badges' into 'master'

Add badges to member avatars

See merge request gitlab-org/gitlab!43741
parents 515804e4 e46b95a6
<script>
import { GlAvatarLink, GlAvatarLabeled, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
import { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants';
......@@ -7,7 +13,11 @@ export default {
name: 'UserAvatar',
avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'),
components: { GlAvatarLink, GlAvatarLabeled },
components: {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
},
directives: {
SafeHtml,
},
......@@ -16,11 +26,18 @@ export default {
type: Object,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
},
computed: {
user() {
return this.member.user;
},
badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
},
},
};
</script>
......@@ -41,7 +58,15 @@ export default {
:size="$options.avatarSize"
:entity-name="user.name"
:entity-id="user.id"
/>
>
<template #meta>
<div v-for="badge in badges" :key="badge.text" class="gl-p-1">
<gl-badge size="sm" :variant="badge.variant">
{{ badge.text }}
</gl-badge>
</div>
</template>
</gl-avatar-labeled>
</gl-avatar-link>
<gl-avatar-labeled
......
......@@ -12,6 +12,10 @@ export default {
type: String,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
member: {
type: Object,
required: true,
......@@ -27,5 +31,5 @@ export default {
</script>
<template>
<component :is="avatarComponent" :member="member" />
<component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" />
</template>
......@@ -44,8 +44,12 @@ export default {
show-empty
>
<template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType }" :member="member">
<member-avatar :member-type="memberType" :member="member" />
<members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell>
</template>
......
......@@ -11,7 +11,7 @@ export default {
},
},
computed: {
...mapState(['sourceId']),
...mapState(['sourceId', 'currentUserId']),
isGroup() {
return Boolean(this.member.sharedWithGroup);
},
......@@ -35,11 +35,15 @@ export default {
isDirectMember() {
return this.member.source?.id === this.sourceId;
},
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
},
},
render() {
return this.$scopedSlots.default({
memberType: this.memberType,
isDirectMember: this.isDirectMember,
isCurrentUser: this.isCurrentUser,
});
},
};
......
import { __ } from '~/locale';
export const generateBadges = (member, isCurrentUser) => [
{
show: isCurrentUser,
text: __("It's you"),
variant: 'success',
},
{
show: member.user?.blocked,
text: __('Blocked'),
variant: 'danger',
},
{
show: member.user?.twoFactorEnabled,
text: __('2FA'),
variant: 'info',
},
];
import { __ } from '~/locale';
import { generateBadges as CEGenerateBadges } from '~/vue_shared/components/members/utils';
export const generateBadges = (member, isCurrentUser) => [
...CEGenerateBadges(member, isCurrentUser),
{
show: member.usingLicense,
text: __('Is using seat'),
variant: 'neutral',
},
{
show: member.groupSso,
text: __('SAML'),
variant: 'info',
},
{
show: member.groupManagedAccount,
text: __('Managed Account'),
variant: 'info',
},
];
import { member as memberMock } from 'jest/vue_shared/components/members/mock_data';
import { generateBadges } from 'ee/vue_shared/components/members/utils';
describe('Members Utils', () => {
describe('generateBadges', () => {
it('has correct properties for each badge', () => {
const badges = generateBadges(memberMock, true);
badges.forEach(badge => {
expect(badge).toEqual(
expect.objectContaining({
show: expect.any(Boolean),
text: expect.any(String),
variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/),
}),
);
});
});
it.each`
member | expected
${{ ...memberMock, usingLicense: true }} | ${{ show: true, text: 'Is using seat', variant: 'neutral' }}
${{ ...memberMock, groupSso: true }} | ${{ show: true, text: 'SAML', variant: 'info' }}
${{ ...memberMock, groupManagedAccount: true }} | ${{ show: true, text: 'Managed Account', variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected));
});
});
});
......@@ -22175,6 +22175,9 @@ msgstr ""
msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr ""
msgid "SAML"
msgstr ""
msgid "SAML SSO"
msgstr ""
......
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui';
import { member, orphanedMember } from '../mock_data';
import { within } from '@testing-library/dom';
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
describe('MemberList', () => {
let wrapper;
const { user } = member;
const { user } = memberMock;
const createComponent = (propsData = {}) => {
wrapper = mount(UserAvatar, {
propsData: {
member,
member: memberMock,
isCurrentUser: false,
...propsData,
},
});
};
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
createWrapper(within(wrapper.element).findByText(text, options));
afterEach(() => {
wrapper.destroy();
......@@ -63,4 +64,25 @@ describe('MemberList', () => {
expect(getByText('Orphaned member').exists()).toBe(true);
});
});
describe('badges', () => {
it.each`
member | badgeText
${{ ...memberMock, usingLicense: true }} | ${'Is using seat'}
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'}
${{ ...memberMock, groupSso: true }} | ${'SAML'}
${{ ...memberMock, groupManagedAccount: true }} | ${'Managed Account'}
`('renders the "$badgeText" badge', ({ member, badgeText }) => {
createComponent({ member });
expect(wrapper.find(GlBadge).text()).toBe(badgeText);
});
it('renders the "It\'s you" badge when member is current user', () => {
createComponent({ isCurrentUser: true });
expect(getByText("It's you").exists()).toBe(true);
});
});
});
......@@ -11,7 +11,10 @@ describe('MemberList', () => {
const createComponent = propsData => {
wrapper = shallowMount(MemberAvatar, {
propsData,
propsData: {
isCurrentUser: false,
...propsData,
},
});
};
......
......@@ -15,6 +15,10 @@ describe('MemberList', () => {
type: Boolean,
required: true,
},
isCurrentUser: {
type: Boolean,
required: true,
},
},
render(createElement) {
return createElement('div', this.memberType);
......@@ -29,6 +33,7 @@ describe('MemberList', () => {
return new Vuex.Store({
state: {
sourceId: 1,
currentUserId: 1,
...state,
},
});
......@@ -42,8 +47,13 @@ describe('MemberList', () => {
propsData,
store: createStore(state),
scopedSlots: {
default:
'<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />',
default: `
<wrapped-component
:member-type="props.memberType"
:is-direct-member="props.isDirectMember"
:is-current-user="props.isCurrentUser"
/>
`,
},
});
};
......@@ -93,4 +103,28 @@ describe('MemberList', () => {
expect(findWrappedComponent().props('isDirectMember')).toBe(false);
});
});
describe('isCurrentUser', () => {
it('returns `true` when `member.user` has the same ID as `currentUserId`', () => {
createComponent({
member: {
...memberMock,
user: {
...memberMock.user,
id: 1,
},
},
});
expect(findWrappedComponent().props('isCurrentUser')).toBe(true);
});
it('returns `false` when `member.user` does not have the same ID as `currentUserId`', () => {
createComponent({
member: memberMock,
});
expect(findWrappedComponent().props('isCurrentUser')).toBe(false);
});
});
});
import { generateBadges } from '~/vue_shared/components/members/utils';
import { member as memberMock } from './mock_data';
describe('Members Utils', () => {
describe('generateBadges', () => {
it('has correct properties for each badge', () => {
const badges = generateBadges(memberMock, true);
badges.forEach(badge => {
expect(badge).toEqual(
expect.objectContaining({
show: expect.any(Boolean),
text: expect.any(String),
variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/),
}),
);
});
});
it.each`
member | expected
${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }}
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${{ show: true, text: '2FA', variant: 'info' }}
`('returns expected output for "$expected.text" badge', ({ member, expected }) => {
expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected));
});
});
});
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