Resolve "Add status message from within user menu"
This commit is contained in:
parent
18777ec78d
commit
4edcb02f94
19 changed files with 736 additions and 138 deletions
|
@ -22,6 +22,7 @@ const Api = {
|
|||
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
|
||||
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
|
||||
usersPath: '/api/:version/users.json',
|
||||
userStatusPath: '/api/:version/user/status',
|
||||
commitPath: '/api/:version/projects/:id/repository/commits',
|
||||
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
|
||||
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
|
||||
|
@ -266,6 +267,15 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
postUserStatus({ emoji, message }) {
|
||||
const url = Api.buildUrl(this.userStatusPath);
|
||||
|
||||
return axios.put(url, {
|
||||
emoji,
|
||||
message,
|
||||
});
|
||||
},
|
||||
|
||||
templates(key, params = {}) {
|
||||
const url = Api.buildUrl(this.templatesPath).replace(':key', key);
|
||||
|
||||
|
|
|
@ -42,10 +42,11 @@ export class AwardsHandler {
|
|||
}
|
||||
|
||||
bindEvents() {
|
||||
const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
|
||||
// If the user shows intent let's pre-build the menu
|
||||
this.registerEventListener(
|
||||
'one',
|
||||
$(document),
|
||||
$parentEl,
|
||||
'mouseenter focus',
|
||||
this.toggleButtonSelector,
|
||||
'mouseenter focus',
|
||||
|
@ -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.preventDefault();
|
||||
this.showEmojiMenu($(e.currentTarget));
|
||||
|
@ -76,7 +77,7 @@ export class AwardsHandler {
|
|||
});
|
||||
|
||||
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();
|
||||
const $target = $(e.currentTarget);
|
||||
const $glEmojiElement = $target.find('gl-emoji');
|
||||
|
@ -168,7 +169,8 @@ export class AwardsHandler {
|
|||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
|
||||
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
|
||||
|
||||
this.addRemainingEmojiMenuCategories();
|
||||
this.setupSearch();
|
||||
|
@ -250,6 +252,12 @@ export class AwardsHandler {
|
|||
}
|
||||
|
||||
positionMenu($menu, $addBtn) {
|
||||
if (this.targetContainerEl) {
|
||||
return $menu.css({
|
||||
top: `${$addBtn.outerHeight()}px`,
|
||||
});
|
||||
}
|
||||
|
||||
const position = $addBtn.data('position');
|
||||
// The menu could potentially be off-screen or in a hidden overflow element
|
||||
// So we position the element absolute in the body
|
||||
|
@ -424,9 +432,7 @@ export class AwardsHandler {
|
|||
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
|
||||
}
|
||||
users.unshift('You');
|
||||
return awardBlock
|
||||
.attr('title', this.toSentence(users))
|
||||
.tooltip('_fixTitle');
|
||||
return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
|
||||
}
|
||||
|
||||
createAwardButtonForVotesBlock(votesBlock, emojiName) {
|
||||
|
@ -609,13 +615,11 @@ export class AwardsHandler {
|
|||
let awardsHandlerPromise = null;
|
||||
export default function loadAwardsHandler(reload = false) {
|
||||
if (!awardsHandlerPromise || reload) {
|
||||
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
|
||||
Emoji => {
|
||||
const awardsHandler = new AwardsHandler(Emoji);
|
||||
awardsHandler.bindEvents();
|
||||
return awardsHandler;
|
||||
},
|
||||
);
|
||||
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
|
||||
const awardsHandler = new AwardsHandler(Emoji);
|
||||
awardsHandler.bindEvents();
|
||||
return awardsHandler;
|
||||
});
|
||||
}
|
||||
return awardsHandlerPromise;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
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.
|
||||
|
@ -17,3 +21,54 @@ export default function initTodoToggle() {
|
|||
$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', () => {
|
|||
const statusEmojiField = document.getElementById('js-status-emoji-field');
|
||||
const statusMessageField = document.getElementById('js-status-message-field');
|
||||
|
||||
const toggleNoEmojiPlaceholder = (isVisible) => {
|
||||
const toggleNoEmojiPlaceholder = isVisible => {
|
||||
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
|
||||
placeholderElement.classList.toggle('hidden', !isVisible);
|
||||
};
|
||||
|
@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
});
|
||||
})
|
||||
.catch(() => createFlash('Failed to load emoji list!'));
|
||||
.catch(() => createFlash('Failed to load emoji list.'));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
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;
|
3
app/assets/javascripts/set_status_modal/event_hub.js
Normal file
3
app/assets/javascripts/set_status_modal/event_hub.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default new Vue();
|
|
@ -0,0 +1,33 @@
|
|||
<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>
|
|
@ -0,0 +1,241 @@
|
|||
<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 @@
|
|||
}
|
||||
|
||||
.header-user {
|
||||
.dropdown-menu {
|
||||
&.show .dropdown-menu {
|
||||
width: auto;
|
||||
min-width: unset;
|
||||
max-height: 323px;
|
||||
margin-top: 4px;
|
||||
color: $gl-text-color;
|
||||
left: auto;
|
||||
|
@ -542,6 +543,18 @@
|
|||
.user-name {
|
||||
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 {
|
||||
|
@ -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 @@
|
|||
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%);
|
|||
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
|
||||
'Courier New', 'andale mono', 'lucida console', monospace;
|
||||
$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
|
||||
|
@ -634,5 +635,4 @@ Modals
|
|||
*/
|
||||
$modal-body-height: 134px;
|
||||
|
||||
|
||||
$priority-label-empty-state-width: 114px;
|
||||
|
|
|
@ -519,59 +519,7 @@ ul.notes {
|
|||
}
|
||||
|
||||
.note-action-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;
|
||||
}
|
||||
}
|
||||
@include emoji-menu-toggle-button;
|
||||
}
|
||||
|
||||
.discussion-toggle-button {
|
||||
|
|
|
@ -81,14 +81,14 @@
|
|||
// Middle dot divider between each element in a list of items.
|
||||
.middle-dot-divider {
|
||||
&::after {
|
||||
content: "\00B7"; // Middle Dot
|
||||
content: '\00B7'; // Middle Dot
|
||||
padding: 0 6px;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after {
|
||||
content: "";
|
||||
content: '';
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +191,6 @@
|
|||
@include media-breakpoint-down(xs) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.profile-crop-image-container {
|
||||
|
@ -215,7 +214,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.user-profile {
|
||||
.cover-controls a {
|
||||
margin-left: 5px;
|
||||
|
@ -418,7 +416,7 @@ table.u2f-registrations {
|
|||
}
|
||||
|
||||
&.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 {
|
|||
}
|
||||
|
||||
.emoji-menu-toggle-button {
|
||||
@extend .note-action-button;
|
||||
@include emoji-menu-toggle-button;
|
||||
|
||||
.no-emoji-placeholder {
|
||||
position: relative;
|
||||
|
|
|
@ -5,7 +5,14 @@
|
|||
.user-name.bold
|
||||
= current_user.name
|
||||
= 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
|
||||
- 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)
|
||||
%li
|
||||
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
|
||||
|
|
|
@ -74,3 +74,6 @@
|
|||
%span.sr-only= _('Toggle navigation')
|
||||
= 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')
|
||||
|
||||
- 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 : '' } }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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
|
|||
|
||||
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. In the text field below `Your status`, enter your status message.
|
||||
1. Select an emoji from the dropdown if you like.
|
||||
|
|
|
@ -2722,6 +2722,9 @@ msgstr ""
|
|||
msgid "Failed to check related branches."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load emoji list."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to remove issue from board, please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -5445,6 +5448,30 @@ msgstr ""
|
|||
msgid "SetPasswordToCloneLink|set a password"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -61,83 +61,229 @@ describe 'User edit profile' do
|
|||
end
|
||||
|
||||
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.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
|
||||
end
|
||||
|
||||
it 'shows the user status form' do
|
||||
visit(profile_path)
|
||||
context 'profile edit form' do
|
||||
it 'shows the user status form' do
|
||||
visit(profile_path)
|
||||
|
||||
expect(page).to have_content('Current status')
|
||||
end
|
||||
expect(page).to have_content('Current status')
|
||||
end
|
||||
|
||||
it 'adds emoji to user status' do
|
||||
emoji = 'biohazard'
|
||||
visit(profile_path)
|
||||
select_emoji(emoji)
|
||||
submit_settings
|
||||
it 'adds emoji to user status' do
|
||||
emoji = 'biohazard'
|
||||
visit(profile_path)
|
||||
select_emoji(emoji)
|
||||
submit_settings
|
||||
|
||||
visit user_path(user)
|
||||
within('.cover-status') do
|
||||
expect(page).to have_emoji(emoji)
|
||||
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'
|
||||
visit(profile_path)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
submit_settings
|
||||
|
||||
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'
|
||||
visit(profile_path)
|
||||
select_emoji(emoji)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
submit_settings
|
||||
|
||||
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' 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
|
||||
|
||||
visit(profile_path)
|
||||
click_button 'js-clear-user-status-button'
|
||||
submit_settings
|
||||
|
||||
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'
|
||||
visit(profile_path)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
|
||||
within('.js-toggle-emoji-menu') do
|
||||
expect(page).to have_emoji('speech_balloon')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds message to user status' do
|
||||
message = 'I have something to say'
|
||||
visit(profile_path)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
submit_settings
|
||||
context 'user menu' do
|
||||
def open_user_status_modal
|
||||
find('.header-user-dropdown-toggle').click
|
||||
|
||||
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'
|
||||
visit(profile_path)
|
||||
select_emoji(emoji)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
submit_settings
|
||||
|
||||
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' 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
|
||||
page.within ".header-user" do
|
||||
click_button 'Set status'
|
||||
end
|
||||
end
|
||||
|
||||
visit(profile_path)
|
||||
click_button 'js-clear-user-status-button'
|
||||
submit_settings
|
||||
def set_user_status_in_modal
|
||||
page.within "#set-user-status-modal" do
|
||||
click_button 'Set status'
|
||||
end
|
||||
end
|
||||
|
||||
visit user_path(user)
|
||||
expect(page).not_to have_selector '.cover-status'
|
||||
end
|
||||
before do
|
||||
visit root_path(user)
|
||||
end
|
||||
|
||||
it 'displays a default emoji if only message is entered' do
|
||||
message = 'a status without emoji'
|
||||
visit(profile_path)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
it 'shows the "Set status" menu item in the user menu' do
|
||||
find('.header-user-dropdown-toggle').click
|
||||
|
||||
within('.js-toggle-emoji-menu') do
|
||||
expect(page).to have_emoji('speech_balloon')
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue