Commit 7a3d74af authored by Mike Greiling's avatar Mike Greiling

Merge branch 'winh-restyle-user-status' into 'master'

Restyle status message input on profile settings

Closes #49645

See merge request gitlab-org/gitlab-ce!20903
parents 9c0f5271 1c10e014
......@@ -33,19 +33,24 @@ const categoryLabelMap = {
const IS_VISIBLE = 'is-visible';
const IS_RENDERED = 'is-rendered';
class AwardsHandler {
export class AwardsHandler {
constructor(emoji) {
this.emoji = emoji;
this.eventListeners = [];
this.toggleButtonSelector = '.js-add-award';
this.menuClass = 'js-award-emoji-menu';
}
bindEvents() {
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
$(document),
'mouseenter focus',
'.js-add-award',
this.toggleButtonSelector,
'mouseenter focus',
() => {
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
if ($menu.length === 0) {
requestAnimationFrame(() => {
this.createEmojiMenu();
......@@ -53,7 +58,7 @@ class AwardsHandler {
}
},
);
this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
......@@ -61,15 +66,17 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
if (!$target.closest(`.${this.menuClass}`).length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu'));
if ($(`.${this.menuClass}`).is(':visible')) {
$(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
this.hideMenuElement($(`.${this.menuClass}`));
}
}
});
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
......@@ -101,7 +108,7 @@ class AwardsHandler {
$addBtn.closest('.js-awards-block').addClass('current');
}
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) {
......@@ -118,7 +125,7 @@ class AwardsHandler {
} else {
$addBtn.addClass('is-loading is-active');
this.createEmojiMenu(() => {
const $createdMenu = $('.emoji-menu');
const $createdMenu = $(`.${this.menuClass}`);
$addBtn.removeClass('is-loading');
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
......@@ -156,7 +163,7 @@ class AwardsHandler {
}
const emojiMenuMarkup = `
<div class="emoji-menu">
<div class="emoji-menu ${this.menuClass}">
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content">
......@@ -185,7 +192,7 @@ class AwardsHandler {
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
const menu = document.querySelector('.emoji-menu');
const menu = document.querySelector(`.${this.menuClass}`);
const emojiContentElement = menu.querySelector('.emoji-menu-content');
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
......@@ -270,9 +277,9 @@ class AwardsHandler {
if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
this.hideMenuElement($(`.${this.menuClass}`));
$('.js-add-award.is-active').removeClass('is-active');
$(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
awardName: emoji,
......@@ -291,9 +298,9 @@ class AwardsHandler {
return typeof callback === 'function' ? callback() : undefined;
});
this.hideMenuElement($('.emoji-menu'));
this.hideMenuElement($(`.${this.menuClass}`));
return $('.js-add-award.is-active').removeClass('is-active');
return $(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
}
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
......@@ -321,7 +328,7 @@ class AwardsHandler {
getVotesBlock() {
if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
const $el = $(`${this.toggleButtonSelector}.is-active`).closest('.note.timeline-entry');
if ($el.length) {
return $el;
......@@ -458,7 +465,7 @@ class AwardsHandler {
}
createEmoji(votesBlock, emoji) {
if ($('.emoji-menu').length) {
if ($(`.${this.menuClass}`).length) {
this.createAwardButtonForVotesBlock(votesBlock, emoji);
}
this.createEmojiMenu(() => {
......@@ -538,7 +545,7 @@ class AwardsHandler {
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
......@@ -608,7 +615,7 @@ class AwardsHandler {
this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
$(`.${this.menuClass}`).remove();
}
}
......@@ -616,7 +623,11 @@ let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
Emoji => new AwardsHandler(Emoji),
Emoji => {
const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents();
return awardsHandler;
},
);
}
return awardsHandlerPromise;
......
import { AwardsHandler } from '~/awards_handler';
class EmojiMenu extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenu;
import $ from 'jquery';
import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
import EmojiMenu from './emoji_menu';
document.addEventListener('DOMContentLoaded', () => {
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder');
const removeStatusEmoji = () => {
const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji');
if (statusEmoji) {
statusEmoji.remove();
}
};
const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji;
findNoEmojiPlaceholder().classList.add('hidden');
removeStatusEmoji();
toggleEmojiMenuButton.innerHTML += emojiTag;
};
const clearEmojiButton = document.getElementById('js-clear-user-status-button');
clearEmojiButton.addEventListener('click', () => {
statusEmojiField.value = '';
statusMessageField.value = '';
removeStatusEmoji();
findNoEmojiPlaceholder().classList.remove('hidden');
});
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
const emojiMenu = new EmojiMenu(
Emoji,
toggleEmojiMenuButtonSelector,
'js-status-emoji-menu',
selectEmojiCallback,
);
emojiMenu.bindEvents();
})
.catch(() => createFlash('Failed to load emoji list!'));
});
......@@ -339,3 +339,13 @@ input[type=color].form-control {
vertical-align: unset;
}
}
// Bootstrap 3 compatibility because bootstrap_form Gem is not updated yet
.input-group-btn:first-child {
@extend .input-group-prepend;
}
// Bootstrap 3 compatibility because bootstrap_form Gem is not updated yet
.input-group-btn:last-child {
@extend .input-group-append;
}
......@@ -546,6 +546,7 @@ ul.notes {
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
......
......@@ -418,3 +418,23 @@ table.u2f-registrations {
}
}
}
.edit-user {
.clear-user-status {
svg {
fill: $gl-text-color-secondary;
}
}
.emoji-menu-toggle-button {
@extend .note-action-button;
.no-emoji-placeholder {
position: relative;
svg {
fill: $gl-text-color-secondary;
}
}
}
}
......@@ -9,8 +9,4 @@ module ProfilesHelper
end
end
end
def show_user_status_field?
Feature.enabled?(:user_status_form) || cookies[:feature_user_status_form] == 'true'
end
end
......@@ -31,17 +31,37 @@
%hr
= link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
- if show_user_status_field?
%hr
.row
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0= s_("User|Current Status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface. The message can contain emoji codes, too.")
%h4.prepend-top-0= s_("User|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8
.row
= f.fields_for :status, @user.status do |status_form|
= status_form.text_field :emoji
= status_form.text_field :message, maxlength: 100
- emoji_button = button_tag type: :button,
class: 'js-toggle-emoji-menu emoji-menu-toggle-button btn has-tooltip',
title: s_("Profiles|Add status emoji") do
- if @user.status
= emoji_icon @user.status.emoji
%span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if @user.status) }
= sprite_icon('emoji_slightly_smiling_face', css_class: 'award-control-icon-neutral')
= sprite_icon('emoji_smiley', css_class: 'award-control-icon-positive')
= sprite_icon('emoji_smile', css_class: 'award-control-icon-super-positive')
- reset_message_button = button_tag type: :button,
id: 'js-clear-user-status-button',
class: 'clear-user-status btn has-tooltip',
title: s_("Profiles|Clear status") do
= sprite_icon("close")
= status_form.hidden_field :emoji, id: 'js-status-emoji-field'
= status_form.text_field :message,
id: 'js-status-message-field',
class: 'form-control input-lg',
label: s_("Profiles|Your status"),
prepend: emoji_button,
append: reset_message_button,
placeholder: s_("Profiles|What's your status?")
%hr
.row
.col-lg-4.profile-settings-sidebar
......
---
title: Restyle status message input on profile settings
merge_request: 20903
author:
type: changed
......@@ -4118,9 +4118,15 @@ msgstr ""
msgid "Profiles|Add key"
msgstr ""
msgid "Profiles|Add status emoji"
msgstr ""
msgid "Profiles|Change username"
msgstr ""
msgid "Profiles|Clear status"
msgstr ""
msgid "Profiles|Current path: %{path}"
msgstr ""
......@@ -4148,7 +4154,7 @@ msgstr ""
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr ""
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface. The message can contain emoji codes, too."
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr ""
msgid "Profiles|Type your %{confirmationValue} to confirm:"
......@@ -4166,6 +4172,9 @@ msgstr ""
msgid "Profiles|Username successfully changed"
msgstr ""
msgid "Profiles|What's your status?"
msgstr ""
msgid "Profiles|You don't have access to delete this user."
msgstr ""
......@@ -4175,6 +4184,9 @@ msgstr ""
msgid "Profiles|Your account is currently an owner in these groups:"
msgstr ""
msgid "Profiles|Your status"
msgstr ""
msgid "Profiles|e.g. My MacBook key"
msgstr ""
......@@ -5763,7 +5775,7 @@ msgstr ""
msgid "Users"
msgstr ""
msgid "User|Current Status"
msgid "User|Current status"
msgstr ""
msgid "Variables"
......
......@@ -8,6 +8,10 @@ describe 'User edit profile' do
visit(profile_path)
end
def submit_settings
click_button 'Update profile settings'
end
it 'changes user profile' do
fill_in 'user_skype', with: 'testskype'
fill_in 'user_linkedin', with: 'testlinkedin'
......@@ -16,7 +20,7 @@ describe 'User edit profile' do
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
submit_settings
expect(user.reload).to have_attributes(
skype: 'testskype',
......@@ -34,7 +38,7 @@ describe 'User edit profile' do
context 'user avatar' do
before do
attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
click_button 'Update profile settings'
submit_settings
end
it 'changes user avatar' do
......@@ -56,30 +60,75 @@ describe 'User edit profile' do
end
end
context 'user status' do
it 'hides user status when the feature is disabled' do
stub_feature_flags(user_status_form: false)
context 'user status', :js do
def select_emoji(emoji_name)
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.click
end
it 'shows the user status form' do
visit(profile_path)
expect(page).not_to have_content('Current Status')
expect(page).to have_content('Current status')
end
it 'shows the status form when the feature is enabled' do
stub_feature_flags(user_status_form: true)
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)
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
expect(page).to have_content('Current Status')
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 'shows the status form when the feature is enabled by setting a cookie', :js do
stub_feature_flags(user_status_form: false)
set_cookie('feature_user_status_form', 'true')
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
expect(page).to have_content('Current Status')
visit user_path(user)
expect(page).not_to have_selector '.cover-status'
end
end
end
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import EmojiMenu from '~/pages/profiles/show/emoji_menu';
import { TEST_HOST } from 'spec/test_constants';
describe('EmojiMenu', () => {
const dummyEmojiTag = '<dummy></tag>';
const dummyToggleButtonSelector = '.toggle-button-selector';
const dummyMenuClass = 'dummy-menu-class';
let emojiMenu;
let dummySelectEmojiCallback;
let dummyEmojiList;
beforeEach(() => {
dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback');
dummyEmojiList = {
glEmojiTag() {
return dummyEmojiTag;
},
normalizeEmojiName(emoji) {
return emoji;
},
isEmojiNameValid() {
return true;
},
getEmojiCategoryMap() {
return { dummyCategory: [] };
},
};
emojiMenu = new EmojiMenu(
dummyEmojiList,
dummyToggleButtonSelector,
dummyMenuClass,
dummySelectEmojiCallback,
);
});
afterEach(() => {
emojiMenu.destroy();
});
describe('addAward', () => {
const dummyAwardUrl = `${TEST_HOST}/award/url`;
const dummyEmoji = 'tropical_fish';
const dummyVotesBlock = () => $('<div />');
it('calls selectEmojiCallback', done => {
expect(dummySelectEmojiCallback).not.toHaveBeenCalled();
emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag);
done();
});
});
it('does not make an axios requst', done => {
spyOn(axios, 'request').and.stub();
emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
expect(axios.request).not.toHaveBeenCalled();
done();
});
});
});
describe('bindEvents', () => {
beforeEach(() => {
spyOn(emojiMenu, 'registerEventListener').and.stub();
});
it('binds event listeners to custom toggle button', () => {
emojiMenu.bindEvents();
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'one',
jasmine.anything(),
'mouseenter focus',
dummyToggleButtonSelector,
'mouseenter focus',
jasmine.anything(),
);
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
jasmine.anything(),
'click',
dummyToggleButtonSelector,
jasmine.anything(),
);
});
it('binds event listeners to custom menu class', () => {
emojiMenu.bindEvents();
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
jasmine.anything(),
'click',
`.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
jasmine.anything(),
);
});
});
describe('createEmojiMenu', () => {
it('renders the menu with custom menu class', () => {
const menuElement = () =>
document.body.querySelector(`.emoji-menu.${dummyMenuClass} .emoji-menu-content`);
expect(menuElement()).toBe(null);
emojiMenu.createEmojiMenu();
expect(menuElement()).not.toBe(null);
});
});
});
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