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,13 +161,17 @@ ...@@ -161,13 +161,17 @@
} }
.cover-controls { .cover-controls {
position: absolute; @include media-breakpoint-up(sm) {
top: 10px; position: absolute;
right: 10px; top: 1rem;
right: 1.25rem;
}
&.left { &.left {
left: 10px; @include media-breakpoint-up(sm) {
right: auto; left: 1.25rem;
right: auto;
}
} }
} }
......
...@@ -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,32 +383,38 @@ describe 'User edit profile' do ...@@ -383,32 +383,38 @@ 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
fill_in 'user_job_title', with: 'Frontend Engineer' it "shows job title and organzation on user's profile" do
fill_in 'user_organization', with: 'GitLab - work info test' fill_in 'user_job_title', with: 'Frontend Engineer'
submit_settings fill_in 'user_organization', with: 'GitLab - work info test'
submit_settings
visit_user visit_user
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
fill_in 'user_job_title', with: 'Frontend Engineer - work info test' it "shows only job title on user's profile" do
submit_settings fill_in 'user_job_title', with: 'Frontend Engineer - work info test'
submit_settings
visit_user visit_user
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
fill_in 'user_organization', with: 'GitLab - work info test' it "shows only organization on user's profile" do
submit_settings fill_in 'user_organization', with: 'GitLab - work info test'
submit_settings
visit_user visit_user
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
it "returns organization if only organization is present" do context 'when only organization is present' do
user = create(:user, organization: 'GitLab') let(:user) { create(:user, organization: 'GitLab') }
expect(helper.work_information(user)).to eq('GitLab')
it "returns organization" do
is_expected.to eq('GitLab')
end
end end
it "returns job title if only job_title is present" do context 'when only job_title is present' do
user = create(:user, job_title: 'Frontend Engineer') let(:user) { create(:user, job_title: 'Frontend Engineer') }
expect(helper.work_information(user)).to eq('Frontend Engineer')
it 'returns job title' do
is_expected.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