Commit 2243e4d4 authored by Phil Hughes's avatar Phil Hughes

Merge branch '49075-add-status-message-from-within-user-menu' into 'master'

Resolve "Add status message from within user menu"

Closes #51393 and #49075

See merge request gitlab-org/gitlab-ce!21643
parents 18777ec7 4edcb02f
...@@ -22,6 +22,7 @@ const Api = { ...@@ -22,6 +22,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key', dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines', commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
...@@ -266,6 +267,15 @@ const Api = { ...@@ -266,6 +267,15 @@ const Api = {
}); });
}, },
postUserStatus({ emoji, message }) {
const url = Api.buildUrl(this.userStatusPath);
return axios.put(url, {
emoji,
message,
});
},
templates(key, params = {}) { templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key); const url = Api.buildUrl(this.templatesPath).replace(':key', key);
......
...@@ -42,10 +42,11 @@ export class AwardsHandler { ...@@ -42,10 +42,11 @@ export class AwardsHandler {
} }
bindEvents() { bindEvents() {
const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener( this.registerEventListener(
'one', 'one',
$(document), $parentEl,
'mouseenter focus', 'mouseenter focus',
this.toggleButtonSelector, this.toggleButtonSelector,
'mouseenter focus', 'mouseenter focus',
...@@ -58,7 +59,7 @@ export class AwardsHandler { ...@@ -58,7 +59,7 @@ export class AwardsHandler {
} }
}, },
); );
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => { this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.showEmojiMenu($(e.currentTarget)); this.showEmojiMenu($(e.currentTarget));
...@@ -76,7 +77,7 @@ export class AwardsHandler { ...@@ -76,7 +77,7 @@ export class AwardsHandler {
}); });
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => { this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
...@@ -168,7 +169,8 @@ export class AwardsHandler { ...@@ -168,7 +169,8 @@ export class AwardsHandler {
</div> </div>
`; `;
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories(); this.addRemainingEmojiMenuCategories();
this.setupSearch(); this.setupSearch();
...@@ -250,6 +252,12 @@ export class AwardsHandler { ...@@ -250,6 +252,12 @@ export class AwardsHandler {
} }
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
if (this.targetContainerEl) {
return $menu.css({
top: `${$addBtn.outerHeight()}px`,
});
}
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body // So we position the element absolute in the body
...@@ -424,9 +432,7 @@ export class AwardsHandler { ...@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.unshift('You'); users.unshift('You');
return awardBlock return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
.attr('title', this.toSentence(users))
.tooltip('_fixTitle');
} }
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
...@@ -609,13 +615,11 @@ export class AwardsHandler { ...@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null; let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) { export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) { if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then( awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
Emoji => {
const awardsHandler = new AwardsHandler(Emoji); const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents(); awardsHandler.bindEvents();
return awardsHandler; return awardsHandler;
}, });
);
} }
return awardsHandlerPromise; return awardsHandlerPromise;
} }
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility'; import { highCountTrim } from '~/lib/utils/text_utility';
import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
/** /**
* Updates todo counter when todos are toggled. * Updates todo counter when todos are toggled.
...@@ -17,3 +21,54 @@ export default function initTodoToggle() { ...@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0); $todoPendingCount.toggleClass('hidden', parsedCount === 0);
}); });
} }
document.addEventListener('DOMContentLoaded', () => {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
Vue.use(Translate);
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalTriggerEl,
data() {
const { hasStatus } = this.$options.el.dataset;
return {
hasStatus: hasStatus === 'true',
};
},
render(createElement) {
return createElement(SetStatusModalTrigger, {
props: {
hasStatus: this.hasStatus,
},
});
},
});
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalWrapperEl,
data() {
const { currentEmoji, currentMessage } = this.$options.el.dataset;
return {
currentEmoji,
currentMessage,
};
},
render(createElement) {
const { currentEmoji, currentMessage } = this;
return createElement(SetStatusModalWrapper, {
props: {
currentEmoji,
currentMessage,
},
});
},
});
}
});
...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field'); const statusMessageField = document.getElementById('js-status-message-field');
const toggleNoEmojiPlaceholder = (isVisible) => { const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder'); const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible); placeholderElement.classList.toggle('hidden', !isVisible);
}; };
...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}) })
.catch(() => createFlash('Failed to load emoji list!')); .catch(() => createFlash('Failed to load emoji list.'));
}); });
import { AwardsHandler } from '~/awards_handler';
class EmojiMenuInModal extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
this.targetContainerEl = targetContainerEl;
this.bindEvents();
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenuInModal;
import Vue from 'vue';
export default new Vue();
<script>
import { s__ } from '~/locale';
import eventHub from './event_hub';
export default {
props: {
hasStatus: {
type: Boolean,
required: true,
},
},
computed: {
buttonText() {
return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
},
},
methods: {
openModal() {
eventHub.$emit('openModal');
},
},
};
</script>
<template>
<button
type="button"
class="btn menu-item"
@click="openModal"
>
{{ buttonText }}
</button>
</template>
<script>
import $ from 'jquery';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
},
props: {
currentEmoji: {
type: String,
required: true,
},
currentMessage: {
type: String,
required: true,
},
},
data() {
return {
defaultEmojiTag: '',
emoji: this.currentEmoji,
emojiMenu: null,
emojiTag: '',
isEmojiMenuVisible: false,
message: this.currentMessage,
modalId: 'set-user-status-modal',
noEmoji: true,
};
},
computed: {
isDirty() {
return this.message.length || this.emoji.length;
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
openModal() {
this.$root.$emit('bv::show::modal', this.modalId);
},
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
if (this.emoji) {
this.emojiTag = Emoji.glEmojiTag(this.emoji);
}
this.noEmoji = this.emoji === '';
this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
this.emojiMenu = new EmojiMenuInModal(
Emoji,
toggleEmojiMenuButtonSelector,
emojiMenuClass,
this.setEmoji,
this.$refs.userStatusForm,
);
})
.catch(() => createFlash(__('Failed to load emoji list.')));
},
showEmojiMenu() {
this.isEmojiMenuVisible = true;
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
},
hideEmojiMenu() {
if (!this.isEmojiMenuVisible) {
return;
}
this.isEmojiMenuVisible = false;
this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
},
setDefaultEmoji() {
const { emojiTag } = this;
const hasStatusMessage = this.message;
if (hasStatusMessage && emojiTag) {
return;
}
if (hasStatusMessage) {
this.noEmoji = false;
this.emojiTag = this.defaultEmojiTag;
} else if (emojiTag === this.defaultEmojiTag) {
this.noEmoji = true;
this.clearEmoji();
}
},
setEmoji(emoji, emojiTag) {
this.emoji = emoji;
this.noEmoji = false;
this.clearEmoji();
this.emojiTag = emojiTag;
},
clearEmoji() {
if (this.emojiTag) {
this.emojiTag = '';
}
},
clearStatusInputs() {
this.emoji = '';
this.message = '';
this.noEmoji = true;
this.clearEmoji();
this.hideEmojiMenu();
},
removeStatus() {
this.clearStatusInputs();
this.setStatus();
},
setStatus() {
const { emoji, message } = this;
Api.postUserStatus({
emoji,
message,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
},
onUpdateSuccess() {
this.closeModal();
window.location.reload();
},
onUpdateFail() {
createFlash(
s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."),
);
this.closeModal();
},
},
};
</script>
<template>
<gl-ui-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
:cancel-title="s__('SetStatusModal|Remove status')"
ok-variant="success"
class="set-user-status-modal"
@shown="setupEmojiListAndAutocomplete"
@hide="hideEmojiMenu"
@ok="setStatus"
@cancel="removeStatus"
>
<div>
<input
v-model="emoji"
class="js-status-emoji-field"
type="hidden"
name="user[status][emoji]"
/>
<div
ref="userStatusForm"
class="form-group position-relative m-0"
>
<div class="input-group">
<span class="input-group-btn">
<button
ref="toggleEmojiMenuButton"
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Add status emoji')"
:aria-label="s__('SetStatusModal|Add status emoji')"
name="button"
type="button"
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
@click="showEmojiMenu"
>
<span v-html="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
<icon
name="emoji_slightly_smiling_face"
css-classes="award-control-icon-neutral"
/>
<icon
name="emoji_smiley"
css-classes="award-control-icon-positive"
/>
<icon
name="emoji_smile"
css-classes="award-control-icon-super-positive"
/>
</span>
</button>
</span>
<input
ref="statusMessageField"
v-model="message"
:placeholder="s__('SetStatusModal|What\'s your status?')"
type="text"
class="form-control form-control input-lg js-status-message-field"
name="user[status][message]"
@keyup="setDefaultEmoji"
@keyup.enter.prevent
@click="hideEmojiMenu"
/>
<span
v-show="isDirty"
class="input-group-btn"
>
<button
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Clear status')"
:aria-label="s__('SetStatusModal|Clear status')"
name="button"
type="button"
class="js-clear-user-status-button clear-user-status btn"
@click="clearStatusInputs()"
>
<icon name="close" />
</button>
</span>
</div>
</div>
</div>
</gl-ui-modal>
</template>
...@@ -529,9 +529,10 @@ ...@@ -529,9 +529,10 @@
} }
.header-user { .header-user {
.dropdown-menu { &.show .dropdown-menu {
width: auto; width: auto;
min-width: unset; min-width: unset;
max-height: 323px;
margin-top: 4px; margin-top: 4px;
color: $gl-text-color; color: $gl-text-color;
left: auto; left: auto;
...@@ -542,6 +543,18 @@ ...@@ -542,6 +543,18 @@
.user-name { .user-name {
display: block; display: block;
} }
.user-status-emoji {
margin-right: 0;
display: block;
vertical-align: text-top;
max-width: 148px;
font-size: 12px;
gl-emoji {
font-size: $gl-font-size;
}
}
} }
svg { svg {
...@@ -573,3 +586,24 @@ ...@@ -573,3 +586,24 @@
} }
} }
} }
.set-user-status-modal {
.modal-body {
min-height: unset;
}
.input-lg {
max-width: unset;
}
.no-emoji-placeholder,
.clear-user-status {
svg {
fill: $gl-text-color-secondary;
}
}
.emoji-menu-toggle-button {
@include emoji-menu-toggle-button;
}
}
...@@ -266,3 +266,59 @@ ...@@ -266,3 +266,59 @@
border-radius: 50%; border-radius: 50%;
} }
} }
@mixin emoji-menu-toggle-button {
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
}
...@@ -314,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); ...@@ -314,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
/* /*
* Dropdowns * Dropdowns
...@@ -634,5 +635,4 @@ Modals ...@@ -634,5 +635,4 @@ Modals
*/ */
$modal-body-height: 134px; $modal-body-height: 134px;
$priority-label-empty-state-width: 114px; $priority-label-empty-state-width: 114px;
...@@ -519,59 +519,7 @@ ul.notes { ...@@ -519,59 +519,7 @@ ul.notes {
} }
.note-action-button { .note-action-button {
line-height: 1; @include emoji-menu-toggle-button;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
} }
.discussion-toggle-button { .discussion-toggle-button {
......
...@@ -81,14 +81,14 @@ ...@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items. // Middle dot divider between each element in a list of items.
.middle-dot-divider { .middle-dot-divider {
&::after { &::after {
content: "\00B7"; // Middle Dot content: '\00B7'; // Middle Dot
padding: 0 6px; padding: 0 6px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
&:last-child { &:last-child {
&::after { &::after {
content: ""; content: '';
padding: 0; padding: 0;
} }
} }
...@@ -191,7 +191,6 @@ ...@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
width: auto; width: auto;
} }
} }
.profile-crop-image-container { .profile-crop-image-container {
...@@ -215,7 +214,6 @@ ...@@ -215,7 +214,6 @@
} }
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
...@@ -418,7 +416,7 @@ table.u2f-registrations { ...@@ -418,7 +416,7 @@ table.u2f-registrations {
} }
&.unverified { &.unverified {
@include status-color($gray-dark, color("gray"), $common-gray-dark); @include status-color($gray-dark, color('gray'), $common-gray-dark);
} }
} }
} }
...@@ -431,7 +429,7 @@ table.u2f-registrations { ...@@ -431,7 +429,7 @@ table.u2f-registrations {
} }
.emoji-menu-toggle-button { .emoji-menu-toggle-button {
@extend .note-action-button; @include emoji-menu-toggle-button;
.no-emoji-placeholder { .no-emoji-placeholder {
position: relative; position: relative;
......
...@@ -5,7 +5,14 @@ ...@@ -5,7 +5,14 @@
.user-name.bold .user-name.bold
= current_user.name = current_user.name
= current_user.to_reference = current_user.to_reference
- if current_user.status
.user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
= emoji_icon current_user.status.emoji
= current_user.status.message_html.html_safe
%li.divider %li.divider
- if can?(current_user, :update_user_status, current_user)
%li
.js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
- if current_user_menu?(:profile) - if current_user_menu?(:profile)
%li %li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
......
...@@ -74,3 +74,6 @@ ...@@ -74,3 +74,6 @@
%span.sr-only= _('Toggle navigation') %span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
---
title: Set user status from within user menu
merge_request: 21643
author:
type: added
...@@ -115,6 +115,13 @@ Please be aware that your status is publicly visible even if your [profile is pr ...@@ -115,6 +115,13 @@ Please be aware that your status is publicly visible even if your [profile is pr
To set your current status: To set your current status:
1. Open the user menu in the top-right corner of the navigation bar.
1. Hit **Set status**, or **Edit status** if you have already set a status.
1. Set the emoji and/or status message to your liking.
1. Hit **Set status**. Alternatively, you can also hit **Remove status** to remove your user status entirely.
or
1. Navigate to your personal [profile settings](#profile-settings). 1. Navigate to your personal [profile settings](#profile-settings).
1. In the text field below `Your status`, enter your status message. 1. In the text field below `Your status`, enter your status message.
1. Select an emoji from the dropdown if you like. 1. Select an emoji from the dropdown if you like.
......
...@@ -2722,6 +2722,9 @@ msgstr "" ...@@ -2722,6 +2722,9 @@ msgstr ""
msgid "Failed to check related branches." msgid "Failed to check related branches."
msgstr "" msgstr ""
msgid "Failed to load emoji list."
msgstr ""
msgid "Failed to remove issue from board, please try again." msgid "Failed to remove issue from board, please try again."
msgstr "" msgstr ""
...@@ -5445,6 +5448,30 @@ msgstr "" ...@@ -5445,6 +5448,30 @@ msgstr ""
msgid "SetPasswordToCloneLink|set a password" msgid "SetPasswordToCloneLink|set a password"
msgstr "" msgstr ""
msgid "SetStatusModal|Add status emoji"
msgstr ""
msgid "SetStatusModal|Clear status"
msgstr ""
msgid "SetStatusModal|Edit status"
msgstr ""
msgid "SetStatusModal|Remove status"
msgstr ""
msgid "SetStatusModal|Set a status"
msgstr ""
msgid "SetStatusModal|Set status"
msgstr ""
msgid "SetStatusModal|Sorry, we weren't able to set your status. Please try again later."
msgstr ""
msgid "SetStatusModal|What's your status?"
msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
......
...@@ -61,13 +61,15 @@ describe 'User edit profile' do ...@@ -61,13 +61,15 @@ describe 'User edit profile' do
end end
context 'user status', :js do context 'user status', :js do
def select_emoji(emoji_name) def select_emoji(emoji_name, is_modal = false)
emoji_menu_class = is_modal ? '.js-modal-status-emoji-menu' : '.js-status-emoji-menu'
toggle_button = find('.js-toggle-emoji-menu') toggle_button = find('.js-toggle-emoji-menu')
toggle_button.click toggle_button.click
emoji_button = find(%Q{.js-status-emoji-menu .js-emoji-btn gl-emoji[data-name="#{emoji_name}"]}) emoji_button = find(%Q{#{emoji_menu_class} .js-emoji-btn gl-emoji[data-name="#{emoji_name}"]})
emoji_button.click emoji_button.click
end end
context 'profile edit form' do
it 'shows the user status form' do it 'shows the user status form' do
visit(profile_path) visit(profile_path)
...@@ -141,4 +143,148 @@ describe 'User edit profile' do ...@@ -141,4 +143,148 @@ describe 'User edit profile' do
end end
end end
end end
context 'user menu' do
def open_user_status_modal
find('.header-user-dropdown-toggle').click
page.within ".header-user" do
click_button 'Set status'
end
end
def set_user_status_in_modal
page.within "#set-user-status-modal" do
click_button 'Set status'
end
end
before do
visit root_path(user)
end
it 'shows the "Set status" menu item in the user menu' do
find('.header-user-dropdown-toggle').click
page.within ".header-user" do
expect(page).to have_content('Set status')
end
end
it 'shows the "Edit status" menu item in the user menu' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit root_path(user)
find('.header-user-dropdown-toggle').click
page.within ".header-user" do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
expect(page).to have_content('Edit status')
end
end
it 'shows user status modal' do
open_user_status_modal
expect(page.find('#set-user-status-modal')).to be_visible
expect(page).to have_content('Set a status')
end
it 'adds emoji to user status' do
emoji = 'biohazard'
open_user_status_modal
select_emoji(emoji, true)
set_user_status_in_modal
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(emoji)
end
end
it 'adds message to user status' do
message = 'I have something to say'
open_user_status_modal
find('.js-status-message-field').native.send_keys(message)
set_user_status_in_modal
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji('speech_balloon')
expect(page).to have_content message
end
end
it 'adds message and emoji to user status' do
emoji = 'tanabata_tree'
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
find('.js-status-message-field').native.send_keys(message)
set_user_status_in_modal
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(emoji)
expect(page).to have_content message
end
end
it 'clears the user status with the "X" button' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
end
find('.header-user-dropdown-toggle').click
page.within ".header-user" do
click_button 'Edit status'
end
find('.js-clear-user-status-button').click
set_user_status_in_modal
visit user_path(user)
expect(page).not_to have_selector '.cover-status'
end
it 'clears the user status with the "Remove status" button' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
end
find('.header-user-dropdown-toggle').click
page.within ".header-user" do
click_button 'Edit status'
end
page.within "#set-user-status-modal" do
click_button 'Remove status'
end
visit user_path(user)
expect(page).not_to have_selector '.cover-status'
end
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
open_user_status_modal
find('.js-status-message-field').native.send_keys(message)
within('.js-toggle-emoji-menu') do
expect(page).to have_emoji('speech_balloon')
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