Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
a231a01b
Commit
a231a01b
authored
Mar 10, 2022
by
Olena Horal-Koretska
Committed by
Frédéric Caplette
Mar 10, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Render user avatar image using `GlAvatar`
This is added behind the FF which is introduced in this MR
parent
6a5a8458
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
172 additions
and
55 deletions
+172
-55
app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
...s/vue_shared/components/user_avatar/user_avatar_image.vue
+28
-14
config/feature_flags/development/gl_avatar_for_all_user_avatars.yml
...ture_flags/development/gl_avatar_for_all_user_avatars.yml
+8
-0
ee/spec/features/merge_requests/user_resets_approvers_spec.rb
...pec/features/merge_requests/user_resets_approvers_spec.rb
+1
-0
ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb
...rojects/settings/merge_request_approvals_settings_spec.rb
+1
-0
lib/gitlab/gon_helper.rb
lib/gitlab/gon_helper.rb
+1
-0
spec/features/boards/boards_spec.rb
spec/features/boards/boards_spec.rb
+1
-0
spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
...ures/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+1
-0
spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
...e_shared/components/user_avatar/user_avatar_image_spec.js
+131
-41
No files found.
app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
View file @
a231a01b
<
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>
...
...
config/feature_flags/development/gl_avatar_for_all_user_avatars.yml
0 → 100644
View file @
a231a01b
---
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
ee/spec/features/merge_requests/user_resets_approvers_spec.rb
View file @
a231a01b
...
@@ -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
)
...
...
ee/spec/features/projects/settings/merge_request_approvals_settings_spec.rb
View file @
a231a01b
...
@@ -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
...
...
lib/gitlab/gon_helper.rb
View file @
a231a01b
...
@@ -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.
...
...
spec/features/boards/boards_spec.rb
View file @
a231a01b
...
@@ -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
...
...
spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
View file @
a231a01b
...
@@ -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
...
...
spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
View file @
a231a01b
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
.
find
Component
(
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
.
find
Component
(
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
'
,
()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment