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>
import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
import { GlPopover, GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
import { s__, sprintf } from '~/locale';
import { s__ } from '~/locale';
import { isString } from 'lodash';
export default {
name: 'UserPopover',
......@@ -11,6 +12,7 @@ export default {
Icon,
GlPopover,
GlSkeletonLoading,
GlSprintf,
UserAvatarImage,
},
props: {
......@@ -53,7 +55,10 @@ export default {
const { jobTitle, organization } = this.user;
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) {
return organization;
} else if (jobTitle) {
......@@ -62,6 +67,9 @@ export default {
return null;
},
workInformationShouldUseSprintf() {
return !isString(this.workInformation);
},
locationIsLoading() {
return !this.user.loaded && this.user.location === null;
},
......@@ -86,17 +94,27 @@ export default {
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</div>
<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" />
<span class="ml-1">{{ user.bio }}</span>
<span ref="bio" class="ml-1">{{ user.bio }}</span>
</div>
<div v-if="workInformation" class="js-work-information d-flex mb-1">
<div v-if="workInformation" class="d-flex mb-1">
<icon
v-show="!workInformationIsLoading"
name="work"
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>
<gl-skeleton-loading
v-if="workInformationIsLoading"
......
......@@ -161,15 +161,19 @@
}
.cover-controls {
@include media-breakpoint-up(sm) {
position: absolute;
top: 10px;
right: 10px;
top: 1rem;
right: 1.25rem;
}
&.left {
left: 10px;
@include media-breakpoint-up(sm) {
left: 1.25rem;
right: auto;
}
}
}
&.groups-cover-block {
background: $white-light;
......
......@@ -197,23 +197,6 @@
}
.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 {
margin: 0 $gl-padding;
......
- 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."
- link_classes = "flex-grow-1 mx-1 "
.gitlab-ui-dev-kit
%h1 GitLab UI development kit
......@@ -64,7 +65,12 @@
Cover block for profile page with avatar, name and description
%code .cover-block
.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
= image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: ''
.cover-title
......@@ -73,13 +79,6 @@
.cover-desc.cgray
= 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
.lead
......
.cover-controls.d-flex.px-2.pb-4.d-sm-block.p-sm-0
= yield
......@@ -4,30 +4,31 @@
- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
- link_classes = "flex-grow-1 mx-1 "
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
.cover-block.user-cover-block{ class: [('border-bottom' if profile_tabs.empty?)] }
.cover-controls
= render layout: 'users/cover_controls' do
- 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')
- elsif current_user
- 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' } }
= icon('exclamation-circle')
- 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
= icon('exclamation-circle')
- 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')
- 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
= icon('users')
......
......@@ -383,7 +383,8 @@ describe 'User edit profile' do
end
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_organization', with: 'GitLab - work info test'
submit_settings
......@@ -392,8 +393,10 @@ describe 'User edit profile' do
expect(page).to have_content('Frontend Engineer at GitLab - work info test')
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'
submit_settings
......@@ -401,8 +404,10 @@ describe 'User edit profile' do
expect(page).to have_content('Frontend Engineer - work info test')
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'
submit_settings
......@@ -411,4 +416,5 @@ describe 'User edit profile' do
expect(page).to have_content('GitLab - work info test')
end
end
end
end
import { GlSkeletonLoading } from '@gitlab/ui';
import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import Icon from '~/vue_shared/components/icon.vue';
......@@ -40,6 +40,9 @@ describe('User Popover Component', () => {
target: findTarget(),
...props,
},
stubs: {
'gl-sprintf': GlSprintf,
},
...options,
});
};
......@@ -87,13 +90,16 @@ describe('User Popover Component', () => {
});
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', () => {
const user = { ...DEFAULT_PROPS.user, bio: 'Engineer' };
const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' };
createWrapper({ user });
expect(wrapper.text()).toContain('Engineer');
expect(wrapper.find('.js-work-information').exists()).toBe(false);
expect(findBio().text()).toBe('My super interesting bio');
expect(findWorkInformation().exists()).toBe(false);
});
it('should show only organization if job title is not available', () => {
......@@ -101,7 +107,7 @@ describe('User Popover Component', () => {
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', () => {
......@@ -109,7 +115,7 @@ describe('User Popover Component', () => {
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', () => {
......@@ -121,40 +127,88 @@ describe('User Popover Component', () => {
createWrapper({ user });
expect(wrapper.find('.js-work-information > span').text()).toBe(
'Frontend Engineer at GitLab',
);
expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab');
});
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 });
expect(wrapper.find('.js-bio').text()).toContain('Engineer');
expect(wrapper.find('.js-work-information').text()).toContain('GitLab');
expect(findBio().text()).toBe('My super interesting bio');
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 = {
...DEFAULT_PROPS.user,
bio: 'Manager & Team Lead',
organization: 'Me & my <funky> Company',
};
createWrapper({ user });
expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead');
expect(wrapper.find('.js-work-information').text()).toContain('Me & my <funky> Company');
expect(findWorkInformation().text()).toBe('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', () => {
const user = {
...DEFAULT_PROPS.user,
bio: 'My super interesting bio',
};
createWrapper({ user });
expect(wrapper.findAll(Icon).filter(icon => icon.props('name') === 'profile').length).toEqual(
1,
);
});
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);
});
});
......
......@@ -180,27 +180,40 @@ describe UsersHelper do
end
describe '#work_information' do
it "returns job title concatinated with organization if both are present" do
user = create(:user, organization: 'GitLab', job_title: 'Frontend Engineer')
expect(helper.work_information(user)).to eq('Frontend Engineer at GitLab')
subject { helper.work_information(user) }
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
context 'when only organization is present' do
let(:user) { create(:user, organization: 'GitLab') }
it "returns organization if only organization is present" do
user = create(:user, organization: 'GitLab')
expect(helper.work_information(user)).to eq('GitLab')
it "returns organization" do
is_expected.to eq('GitLab')
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
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
it "returns nil if job_title and organization are not present" do
expect(helper.work_information(user)).to be_nil
context 'when neither organization nor job_title are present' do
it { is_expected.to be_nil }
end
it "returns nil user paramater is nil" do
expect(helper.work_information(nil)).to be_nil
context 'when user parameter is nil' do
let(:user) { nil }
it { is_expected.to be_nil }
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