Commit 62ad1a2f authored by Frédéric Caplette's avatar Frédéric Caplette

Merge branch '352508-gl-avatars-for-all-user-avatars' into 'master'

Render user avatar image using `GlAvatar`

See merge request gitlab-org/gitlab!81437
parents efaec9fc a231a01b
...@@ -6,25 +6,28 @@ ...@@ -6,25 +6,28 @@
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,11 +26,87 @@ describe('User Avatar Image Component', () => { ...@@ -21,11 +26,87 @@ describe('User Avatar Image Component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('`glAvatarForAllUserAvatars` feature flag enabled', () => {
describe('Initialization', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
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');
});
});
describe('Initialization when lazy', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
propsData: {
...PROVIDED_PROPS,
lazy: true,
},
provide: {
glFeatures: {
glAvatarForAllUserAvatars: true,
},
},
});
});
it('should add lazy attributes', () => {
const avatar = wrapper.findComponent(GlAvatar);
expect(avatar.classes()).toContain('lazy');
expect(avatar.attributes()).toMatchObject({
src: placeholderImage,
'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
});
});
});
describe('Initialization without src', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage);
});
it('should have default avatar image', () => {
const imageElement = wrapper.find('img');
expect(imageElement.attributes('src')).toBe(
`${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
);
});
});
});
describe('`glAvatarForAllUserAvatars` feature flag disabled', () => {
describe('Initialization', () => { describe('Initialization', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, { wrapper = shallowMount(UserAvatarImage, {
propsData: { propsData: {
...DEFAULT_PROPS, ...PROVIDED_PROPS,
}, },
}); });
}); });
...@@ -34,14 +115,18 @@ describe('User Avatar Image Component', () => { ...@@ -34,14 +115,18 @@ describe('User Avatar Image Component', () => {
const imageElement = wrapper.find('img'); const imageElement = wrapper.find('img');
expect(imageElement.exists()).toBe(true); expect(imageElement.exists()).toBe(true);
expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); expect(imageElement.attributes('src')).toBe(
expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt); );
expect(imageElement.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
}); });
it('should properly render img css', () => { it('should properly render img css', () => {
const classes = wrapper.find('img').classes(); const classes = wrapper.find('img').classes();
expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses])); expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
expect(classes).not.toContain('lazy'); expect(classes).not.toContain('lazy');
}); });
}); });
...@@ -50,7 +135,7 @@ describe('User Avatar Image Component', () => { ...@@ -50,7 +135,7 @@ describe('User Avatar Image Component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, { wrapper = shallowMount(UserAvatarImage, {
propsData: { propsData: {
...DEFAULT_PROPS, ...PROVIDED_PROPS,
lazy: true, lazy: true,
}, },
}); });
...@@ -61,7 +146,9 @@ describe('User Avatar Image Component', () => { ...@@ -61,7 +146,9 @@ describe('User Avatar Image Component', () => {
expect(imageElement.classes()).toContain('lazy'); expect(imageElement.classes()).toContain('lazy');
expect(imageElement.attributes('src')).toBe(placeholderImage); expect(imageElement.attributes('src')).toBe(placeholderImage);
expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); expect(imageElement.attributes('data-src')).toBe(
`${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
);
}); });
}); });
...@@ -73,12 +160,15 @@ describe('User Avatar Image Component', () => { ...@@ -73,12 +160,15 @@ describe('User Avatar Image Component', () => {
it('should have default avatar image', () => { it('should have default avatar image', () => {
const imageElement = wrapper.find('img'); 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