Commit d55ccf50 authored by peterhegman's avatar peterhegman

Implement review comments

Changes include:
- Use GlSprintf component for i18n to prevent encoding of special
characters
- Improve readability of RSpec tests
- Convert `.cover-controls` to utility classes
parent 2959e0fa
<script> <script>
import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; import { GlPopover, GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji'; import { glEmojiTag } from '../../../emoji';
import { s__, sprintf } from '~/locale'; import { s__ } from '~/locale';
import { isString } from 'lodash';
export default { export default {
name: 'UserPopover', name: 'UserPopover',
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
Icon, Icon,
GlPopover, GlPopover,
GlSkeletonLoading, GlSkeletonLoading,
GlSprintf,
UserAvatarImage, UserAvatarImage,
}, },
props: { props: {
...@@ -53,7 +55,10 @@ export default { ...@@ -53,7 +55,10 @@ export default {
const { jobTitle, organization } = this.user; const { jobTitle, organization } = this.user;
if (organization && jobTitle) { if (organization && jobTitle) {
return sprintf(s__('Profile|%{jobTitle} at %{organization}'), { jobTitle, organization }); return {
message: s__('Profile|%{jobTitle} at %{organization}'),
placeholders: { organization, jobTitle },
};
} else if (organization) { } else if (organization) {
return organization; return organization;
} else if (jobTitle) { } else if (jobTitle) {
...@@ -62,6 +67,9 @@ export default { ...@@ -62,6 +67,9 @@ export default {
return null; return null;
}, },
workInformationShouldUseSprintf() {
return !isString(this.workInformation);
},
locationIsLoading() { locationIsLoading() {
return !this.user.loaded && this.user.location === null; return !this.user.loaded && this.user.location === null;
}, },
...@@ -86,17 +94,27 @@ export default { ...@@ -86,17 +94,27 @@ export default {
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" /> <gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</div> </div>
<div class="text-secondary"> <div class="text-secondary">
<div v-if="user.bio" class="js-bio d-flex mb-1"> <div v-if="user.bio" class="d-flex mb-1">
<icon name="profile" class="category-icon flex-shrink-0" /> <icon name="profile" class="category-icon flex-shrink-0" />
<span class="ml-1">{{ user.bio }}</span> <span ref="bio" class="ml-1">{{ user.bio }}</span>
</div> </div>
<div v-if="workInformation" class="js-work-information d-flex mb-1"> <div v-if="workInformation" class="d-flex mb-1">
<icon <icon
v-show="!workInformationIsLoading" v-show="!workInformationIsLoading"
name="work" name="work"
class="category-icon flex-shrink-0" class="category-icon flex-shrink-0"
/> />
<span class="ml-1">{{ workInformation }}</span> <span ref="workInformation" class="ml-1">
<gl-sprintf v-if="workInformationShouldUseSprintf" :message="workInformation.message">
<template
v-for="(placeholder, slotName) in workInformation.placeholders"
v-slot:[slotName]
>
<span :key="slotName">{{ placeholder }}</span>
</template>
</gl-sprintf>
<span v-else>{{ workInformation }}</span>
</span>
</div> </div>
<gl-skeleton-loading <gl-skeleton-loading
v-if="workInformationIsLoading" v-if="workInformationIsLoading"
......
...@@ -161,15 +161,19 @@ ...@@ -161,15 +161,19 @@
} }
.cover-controls { .cover-controls {
@include media-breakpoint-up(sm) {
position: absolute; position: absolute;
top: 10px; top: 1rem;
right: 10px; right: 1.25rem;
}
&.left { &.left {
left: 10px; @include media-breakpoint-up(sm) {
left: 1.25rem;
right: auto; right: auto;
} }
} }
}
&.groups-cover-block { &.groups-cover-block {
background: $white-light; background: $white-light;
......
...@@ -197,23 +197,6 @@ ...@@ -197,23 +197,6 @@
} }
.user-profile { .user-profile {
.cover-controls {
@include media-breakpoint-down(xs) {
position: static;
display: flex;
padding: 0 0.875rem 1.25rem;
}
a {
margin-left: 0.25rem;
@include media-breakpoint-down(xs) {
margin: 0 0.125rem;
flex: 1 0 auto;
}
}
}
.profile-header { .profile-header {
margin: 0 $gl-padding; margin: 0 $gl-padding;
......
- page_title "UI Development Kit", "Help" - page_title "UI Development Kit", "Help"
- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." - lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare."
- link_classes = "flex-grow-1 mx-1 "
.gitlab-ui-dev-kit .gitlab-ui-dev-kit
%h1 GitLab UI development kit %h1 GitLab UI development kit
...@@ -64,7 +65,12 @@ ...@@ -64,7 +65,12 @@
Cover block for profile page with avatar, name and description Cover block for profile page with avatar, name and description
%code .cover-block %code .cover-block
.example .example
.cover-block .cover-block.user-cover-block
= render layout: 'users/cover_controls' do
= link_to '#', class: link_classes + 'btn btn-default' do
= icon('pencil')
= link_to '#', class: link_classes + 'btn btn-default' do
= icon('rss')
.avatar-holder .avatar-holder
= image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: '' = image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title .cover-title
...@@ -73,13 +79,6 @@ ...@@ -73,13 +79,6 @@
.cover-desc.cgray .cover-desc.cgray
= lorem = lorem
.cover-controls
= link_to '#', class: 'btn btn-default' do
= icon('pencil')
&nbsp;
= link_to '#', class: 'btn btn-default' do
= icon('rss')
%h2#lists Lists %h2#lists Lists
.lead .lead
......
.cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
= yield
...@@ -4,30 +4,31 @@ ...@@ -4,30 +4,31 @@
- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name - page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name
- page_description @user.bio - page_description @user.bio
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- link_classes = "flex-grow-1 mx-1 "
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile .user-profile
.cover-block.user-cover-block{ class: [('border-bottom' if profile_tabs.empty?)] } .cover-block.user-cover-block{ class: [('border-bottom' if profile_tabs.empty?)] }
.cover-controls = render layout: 'users/cover_controls' do
- if @user == current_user - if @user == current_user
= link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do = link_to profile_path, class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
= icon('pencil') = icon('pencil')
- elsif current_user - elsif current_user
- if @user.abuse_report - if @user.abuse_report
%button.btn.btn-danger{ title: s_('UserProfile|Already reported for abuse'), %button{ class: link_classes + 'btn btn-danger mr-1', title: s_('UserProfile|Already reported for abuse'),
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } } data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle') = icon('exclamation-circle')
- else - else
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn', = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn',
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle') = icon('exclamation-circle')
- if can?(current_user, :read_user_profile, @user) - if can?(current_user, :read_user_profile, @user)
= link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do = link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
= icon('rss') = icon('rss')
- if current_user && current_user.admin? - if current_user && current_user.admin?
= link_to [:admin, @user], class: 'btn btn-default', title: s_('UserProfile|View user in admin area'), = link_to [:admin, @user], class: link_classes + 'btn btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users') = icon('users')
......
...@@ -383,7 +383,8 @@ describe 'User edit profile' do ...@@ -383,7 +383,8 @@ describe 'User edit profile' do
end end
context 'work information', :js do context 'work information', :js do
it 'shows user\'s job title and organization when both entered' do context 'when job title and organziation are entered' do
it "shows job title and organzation on user's profile" do
fill_in 'user_job_title', with: 'Frontend Engineer' fill_in 'user_job_title', with: 'Frontend Engineer'
fill_in 'user_organization', with: 'GitLab - work info test' fill_in 'user_organization', with: 'GitLab - work info test'
submit_settings submit_settings
...@@ -392,8 +393,10 @@ describe 'User edit profile' do ...@@ -392,8 +393,10 @@ describe 'User edit profile' do
expect(page).to have_content('Frontend Engineer at GitLab - work info test') expect(page).to have_content('Frontend Engineer at GitLab - work info test')
end end
end
it 'shows user\'s job title when only job title is entered' do context 'when only job title is entered' do
it "shows only job title on user's profile" do
fill_in 'user_job_title', with: 'Frontend Engineer - work info test' fill_in 'user_job_title', with: 'Frontend Engineer - work info test'
submit_settings submit_settings
...@@ -401,8 +404,10 @@ describe 'User edit profile' do ...@@ -401,8 +404,10 @@ describe 'User edit profile' do
expect(page).to have_content('Frontend Engineer - work info test') expect(page).to have_content('Frontend Engineer - work info test')
end end
end
it 'shows user\'s organization when only organization is entered' do context 'when only organization is entered' do
it "shows only organization on user's profile" do
fill_in 'user_organization', with: 'GitLab - work info test' fill_in 'user_organization', with: 'GitLab - work info test'
submit_settings submit_settings
...@@ -411,4 +416,5 @@ describe 'User edit profile' do ...@@ -411,4 +416,5 @@ describe 'User edit profile' do
expect(page).to have_content('GitLab - work info test') expect(page).to have_content('GitLab - work info test')
end end
end end
end
end end
import { GlSkeletonLoading } from '@gitlab/ui'; import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -40,6 +40,9 @@ describe('User Popover Component', () => { ...@@ -40,6 +40,9 @@ describe('User Popover Component', () => {
target: findTarget(), target: findTarget(),
...props, ...props,
}, },
stubs: {
'gl-sprintf': GlSprintf,
},
...options, ...options,
}); });
}; };
...@@ -87,13 +90,16 @@ describe('User Popover Component', () => { ...@@ -87,13 +90,16 @@ describe('User Popover Component', () => {
}); });
describe('job data', () => { describe('job data', () => {
const findWorkInformation = () => wrapper.find({ ref: 'workInformation' });
const findBio = () => wrapper.find({ ref: 'bio' });
it('should show only bio if organization and job title are not available', () => { it('should show only bio if organization and job title are not available', () => {
const user = { ...DEFAULT_PROPS.user, bio: 'Engineer' }; const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' };
createWrapper({ user }); createWrapper({ user });
expect(wrapper.text()).toContain('Engineer'); expect(findBio().text()).toBe('My super interesting bio');
expect(wrapper.find('.js-work-information').exists()).toBe(false); expect(findWorkInformation().exists()).toBe(false);
}); });
it('should show only organization if job title is not available', () => { it('should show only organization if job title is not available', () => {
...@@ -101,7 +107,7 @@ describe('User Popover Component', () => { ...@@ -101,7 +107,7 @@ describe('User Popover Component', () => {
createWrapper({ user }); createWrapper({ user });
expect(wrapper.find('.js-work-information > span').text()).toBe('GitLab'); expect(findWorkInformation().text()).toBe('GitLab');
}); });
it('should show only job title if organization is not available', () => { it('should show only job title if organization is not available', () => {
...@@ -109,7 +115,7 @@ describe('User Popover Component', () => { ...@@ -109,7 +115,7 @@ describe('User Popover Component', () => {
createWrapper({ user }); createWrapper({ user });
expect(wrapper.find('.js-work-information > span').text()).toBe('Frontend Engineer'); expect(findWorkInformation().text()).toBe('Frontend Engineer');
}); });
it('should show organization and job title if they are both available', () => { it('should show organization and job title if they are both available', () => {
...@@ -121,40 +127,88 @@ describe('User Popover Component', () => { ...@@ -121,40 +127,88 @@ describe('User Popover Component', () => {
createWrapper({ user }); createWrapper({ user });
expect(wrapper.find('.js-work-information > span').text()).toBe( expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
'Frontend Engineer at GitLab',
);
}); });
it('should display bio and job info in separate lines', () => { it('should display bio and job info in separate lines', () => {
const user = { ...DEFAULT_PROPS.user, bio: 'Engineer', organization: 'GitLab' }; const user = {
...DEFAULT_PROPS.user,
bio: 'My super interesting bio',
organization: 'GitLab',
};
createWrapper({ user }); createWrapper({ user });
expect(wrapper.find('.js-bio').text()).toContain('Engineer'); expect(findBio().text()).toBe('My super interesting bio');
expect(wrapper.find('.js-work-information').text()).toContain('GitLab'); expect(findWorkInformation().text()).toBe('GitLab');
}); });
it('should not encode special characters in bio and organization', () => { it('should not encode special characters in bio', () => {
const user = {
...DEFAULT_PROPS.user,
bio: 'I like <html> & CSS',
};
createWrapper({ user });
expect(findBio().text()).toBe('I like <html> & CSS');
});
it('should not encode special characters in organization', () => {
const user = { const user = {
...DEFAULT_PROPS.user, ...DEFAULT_PROPS.user,
bio: 'Manager & Team Lead',
organization: 'Me & my <funky> Company', organization: 'Me & my <funky> Company',
}; };
createWrapper({ user }); createWrapper({ user });
expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead'); expect(findWorkInformation().text()).toBe('Me & my <funky> Company');
expect(wrapper.find('.js-work-information').text()).toContain('Me & my <funky> Company'); });
it('should not encode special characters in job title', () => {
const user = {
...DEFAULT_PROPS.user,
jobTitle: 'Manager & Team Lead',
};
createWrapper({ user });
expect(findWorkInformation().text()).toBe('Manager & Team Lead');
});
it('should not encode special characters when both job title and organization are set', () => {
const user = {
...DEFAULT_PROPS.user,
jobTitle: 'Manager & Team Lead',
organization: 'Me & my <funky> Company',
};
createWrapper({ user });
expect(findWorkInformation().text()).toBe('Manager & Team Lead at Me & my <funky> Company');
}); });
it('shows icon for bio', () => { it('shows icon for bio', () => {
const user = {
...DEFAULT_PROPS.user,
bio: 'My super interesting bio',
};
createWrapper({ user });
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual( expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual(
1, 1,
); );
}); });
it('shows icon for organization', () => { it('shows icon for organization', () => {
const user = {
...DEFAULT_PROPS.user,
organization: 'GitLab',
};
createWrapper({ user });
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1); expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'work').length).toEqual(1);
}); });
}); });
......
...@@ -180,27 +180,40 @@ describe UsersHelper do ...@@ -180,27 +180,40 @@ describe UsersHelper do
end end
describe '#work_information' do describe '#work_information' do
it "returns job title concatinated with organization if both are present" do subject { helper.work_information(user) }
user = create(:user, organization: 'GitLab', job_title: 'Frontend Engineer')
expect(helper.work_information(user)).to eq('Frontend Engineer at GitLab') context 'when both job_title and organization are present' do
let(:user) { create(:user, organization: 'GitLab', job_title: 'Frontend Engineer') }
it 'returns job title concatinated with organization' do
is_expected.to eq('Frontend Engineer at GitLab')
end end
end
context 'when only organization is present' do
let(:user) { create(:user, organization: 'GitLab') }
it "returns organization if only organization is present" do it "returns organization" do
user = create(:user, organization: 'GitLab') is_expected.to eq('GitLab')
expect(helper.work_information(user)).to eq('GitLab')
end end
end
context 'when only job_title is present' do
let(:user) { create(:user, job_title: 'Frontend Engineer') }
it "returns job title if only job_title is present" do it 'returns job title' do
user = create(:user, job_title: 'Frontend Engineer') is_expected.to eq('Frontend Engineer')
expect(helper.work_information(user)).to eq('Frontend Engineer') end
end end
it "returns nil if job_title and organization are not present" do context 'when neither organization nor job_title are present' do
expect(helper.work_information(user)).to be_nil it { is_expected.to be_nil }
end end
it "returns nil user paramater is nil" do context 'when user parameter is nil' do
expect(helper.work_information(nil)).to be_nil let(:user) { nil }
it { is_expected.to be_nil }
end end
end end
end end
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