Commit 7513e38b authored by peterhegman's avatar peterhegman

Show pronouns in user popover

Show pronouns in user popover that is displayed when hovering over
user's name on issues and MRs.

Changelog: added
parent 1dea3fe9
......@@ -22,8 +22,16 @@ export default {
required: false,
default: '',
},
pronouns: {
type: String,
required: false,
default: '',
},
},
computed: {
hasPronouns() {
return this.pronouns !== '';
},
isBusy() {
return isUserBusy(this.availability);
},
......@@ -32,9 +40,25 @@ export default {
</script>
<template>
<span :class="containerClasses">
<gl-sprintf v-if="isBusy" :message="s__('UserAvailability|%{author} (Busy)')">
<template #author>{{ name }}</template>
<gl-sprintf
v-if="isBusy"
:message="s__('UserAvailability|%{author} %{spanStart}(Busy)%{spanEnd}')"
>
<template #author>
{{ name }}
<span v-if="hasPronouns" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal"
>({{ pronouns }})</span
>
</template>
<template #span="{ content }">
<span class="gl-text-gray-500 gl-font-sm gl-font-weight-normal">{{ content }}</span>
</template>
</gl-sprintf>
<template v-else>{{ name }}</template>
<template v-else>
{{ name }}
<span v-if="hasPronouns" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal"
>({{ pronouns }})</span
>
</template>
</span>
</template>
......@@ -44,6 +44,7 @@ const populateUserInfo = (user) => {
bioHtml: sanitize(userData.bio_html),
workInformation: userData.work_information,
websiteUrl: userData.website_url,
pronouns: userData.pronouns,
loaded: true,
});
}
......
......@@ -72,7 +72,11 @@ export default {
<template v-else>
<div class="gl-mb-3">
<h5 class="gl-m-0">
<user-name-with-status :name="user.name" :availability="availabilityStatus" />
<user-name-with-status
:name="user.name"
:availability="availabilityStatus"
:pronouns="user.pronouns"
/>
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
</div>
......
......@@ -36,7 +36,10 @@ exports[`Event Item with action buttons renders the action buttons 1`] = `
<span
class="note-header-author-name gl-font-weight-bold"
>
Tanuki
Tanuki
<!---->
</span>
</a>
......
......@@ -5,7 +5,7 @@ module API
class User < UserBasic
include UsersHelper
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title
expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :pronouns
expose :bot?, as: :bot
expose :work_information do |user|
work_information(user)
......
......@@ -35577,6 +35577,9 @@ msgstr ""
msgid "User was successfully updated."
msgstr ""
msgid "UserAvailability|%{author} %{spanStart}(Busy)%{spanEnd}"
msgstr ""
msgid "UserAvailability|%{author} (Busy)"
msgstr ""
......
......@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe 'User sees user popover', :js do
include Spec::Support::Helpers::Features::NotesHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, pronouns: 'they/them') }
let_it_be(:project) { create(:project, :repository, creator: user) }
let(:user) { project.creator }
let(:merge_request) do
create(:merge_request, source_project: project, target_project: project)
end
......@@ -32,7 +32,7 @@ RSpec.describe 'User sees user popover', :js do
expect(page).to have_css(popover_selector, visible: true)
page.within(popover_selector) do
expect(page).to have_content(user.name)
expect(page).to have_content("#{user.name} (they/them)")
end
end
......
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
const name = 'Goku';
const name = 'Administrator';
const containerClasses = 'gl-cool-class gl-over-9000';
describe('UserNameWithStatus', () => {
let wrapper;
function createComponent(props = {}) {
return shallowMount(UserNameWithStatus, {
wrapper = mount(UserNameWithStatus, {
propsData: { name, containerClasses, ...props },
stubs: {
GlSprintf,
},
});
}
beforeEach(() => {
wrapper = createComponent();
createComponent();
});
afterEach(() => {
......@@ -41,11 +37,35 @@ describe('UserNameWithStatus', () => {
describe(`with availability="${AVAILABILITY_STATUS.BUSY}"`, () => {
beforeEach(() => {
wrapper = createComponent({ availability: AVAILABILITY_STATUS.BUSY });
createComponent({ availability: AVAILABILITY_STATUS.BUSY });
});
it('will render "Busy"', () => {
expect(wrapper.html()).toContain('Goku (Busy)');
expect(wrapper.text()).toContain('(Busy)');
});
});
describe('when user has pronouns set', () => {
const pronouns = 'they/them';
beforeEach(() => {
createComponent({ pronouns });
});
it("renders user's name with pronouns", () => {
expect(wrapper.text()).toMatchInterpolatedText(`Administrator (${pronouns})`);
});
});
describe('when user does not have pronouns set', () => {
it.each`
pronouns
${undefined}
${''}
`("renders the user's name", ({ pronouns }) => {
createComponent({ pronouns });
expect(wrapper.text()).toMatchInterpolatedText('Administrator');
});
});
});
import { GlSkeletonLoader, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
......@@ -13,6 +13,7 @@ const DEFAULT_PROPS = {
bio: null,
workInformation: null,
status: null,
pronouns: 'they/them',
loaded: true,
},
};
......@@ -30,23 +31,18 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
const findByTestId = (testid) => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => findByTestId('user-popover-bot-docs-link');
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(UserPopover, {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: findTarget(),
...props,
},
stubs: {
GlSprintf,
UserNameWithStatus,
},
...options,
});
};
......@@ -232,6 +228,12 @@ describe('User Popover Component', () => {
expect(wrapper.text()).not.toContain('(Busy)');
});
it('passes `pronouns` prop to `UserNameWithStatus` component', () => {
createWrapper();
expect(findUserName().props('pronouns')).toBe('they/them');
});
});
describe('bot user', () => {
......
......@@ -9,7 +9,7 @@ RSpec.describe API::Entities::User do
subject { described_class.new(user, current_user: current_user).as_json }
it 'exposes correct attributes' do
expect(subject).to include(:bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :work_information)
expect(subject).to include(:bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :work_information, :pronouns)
end
it 'exposes created_at if the current user can read the user profile' do
......
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