diff options
19 files changed, 730 insertions, 132 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index cd800d75f7a..ecc2440c7e6 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 5b0c4285339..cace8bb9dba 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -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; } diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 4ae3a714bee..175d0b8498b 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -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, + }, + }); + }, + }); + } +}); diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index aea7b649c20..c7ce4675573 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -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.')); }); diff --git a/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js new file mode 100644 index 00000000000..14a89ef9293 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js @@ -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; diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue new file mode 100644 index 00000000000..48e5ede80f2 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.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> diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue new file mode 100644 index 00000000000..43f0b6651b9 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -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> diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 11a30d83f03..c430009bfe0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 7f37dd3de91..11597c7e85e 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -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; + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 837016db67b..b7a95f604b8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -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; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index c9e0899425f..a2070087963 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -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 { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index caa839c32a5..f084adaf5d3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -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; diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index 9ed05d6e3d0..261d758622b 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -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 } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 044b49c12cc..39604611440 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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 : '' } } diff --git a/changelogs/unreleased/49075-add-status-message-from-within-user-menu.yml b/changelogs/unreleased/49075-add-status-message-from-within-user-menu.yml new file mode 100644 index 00000000000..2c65c92dd8b --- /dev/null +++ b/changelogs/unreleased/49075-add-status-message-from-within-user-menu.yml @@ -0,0 +1,5 @@ +--- +title: Set user status from within user menu +merge_request: 21643 +author: +type: added diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 8604ea27f99..ab62762f343 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -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. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index be002d84f0d..93c7ff32946 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index 206a3a4fe9a..e168bb0fc89 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -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 - 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 + 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 + visit user_path(user) + within('.cover-status') do + expect(page).to have_emoji('speech_balloon') + expect(page).to have_content message + end 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 + 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 + visit user_path(user) + within('.cover-status') do + expect(page).to have_emoji(emoji) + expect(page).to have_content message + end end - end - it 'clears the user status' do - user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread') + 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) - within('.cover-status') do - expect(page).to have_emoji(user_status.emoji) - expect(page).to have_content user_status.message + visit user_path(user) + expect(page).not_to have_selector '.cover-status' end - visit(profile_path) - click_button 'js-clear-user-status-button' - submit_settings + 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 - visit user_path(user) - expect(page).not_to have_selector '.cover-status' + within('.js-toggle-emoji-menu') do + expect(page).to have_emoji('speech_balloon') + end + end 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 + 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') + within('.js-toggle-emoji-menu') do + expect(page).to have_emoji('speech_balloon') + end end end end |