GitLab wurde erfolgreich aktualisiert. Durch regelmäßige Updates bleibt das THM GitLab sicher. Danke für Ihre Geduld.

Commit 1c10e014 authored by Winnie Hellmann's avatar Winnie Hellmann

Restyle status message input on profile settings

parent 313b79d8
......@@ -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 @@ def attribute_provider_label(attribute)
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
......@@ -4067,9 +4067,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 ""
......@@ -4097,7 +4103,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:"
......@@ -4115,6 +4121,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 ""
......@@ -4124,6 +4133,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 ""
......@@ -5685,7 +5697,7 @@ msgstr ""
msgid "Users"
msgstr ""
msgid "User|Current Status"
msgid "User|Current status"
msgstr ""
msgid "Variables"
......
......@@ -8,6 +8,10 @@
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 @@
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 @@
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 @@
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', () => {