Commit a231a01b authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Frédéric Caplette

Render user avatar image using `GlAvatar`

This is added behind the FF which is introduced in this MR
parent 6a5a8458
<script> <script>
/* This is a re-usable vue component for rendering a user avatar that /* This is a re-usable vue component for rendering a user avatar that
does not need to link to the user's profile. The image and an optional does not need to link to the user's profile. The image and an optional
tooltip can be configured by props passed to this component. tooltip can be configured by props passed to this component.
Sample configuration: Sample configuration:
<user-avatar-image <user-avatar-image
:lazy="true" lazy
:img-src="userAvatarSrc" :img-src="userAvatarSrc"
:img-alt="tooltipText" :img-alt="tooltipText"
:tooltip-text="tooltipText" :tooltip-text="tooltipText"
tooltip-placement="top" tooltip-placement="top"
/> />
*/ */
import { GlTooltip } from '@gitlab/ui'; import { GlTooltip, GlAvatar } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png'; import defaultAvatarUrl from 'images/no_avatar.png';
import { __ } from '~/locale'; import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { placeholderImage } from '../../../lazy_loader'; import { placeholderImage } from '../../../lazy_loader';
export default { export default {
name: 'UserAvatarImage', name: 'UserAvatarImage',
components: { components: {
GlTooltip, GlTooltip,
GlAvatar,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
lazy: { lazy: {
type: Boolean, type: Boolean,
...@@ -85,7 +88,20 @@ export default { ...@@ -85,7 +88,20 @@ export default {
<template> <template>
<span> <span>
<gl-avatar
v-if="glFeatures.glAvatarForAllUserAvatars"
ref="userAvatarImage"
:class="{
lazy: lazy,
[cssClasses]: true,
}"
:src="resultantSrcAttribute"
:data-src="sanitizedSource"
:size="size"
:alt="imgAlt"
/>
<img <img
v-else
ref="userAvatarImage" ref="userAvatarImage"
:class="{ :class="{
lazy: lazy, lazy: lazy,
...@@ -100,11 +116,9 @@ export default { ...@@ -100,11 +116,9 @@ export default {
class="avatar" class="avatar"
/> />
<gl-tooltip <gl-tooltip
v-if="tooltipText || $slots.default"
:target="() => $refs.userAvatarImage" :target="() => $refs.userAvatarImage"
:placement="tooltipPlacement" :placement="tooltipPlacement"
boundary="window" boundary="window"
class="js-user-avatar-image-tooltip"
> >
<slot> {{ tooltipText }} </slot> <slot> {{ tooltipText }} </slot>
</gl-tooltip> </gl-tooltip>
......
---
name: gl_avatar_for_all_user_avatars
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81437
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353477
milestone: '14.9'
type: development
group: group::foundations
default_enabled: false
...@@ -17,6 +17,7 @@ RSpec.describe 'Merge Requests > User resets approvers', :js do ...@@ -17,6 +17,7 @@ RSpec.describe 'Merge Requests > User resets approvers', :js do
before do before do
stub_licensed_features(multiple_approval_rules: true) stub_licensed_features(multiple_approval_rules: true)
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
project_approvers.each do |approver| project_approvers.each do |approver|
project.add_developer(approver) project.add_developer(approver)
......
...@@ -18,6 +18,7 @@ RSpec.describe 'Project settings > [EE] Merge Request Approvals', :js do ...@@ -18,6 +18,7 @@ RSpec.describe 'Project settings > [EE] Merge Request Approvals', :js do
project.add_maintainer(user) project.add_maintainer(user)
group.add_developer(user) group.add_developer(user)
group.add_developer(group_member) group.add_developer(group_member)
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
end end
it 'adds approver' do it 'adds approver' do
......
...@@ -59,6 +59,7 @@ module Gitlab ...@@ -59,6 +59,7 @@ module Gitlab
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml) push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml) push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml)
push_frontend_feature_flag(:source_editor_toolbar, default_enabled: :yaml) push_frontend_feature_flag(:source_editor_toolbar, default_enabled: :yaml)
push_frontend_feature_flag(:gl_avatar_for_all_user_avatars, default_enabled: :yaml)
end end
# Exposes the state of a feature flag to the frontend code. # Exposes the state of a feature flag to the frontend code.
......
...@@ -23,6 +23,7 @@ RSpec.describe 'Project issue boards', :js do ...@@ -23,6 +23,7 @@ RSpec.describe 'Project issue boards', :js do
project.add_maintainer(user2) project.add_maintainer(user2)
sign_in(user) sign_in(user)
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
set_cookie('sidebar_collapsed', 'true') set_cookie('sidebar_collapsed', 'true')
end end
......
...@@ -25,6 +25,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do ...@@ -25,6 +25,7 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in user sign_in user
stub_feature_flags(gl_avatar_for_all_user_avatars: false)
set_cookie('sidebar_collapsed', 'true') set_cookie('sidebar_collapsed', 'true')
end end
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlAvatar, GlTooltip } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png'; import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '~/lazy_loader'; import { placeholderImage } from '~/lazy_loader';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
jest.mock('images/no_avatar.png', () => 'default-avatar-url'); jest.mock('images/no_avatar.png', () => 'default-avatar-url');
const DEFAULT_PROPS = { const PROVIDED_PROPS = {
size: 99, size: 32,
imgSrc: 'myavatarurl.com', imgSrc: 'myavatarurl.com',
imgAlt: 'mydisplayname', imgAlt: 'mydisplayname',
cssClasses: 'myextraavatarclass', cssClasses: 'myextraavatarclass',
...@@ -14,6 +15,10 @@ const DEFAULT_PROPS = { ...@@ -14,6 +15,10 @@ const DEFAULT_PROPS = {
tooltipPlacement: 'bottom', tooltipPlacement: 'bottom',
}; };
const DEFAULT_PROPS = {
size: 20,
};
describe('User Avatar Image Component', () => { describe('User Avatar Image Component', () => {
let wrapper; let wrapper;
...@@ -21,64 +26,149 @@ describe('User Avatar Image Component', () => { ...@@ -21,64 +26,149 @@ describe('User Avatar Image Component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('Initialization', () => { describe('`glAvatarForAllUserAvatars` feature flag enabled', () => {
beforeEach(() => { describe('Initialization', () => {
wrapper = shallowMount(UserAvatarImage, { beforeEach(() => {
propsData: { wrapper = shallowMount(UserAvatarImage, {
...DEFAULT_PROPS, propsData: {
}, ...PROVIDED_PROPS,
},
provide: {
glFeatures: {
glAvatarForAllUserAvatars: true,
},
},
});
});
it('should render `GlAvatar` and provide correct properties to it', () => {
const avatar = wrapper.findComponent(GlAvatar);
expect(avatar.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(avatar.props()).toMatchObject({
src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
alt: PROVIDED_PROPS.imgAlt,
});
});
it('should add correct CSS classes', () => {
const classes = wrapper.findComponent(GlAvatar).classes();
expect(classes).toContain(PROVIDED_PROPS.cssClasses);
expect(classes).not.toContain('lazy');
}); });
}); });
it('should have <img> as a child element', () => { describe('Initialization when lazy', () => {
const imageElement = wrapper.find('img'); beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
lazy: true,
},
provide: {
glFeatures: {
glAvatarForAllUserAvatars: true,
},
},
});
});
it('should add lazy attributes', () => {
const avatar = wrapper.findComponent(GlAvatar);
expect(imageElement.exists()).toBe(true); expect(avatar.classes()).toContain('lazy');
expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); expect(avatar.attributes()).toMatchObject({
expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); src: placeholderImage,
expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt); 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
});
});
}); });
it('should properly render img css', () => { describe('Initialization without src', () => {
const classes = wrapper.find('img').classes(); beforeEach(() => {
expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses])); wrapper = shallowMount(UserAvatarImage);
expect(classes).not.toContain('lazy'); });
it('should have default avatar image', () => {
const imageElement = wrapper.find('img');
expect(imageElement.attributes('src')).toBe(
`${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
);
});
}); });
}); });
describe('Initialization when lazy', () => { describe('`glAvatarForAllUserAvatars` feature flag disabled', () => {
beforeEach(() => { describe('Initialization', () => {
wrapper = shallowMount(UserAvatarImage, { beforeEach(() => {
propsData: { wrapper = shallowMount(UserAvatarImage, {
...DEFAULT_PROPS, propsData: {
lazy: true, ...PROVIDED_PROPS,
}, },
});
}); });
});
it('should add lazy attributes', () => { it('should have <img> as a child element', () => {
const imageElement = wrapper.find('img'); const imageElement = wrapper.find('img');
expect(imageElement.exists()).toBe(true);
expect(imageElement.attributes('src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(imageElement.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
});
expect(imageElement.classes()).toContain('lazy'); it('should properly render img css', () => {
expect(imageElement.attributes('src')).toBe(placeholderImage); const classes = wrapper.find('img').classes();
expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
expect(classes).not.toContain('lazy');
});
}); });
});
describe('Initialization without src', () => { describe('Initialization when lazy', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(UserAvatarImage); wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
lazy: true,
},
});
});
it('should add lazy attributes', () => {
const imageElement = wrapper.find('img');
expect(imageElement.classes()).toContain('lazy');
expect(imageElement.attributes('src')).toBe(placeholderImage);
expect(imageElement.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
});
}); });
it('should have default avatar image', () => { describe('Initialization without src', () => {
const imageElement = wrapper.find('img'); beforeEach(() => {
wrapper = shallowMount(UserAvatarImage);
});
it('should have default avatar image', () => {
const imageElement = wrapper.find('img');
expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`); expect(imageElement.attributes('src')).toBe(
`${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
);
});
}); });
}); });
describe('dynamic tooltip content', () => { describe('dynamic tooltip content', () => {
const props = DEFAULT_PROPS; const props = PROVIDED_PROPS;
const slots = { const slots = {
default: ['Action!'], default: ['Action!'],
}; };
...@@ -91,11 +181,11 @@ describe('User Avatar Image Component', () => { ...@@ -91,11 +181,11 @@ describe('User Avatar Image Component', () => {
}); });
it('renders the tooltip slot', () => { it('renders the tooltip slot', () => {
expect(wrapper.find('.js-user-avatar-image-tooltip').exists()).toBe(true); expect(wrapper.findComponent(GlTooltip).exists()).toBe(true);
}); });
it('renders the tooltip content', () => { it('renders the tooltip content', () => {
expect(wrapper.find('.js-user-avatar-image-tooltip').text()).toContain(slots.default[0]); expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
}); });
it('does not render tooltip data attributes for on avatar image', () => { it('does not render tooltip data attributes for on avatar image', () => {
......
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