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> <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 { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants'; import { AVATAR_SIZE } from '../constants';
...@@ -7,7 +13,11 @@ export default { ...@@ -7,7 +13,11 @@ export default {
name: 'UserAvatar', name: 'UserAvatar',
avatarSize: AVATAR_SIZE, avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'), orphanedUserLabel: __('Orphaned member'),
components: { GlAvatarLink, GlAvatarLabeled }, components: {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
},
directives: { directives: {
SafeHtml, SafeHtml,
}, },
...@@ -16,11 +26,18 @@ export default { ...@@ -16,11 +26,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
user() { user() {
return this.member.user; return this.member.user;
}, },
badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
},
}, },
}; };
</script> </script>
...@@ -41,7 +58,15 @@ export default { ...@@ -41,7 +58,15 @@ export default {
:size="$options.avatarSize" :size="$options.avatarSize"
:entity-name="user.name" :entity-name="user.name"
:entity-id="user.id" :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-link>
<gl-avatar-labeled <gl-avatar-labeled
......
...@@ -12,6 +12,10 @@ export default { ...@@ -12,6 +12,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
member: { member: {
type: Object, type: Object,
required: true, required: true,
...@@ -27,5 +31,5 @@ export default { ...@@ -27,5 +31,5 @@ export default {
</script> </script>
<template> <template>
<component :is="avatarComponent" :member="member" /> <component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" />
</template> </template>
...@@ -44,8 +44,12 @@ export default { ...@@ -44,8 +44,12 @@ export default {
show-empty show-empty
> >
<template #cell(account)="{ item: member }"> <template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType }" :member="member"> <members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar :member-type="memberType" :member="member" /> <member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell> </members-table-cell>
</template> </template>
......
...@@ -11,7 +11,7 @@ export default { ...@@ -11,7 +11,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['sourceId']), ...mapState(['sourceId', 'currentUserId']),
isGroup() { isGroup() {
return Boolean(this.member.sharedWithGroup); return Boolean(this.member.sharedWithGroup);
}, },
...@@ -35,11 +35,15 @@ export default { ...@@ -35,11 +35,15 @@ export default {
isDirectMember() { isDirectMember() {
return this.member.source?.id === this.sourceId; return this.member.source?.id === this.sourceId;
}, },
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
},
}, },
render() { render() {
return this.$scopedSlots.default({ return this.$scopedSlots.default({
memberType: this.memberType, memberType: this.memberType,
isDirectMember: this.isDirectMember, 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 "" ...@@ -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." msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr "" msgstr ""
msgid "SAML"
msgstr ""
msgid "SAML SSO" msgid "SAML SSO"
msgstr "" msgstr ""
......
import { mount, createWrapper } from '@vue/test-utils'; import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui'; import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { member, orphanedMember } from '../mock_data'; import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue'; import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
describe('MemberList', () => { describe('MemberList', () => {
let wrapper; let wrapper;
const { user } = member; const { user } = memberMock;
const createComponent = (propsData = {}) => { const createComponent = (propsData = {}) => {
wrapper = mount(UserAvatar, { wrapper = mount(UserAvatar, {
propsData: { propsData: {
member, member: memberMock,
isCurrentUser: false,
...propsData, ...propsData,
}, },
}); });
}; };
const getByText = (text, options) => const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options)); createWrapper(within(wrapper.element).findByText(text, options));
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -63,4 +64,25 @@ describe('MemberList', () => { ...@@ -63,4 +64,25 @@ describe('MemberList', () => {
expect(getByText('Orphaned member').exists()).toBe(true); 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', () => { ...@@ -11,7 +11,10 @@ describe('MemberList', () => {
const createComponent = propsData => { const createComponent = propsData => {
wrapper = shallowMount(MemberAvatar, { wrapper = shallowMount(MemberAvatar, {
propsData, propsData: {
isCurrentUser: false,
...propsData,
},
}); });
}; };
......
...@@ -15,6 +15,10 @@ describe('MemberList', () => { ...@@ -15,6 +15,10 @@ describe('MemberList', () => {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
}, },
render(createElement) { render(createElement) {
return createElement('div', this.memberType); return createElement('div', this.memberType);
...@@ -29,6 +33,7 @@ describe('MemberList', () => { ...@@ -29,6 +33,7 @@ describe('MemberList', () => {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
sourceId: 1, sourceId: 1,
currentUserId: 1,
...state, ...state,
}, },
}); });
...@@ -42,8 +47,13 @@ describe('MemberList', () => { ...@@ -42,8 +47,13 @@ describe('MemberList', () => {
propsData, propsData,
store: createStore(state), store: createStore(state),
scopedSlots: { scopedSlots: {
default: default: `
'<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />', <wrapped-component
:member-type="props.memberType"
:is-direct-member="props.isDirectMember"
:is-current-user="props.isCurrentUser"
/>
`,
}, },
}); });
}; };
...@@ -93,4 +103,28 @@ describe('MemberList', () => { ...@@ -93,4 +103,28 @@ describe('MemberList', () => {
expect(findWrappedComponent().props('isDirectMember')).toBe(false); 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