Commit c07e57bd authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Luke Duncalfe

Add clear user status dropdown

This dropdown gets added to the set user
status modal. It allows a user the option
to set a predefined range to clear their
status.
parent bd8e2856
......@@ -55,12 +55,13 @@ export function getUserProjects(userId, query, options, callback) {
.catch(() => flash(__('Something went wrong while fetching projects')));
}
export function updateUserStatus({ emoji, message, availability }) {
export function updateUserStatus({ emoji, message, availability, clearStatusAfter }) {
const url = buildApiUrl(USER_POST_STATUS_PATH);
return axios.put(url, {
emoji,
message,
availability,
clear_status_after: clearStatusAfter,
});
}
......@@ -46,6 +46,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
currentClearStatusAfter,
} = setStatusModalWrapperEl.dataset;
return {
......@@ -54,6 +55,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
currentClearStatusAfter,
};
},
render(createElement) {
......@@ -63,6 +65,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
currentClearStatusAfter,
} = this;
return createElement(SetStatusModalWrapper, {
......@@ -72,6 +75,7 @@ function initStatusTriggers() {
currentMessage,
currentAvailability,
canSetUserAvailability,
currentClearStatusAfter,
},
});
},
......
<script>
/* eslint-disable vue/no-v-html */
import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui';
import {
GlToast,
GlModal,
GlTooltipDirective,
GlIcon,
GlFormCheckbox,
GlDropdown,
GlDropdownItem,
} from '@gitlab/ui';
import $ from 'jquery';
import Vue from 'vue';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import { timeRanges } from '~/vue_shared/constants';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy } from './utils';
......@@ -20,11 +29,21 @@ export const AVAILABILITY_STATUS = {
Vue.use(GlToast);
const statusTimeRanges = [
{
label: __('Never'),
name: 'never',
},
...timeRanges,
];
export default {
components: {
GlIcon,
GlModal,
GlFormCheckbox,
GlDropdown,
GlDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -53,6 +72,11 @@ export default {
required: false,
default: false,
},
currentClearStatusAfter: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -65,6 +89,10 @@ export default {
modalId: 'set-user-status-modal',
noEmoji: true,
availability: isUserBusy(this.currentAvailability),
clearStatusAfter: statusTimeRanges[0].label,
clearStatusAfterMessage: sprintf(s__('SetStatusModal|Your status resets on %{date}.'), {
date: this.currentClearStatusAfter,
}),
};
},
computed: {
......@@ -161,12 +189,16 @@ export default {
this.setStatus();
},
setStatus() {
const { emoji, message, availability } = this;
const { emoji, message, availability, clearStatusAfter } = this;
updateUserStatus({
emoji,
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
clearStatusAfter:
clearStatusAfter === statusTimeRanges[0].label
? null
: clearStatusAfter.replace(' ', '_'),
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
......@@ -183,7 +215,11 @@ export default {
this.closeModal();
},
setClearStatusAfter(after) {
this.clearStatusAfter = after;
},
},
statusTimeRanges,
};
</script>
......@@ -268,10 +304,31 @@ export default {
</div>
<div class="gl-display-flex">
<span class="gl-text-gray-600 gl-ml-5">
{{ s__('SetStatusModal|"Busy" will be shown next to your name') }}
{{ s__('SetStatusModal|A busy indicator is shown next to your name and avatar.') }}
</span>
</div>
</div>
<div class="form-group">
<div class="gl-display-flex gl-align-items-baseline">
<span class="gl-mr-3">{{ s__('SetStatusModal|Clear status after') }}</span>
<gl-dropdown :text="clearStatusAfter" data-testid="clear-status-at-dropdown">
<gl-dropdown-item
v-for="after in $options.statusTimeRanges"
:key="after.name"
:data-testid="after.name"
@click="setClearStatusAfter(after.label)"
>{{ after.label }}</gl-dropdown-item
>
</gl-dropdown>
</div>
<div
v-if="currentClearStatusAfter.length"
class="gl-mt-3 gl-text-gray-400 gl-font-sm"
data-testid="clear-status-at-message"
>
{{ clearStatusAfterMessage }}
</div>
</div>
</div>
</div>
</gl-modal>
......
......@@ -159,13 +159,20 @@ module PageLayoutHelper
end
def user_status_properties(user)
default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), default_emoji: UserStatus::DEFAULT_EMOJI }
default_properties = {
current_emoji: '',
current_message: '',
can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml),
default_emoji: UserStatus::DEFAULT_EMOJI
}
return default_properties unless user&.status
default_properties.merge({
current_emoji: user.status.emoji.to_s,
current_message: user.status.message.to_s,
current_availability: user.status.availability.to_s
current_availability: user.status.availability.to_s,
current_clear_status_after: user.status.clear_status_at.to_s
})
end
......
---
title: User Availability - Allow users to schedule un-setting of their status values
merge_request: 56649
author:
type: added
......@@ -106,7 +106,8 @@ To show private contributions:
## Set your current status
> Introduced in GitLab 11.2.
> - Introduced in GitLab 11.2.
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56649) in GitLab 13.10.
You can provide a custom status message for your user profile along with an emoji that describes it.
This may be helpful when you are out of office or otherwise not available.
......@@ -119,6 +120,7 @@ To set your current status:
1. Select **Set status** or, if you have already set a status, **Edit status**.
1. Set the desired emoji and status message. Status messages must be plain text and 100 characters or less.
They can also contain emoji codes like, `I'm on vacation :palm_tree:`.
1. Select a value from the **Clear status after** dropdown.
1. Select **Set status**. Alternatively, you can select **Remove status** to remove your user status entirely.
You can also set your current status by [using the API](../../api/users.md#user-status).
......
......@@ -27700,7 +27700,7 @@ msgstr ""
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
msgid "SetStatusModal|\"Busy\" will be shown next to your name"
msgid "SetStatusModal|A busy indicator is shown next to your name and avatar."
msgstr ""
msgid "SetStatusModal|Add status emoji"
......@@ -27712,6 +27712,9 @@ msgstr ""
msgid "SetStatusModal|Clear status"
msgstr ""
msgid "SetStatusModal|Clear status after"
msgstr ""
msgid "SetStatusModal|Edit status"
msgstr ""
......@@ -27733,6 +27736,9 @@ msgstr ""
msgid "SetStatusModal|What's your status?"
msgstr ""
msgid "SetStatusModal|Your status resets on %{date}."
msgstr ""
msgid "Sets %{epic_ref} as parent epic."
msgstr ""
......
......@@ -44,6 +44,7 @@ describe('SetStatusModalWrapper', () => {
const findNoEmojiPlaceholder = () => wrapper.find('.js-no-emoji-placeholder');
const findToggleEmojiButton = () => wrapper.find('.js-toggle-emoji-menu');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
const initModal = ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
const modal = findModal();
......@@ -57,18 +58,18 @@ describe('SetStatusModalWrapper', () => {
return wrapper.vm.$nextTick();
};
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent();
return initModal();
});
afterEach(() => {
wrapper.destroy();
mockEmoji.restore();
});
describe('with minimum props', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent();
return initModal();
});
it('sets the hidden status emoji field', () => {
const field = findFormField('emoji');
expect(field.exists()).toBe(true);
......@@ -96,6 +97,14 @@ describe('SetStatusModalWrapper', () => {
findToggleEmojiButton().trigger('click');
expect(wrapper.vm.showEmojiMenu).toHaveBeenCalled();
});
it('displays the clear status at dropdown', () => {
expect(wrapper.find('[data-testid="clear-status-at-dropdown"]').exists()).toBe(true);
});
it('does not display the clear status at message', () => {
expect(findClearStatusAtMessage().exists()).toBe(false);
});
});
describe('with no currentMessage set', () => {
......@@ -146,9 +155,28 @@ describe('SetStatusModalWrapper', () => {
});
});
describe('with currentClearStatusAfter set', () => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent({ currentClearStatusAfter: '2021-01-01 00:00:00 UTC' });
return initModal();
});
it('displays the clear status at message', () => {
const clearStatusAtMessage = findClearStatusAtMessage();
expect(clearStatusAtMessage.exists()).toBe(true);
expect(clearStatusAtMessage.text()).toBe('Your status resets on 2021-01-01 00:00:00 UTC.');
});
});
describe('update status', () => {
describe('succeeds', () => {
beforeEach(() => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent();
await initModal();
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
});
......@@ -167,18 +195,26 @@ describe('SetStatusModalWrapper', () => {
// set the availability status
findAvailabilityCheckbox().vm.$emit('input', true);
// set the currentClearStatusAfter to 30 minutes
wrapper.find('[data-testid="thirtyMinutes"]').vm.$emit('click');
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
const commonParams = { emoji: defaultEmoji, message: defaultMessage };
const commonParams = {
emoji: defaultEmoji,
message: defaultMessage,
};
expect(UserApi.updateUserStatus).toHaveBeenCalledTimes(2);
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(1, {
availability: AVAILABILITY_STATUS.NOT_SET,
clearStatusAfter: null,
...commonParams,
});
expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(2, {
availability: AVAILABILITY_STATUS.BUSY,
clearStatusAfter: '30_minutes',
...commonParams,
});
});
......@@ -208,7 +244,11 @@ describe('SetStatusModalWrapper', () => {
});
describe('with errors', () => {
beforeEach(() => {
beforeEach(async () => {
mockEmoji = await initEmojiMock();
wrapper = createComponent();
await initModal();
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
});
......
......@@ -223,39 +223,37 @@ RSpec.describe PageLayoutHelper do
end
describe '#user_status_properties' do
using RSpec::Parameterized::TableSyntax
let(:user) { build(:user) }
availability_types = Types::AvailabilityEnum.enum
where(:message, :emoji, :availability) do
"Some message" | UserStatus::DEFAULT_EMOJI | availability_types[:busy]
"Some message" | UserStatus::DEFAULT_EMOJI | availability_types[:not_set]
"Some message" | "basketball" | availability_types[:busy]
"Some message" | "basketball" | availability_types[:not_set]
"Some message" | "" | availability_types[:busy]
"Some message" | "" | availability_types[:not_set]
"" | UserStatus::DEFAULT_EMOJI | availability_types[:busy]
"" | UserStatus::DEFAULT_EMOJI | availability_types[:not_set]
"" | "basketball" | availability_types[:busy]
"" | "basketball" | availability_types[:not_set]
"" | "" | availability_types[:busy]
"" | "" | availability_types[:not_set]
end
subject { helper.user_status_properties(user) }
with_them do
it "sets the default user status fields" do
user.status = UserStatus.new(message: message, emoji: emoji, availability: availability)
result = {
context 'when the user has no status' do
it 'returns default properties' do
is_expected.to eq({
current_emoji: '',
current_message: '',
can_set_user_availability: true,
current_availability: availability,
current_emoji: emoji,
current_message: message,
default_emoji: UserStatus::DEFAULT_EMOJI
}
})
end
end
context 'when user has a status' do
let(:time) { 3.hours.ago }
expect(helper.user_status_properties(user)).to eq(result)
before do
user.status = UserStatus.new(message: 'Some message', emoji: 'basketball', availability: 'busy', clear_status_at: time)
end
it 'merges the status properties with the defaults' do
is_expected.to eq({
current_clear_status_after: time.to_s,
current_availability: 'busy',
current_emoji: 'basketball',
current_message: 'Some message',
can_set_user_availability: true,
default_emoji: UserStatus::DEFAULT_EMOJI
})
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