Commit f96d2118 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Martin Wortschack

Display busy status in user popover

Minor refactor to reuse some of the
methods for user availability
parent e7650bc4
import $ from 'jquery';
import '~/lib/utils/jquery_at_who';
import { escape, template } from 'lodash';
import { s__ } from '~/locale';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import { isUserBusy } from '~/set_status_modal/utils';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
import { spriteIcon } from './lib/utils/common_utils';
......@@ -39,6 +41,7 @@ export function membersBeforeSave(members) {
title: sanitize(title),
search: sanitize(`${member.username} ${member.name}`),
icon: avatarIcon,
availability: member.availability,
};
});
}
......@@ -253,13 +256,17 @@ class GfmAutoComplete {
alias: 'users',
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
const { avatarTag, username, title, icon } = value;
const { avatarTag, username, title, icon, availability } = value;
if (username != null) {
tmpl = GfmAutoComplete.Members.templateFunction({
avatarTag,
username,
title,
icon,
availabilityStatus:
availability && isUserBusy(availability)
? `<span class="gl-text-gray-500"> ${s__('UserAvailability|(Busy)')}</span>`
: '',
});
}
return tmpl;
......@@ -775,8 +782,10 @@ GfmAutoComplete.Emoji = {
};
// Team Members
GfmAutoComplete.Members = {
templateFunction({ avatarTag, username, title, icon }) {
return `<li>${avatarTag} ${username} <small>${escape(title)}</small> ${icon}</li>`;
templateFunction({ avatarTag, username, title, icon, availabilityStatus }) {
return `<li>${avatarTag} ${username} <small>${escape(
title,
)}${availabilityStatus}</small> ${icon}</li>`;
},
};
GfmAutoComplete.Labels = {
......
<script>
/* eslint-disable vue/no-v-html */
import { mapActions } from 'vuex';
import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlIcon, GlLoadingIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { isUserBusy } from '~/set_status_modal/utils';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
......@@ -11,6 +12,7 @@ export default {
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
GlLoadingIcon,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -85,9 +87,16 @@ export default {
authorStatus() {
return this.author.status_tooltip_html;
},
authorIsBusy() {
const { status } = this.author;
return status?.availability && isUserBusy(status.availability);
},
emojiElement() {
return this.$refs?.authorStatus?.querySelector('gl-emoji');
},
authorName() {
return this.author.name;
},
},
mounted() {
this.emojiTitle = this.emojiElement ? this.emojiElement.getAttribute('title') : '';
......@@ -146,7 +155,12 @@ export default {
:data-username="author.username"
>
<slot name="note-header-info"></slot>
<span class="note-header-author-name bold">{{ author.name }}</span>
<span class="note-header-author-name gl-font-weight-bold">
<gl-sprintf v-if="authorIsBusy" :message="s__('UserAvailability|%{author} (Busy)')">
<template #author>{{ authorName }}</template>
</gl-sprintf>
<template v-else>{{ authorName }}</template>
</span>
</a>
<span
v-if="authorStatus"
......
<script>
import { AVAILABILITY_STATUS, isUserBusy, isValidAvailibility } from '../utils';
export default {
name: 'UserAvailabilityStatus',
props: {
availability: {
type: String,
required: true,
validator: isValidAvailibility,
},
},
computed: {
isBusy() {
const { availability = AVAILABILITY_STATUS.NOT_SET } = this;
return isUserBusy(availability);
},
},
};
</script>
<template>
<span v-if="isBusy" class="gl-font-weight-normal gl-text-gray-500">{{
s__('UserAvailability|(Busy)')
}}</span>
</template>
......@@ -7,6 +7,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import Api from '~/api';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy, isValidAvailibility } from './utils';
import * as Emoji from '~/emoji';
const emojiMenuClass = 'js-modal-status-emoji-menu';
......@@ -28,6 +29,17 @@ export default {
type: String,
required: true,
},
currentAvailability: {
type: String,
required: false,
validator: isValidAvailibility,
default: '',
},
canSetUserAvailability: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -39,6 +51,7 @@ export default {
message: this.currentMessage,
modalId: 'set-user-status-modal',
noEmoji: true,
availability: isUserBusy(this.currentAvailability),
};
},
computed: {
......
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
export const isUserBusy = status => status === AVAILABILITY_STATUS.BUSY;
export const isValidAvailibility = availability =>
availability.length ? Object.values(AVAILABILITY_STATUS).includes(availability) : true;
......@@ -6,6 +6,7 @@ import {
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlIcon,
} from '@gitlab/ui';
import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
......@@ -25,6 +26,7 @@ export default {
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
UserAvailabilityStatus,
},
props: {
target: {
......@@ -63,6 +65,9 @@ export default {
websiteUrl.length
);
},
availabilityStatus() {
return this.user?.status?.availability || null;
},
},
};
</script>
......@@ -89,6 +94,10 @@ export default {
<div class="gl-mb-3">
<h5 class="gl-m-0">
{{ user.name }}
<user-availability-status
v-if="availabilityStatus"
:availability="availabilityStatus"
/>
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
</div>
......
......@@ -29,4 +29,12 @@ module ProfilesHelper
def user_profile?
params[:controller] == 'users'
end
def availability_values
Types::AvailabilityEnum.enum
end
def user_status_set_to_busy?(status)
status&.availability == availability_values[:busy]
end
end
......@@ -45,7 +45,8 @@ module Users
type: user.class.name,
username: user.username,
name: user.name,
avatar_url: user.avatar_url
avatar_url: user.avatar_url,
availability: user&.status&.availability
}
end
......
......@@ -2,8 +2,10 @@
%ul
%li.current-user
.user-name.bold
.user-name.gl-font-weight-bold
= current_user.name
- if current_user&.status && user_status_set_to_busy?(current_user.status)
%span.gl-font-weight-normal.gl-text-gray-500= s_("UserProfile|(Busy)")
= current_user.to_reference
- if current_user.status
.user-status.d-flex.align-items-center.gl-mt-2.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
......
......@@ -47,6 +47,8 @@
.user-info
.cover-title{ itemprop: 'name' }
= @user.name
- if @user&.status && user_status_set_to_busy?(@user.status)
%span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle= s_("UserProfile|(Busy)")
- if @user.status
.cover-status
......
......@@ -32,7 +32,7 @@ exports[`Event Item with action buttons renders the action buttons 1`] = `
>
<span
class="note-header-author-name bold"
class="note-header-author-name gl-font-weight-bold"
>
Tanuki
</span>
......
......@@ -16,7 +16,8 @@ RSpec.describe Groups::ParticipantsService do
type: user.class.name,
username: user.username,
name: user.name,
avatar_url: user.avatar_url
avatar_url: user.avatar_url,
availability: user&.status&.availability
}
end
......
......@@ -29501,6 +29501,12 @@ msgstr ""
msgid "User was successfully updated."
msgstr ""
msgid "UserAvailability|%{author} (Busy)"
msgstr ""
msgid "UserAvailability|(Busy)"
msgstr ""
msgid "UserLists|Add"
msgstr ""
......@@ -29558,6 +29564,9 @@ msgstr ""
msgid "UserList|created %{timeago}"
msgstr ""
msgid "UserProfile|(Busy)"
msgstr ""
msgid "UserProfile|Activity"
msgstr ""
......
......@@ -378,6 +378,7 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
title: '',
icon: '',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> </li>');
});
......@@ -389,6 +390,7 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
......@@ -400,9 +402,24 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
title: 'MyGroup+',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
});
it('should add user availability status if availabilityStatus is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
}),
).toBe(
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
);
});
});
describe('labels', () => {
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlSprintf } from '@gitlab/ui';
import NoteHeader from '~/notes/components/note_header.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -28,6 +30,9 @@ describe('NoteHeader component', () => {
path: '/root',
state: 'active',
username: 'root',
status: {
availability: '',
},
};
const createComponent = props => {
......@@ -37,6 +42,7 @@ describe('NoteHeader component', () => {
actions,
}),
propsData: { ...props },
stubs: { GlSprintf },
});
};
......@@ -97,6 +103,12 @@ describe('NoteHeader component', () => {
expect(wrapper.find('.js-user-link').exists()).toBe(true);
});
it('renders busy status if author availability is set', () => {
createComponent({ author: { ...author, status: { availability: AVAILABILITY_STATUS.BUSY } } });
expect(wrapper.find('.js-user-link').text()).toContain('(Busy)');
});
it('renders deleted user text if author is not passed as a prop', () => {
createComponent();
......
import { shallowMount } from '@vue/test-utils';
import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
describe('UserAvailabilityStatus', () => {
let wrapper;
const createComponent = (props = {}) => {
return shallowMount(UserAvailabilityStatus, {
propsData: {
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('with availability status', () => {
it(`set to ${AVAILABILITY_STATUS.BUSY}`, () => {
wrapper = createComponent({ availability: AVAILABILITY_STATUS.BUSY });
expect(wrapper.text()).toContain('(Busy)');
});
it(`set to ${AVAILABILITY_STATUS.NOT_SET}`, () => {
wrapper = createComponent({ availability: AVAILABILITY_STATUS.NOT_SET });
expect(wrapper.html()).toBe('');
});
});
});
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import UserAvailabilityStatus from '~/set_status_modal/components/user_availability_status.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
const DEFAULT_PROPS = {
user: {
......@@ -34,6 +36,7 @@ describe('User Popover Component', () => {
const findByTestId = testid => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
const findAvailabilityStatus = () => wrapper.find(UserAvailabilityStatus);
const createWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(UserPopover, {
......@@ -43,7 +46,8 @@ describe('User Popover Component', () => {
...props,
},
stubs: {
'gl-sprintf': GlSprintf,
GlSprintf,
UserAvailabilityStatus,
},
...options,
});
......@@ -199,6 +203,30 @@ describe('User Popover Component', () => {
expect(findUserStatus().exists()).toBe(false);
});
it('should show the busy status if user set to busy', () => {
const user = {
...DEFAULT_PROPS.user,
status: { availability: AVAILABILITY_STATUS.BUSY },
};
createWrapper({ user });
expect(findAvailabilityStatus().exists()).toBe(true);
expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain('(Busy)');
});
it('should hide the busy status for any other status', () => {
const user = {
...DEFAULT_PROPS.user,
status: { availability: AVAILABILITY_STATUS.NOT_SET },
};
createWrapper({ user });
expect(wrapper.text()).not.toContain('(Busy)');
});
});
describe('security bot', () => {
......
......@@ -80,6 +80,21 @@ RSpec.describe ProfilesHelper do
end
end
describe "#user_status_set_to_busy?" do
using RSpec::Parameterized::TableSyntax
where(:availability, :result) do
"busy" | true
"not_set" | false
"" | false
nil | false
end
with_them do
it { expect(helper.user_status_set_to_busy?(OpenStruct.new(availability: availability))).to eq(result) }
end
end
def stub_cas_omniauth_provider
provider = OpenStruct.new(
'name' => 'cas3',
......
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