[IMP] web_settings_dashboard,*: improve UX

*crm,project: css selector adapted in tours

This rev. improves the UX of the 'Invite users' and 'Pending
invitations' part of the Settings dashboard.

Task 32804
Closes #19531

Co-authored-by: Aaron Bohy <aab@odoo.com>
This commit is contained in:
Jigar Patel
2018-03-15 08:32:43 +01:00
committed by Aaron Bohy
co-authored by Aaron Bohy
parent 7dc30bf735
commit 89050f61b7
8 changed files with 340 additions and 56 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ tour.STEPS.MENU_MORE, {
content: _t("Configuration options are available in the Settings app."),
position: "bottom"
}, {
trigger: ".o_web_settings_dashboard textarea#user_emails",
trigger: ".o_web_settings_dashboard .o_user_emails",
content: _t("<b>Invite coworkers</b> via email.<br/><i>Enter one email per line.</i>"),
position: "right"
}, tour.STEPS.TOGGLE_HOME_MENU,
+1 -1
View File
@@ -94,7 +94,7 @@ tour.STEPS.MENU_MORE, {
content: _t("Configuration options are available in the Settings app."),
position: "bottom"
}, {
trigger: ".o_web_settings_dashboard textarea#user_emails",
trigger: ".o_web_settings_dashboard .o_user_emails",
content: _t("<b>Invite coworkers</b> via email.<br/><i>Enter one email per line.</i>"),
position: "right"
}, tour.STEPS.TOGGLE_HOME_MENU,
@@ -32,6 +32,7 @@ class WebSettingsDashboard(http.Controller):
SELECT count(u.*)
FROM res_users u
WHERE active=true AND
share=false AND
NOT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id)
""")
pending_count = cr.dictfetchall()[0].get('count')
@@ -39,8 +40,9 @@ class WebSettingsDashboard(http.Controller):
cr.execute("""
SELECT id, login
FROM res_users u
WHERE active=true
AND NOT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id)
WHERE active=true AND
share=false AND
NOT exists(SELECT 1 FROM res_users_log WHERE create_uid=u.id)
ORDER BY id desc
LIMIT 10
""")
@@ -61,55 +61,86 @@ var Dashboard = AbstractAction.extend({
load_company: function (data) {
return new DashboardCompany(this, data.company).replace(this.$('.o_web_settings_dashboard_company'));
}
},
});
var DashboardInvitations = Widget.extend({
template: 'DashboardInvitations',
events: {
'click .o_web_settings_dashboard_invitations': 'send_invitations',
'click .o_web_settings_dashboard_invite': '_onClickInvite',
'click .o_web_settings_dashboard_access_rights': 'on_access_rights_clicked',
'click .o_web_settings_dashboard_user': 'on_user_clicked',
'click .o_web_settings_dashboard_more': 'on_more',
'click .o_badge_remove': '_onClickBadgeRemove',
'keydown .o_user_emails': '_onKeydownUserEmails',
},
init: function(parent, data){
init: function(parent, data) {
this.data = data;
this.parent = parent;
this.emails = [];
return this._super.apply(this, arguments);
},
send_invitations: function(e){
var self = this;
var $target = $(e.currentTarget);
var user_emails = _.filter($(e.delegateTarget).find('#user_emails').val().split(/[\n, ]/), function(email){
return email !== "";
});
var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,63}(?:\.[a-z]{2})?)$/i;
var is_valid_emails = _.every(user_emails, function(email) {
return re.test(email);
});
if (is_valid_emails) {
// Disable button
$target.prop('disabled', true);
$target.find('i.fa-cog').removeClass('hidden');
// Try to create user accountst
this._rpc({
model: 'res.users',
method: 'web_dashboard_create_users',
args: [user_emails],
})
.then(function() {
self.reload();
})
.always(function() {
// Re-enable button
$(e.delegateTarget).find('.o_web_settings_dashboard_invitations').prop('disabled', false);
$(e.delegateTarget).find('i.fa-cog').addClass('hidden');
});
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Creates and appends badges for valid and unique email addresses
*
* @private
*/
_createBadges: function () {
var $userEmails = this.$('.o_user_emails');
var value = $userEmails.val().trim();
if (value) {
// filter out duplicates
var emails = _.uniq(value.split(/[ ,;\n]+/));
// filter out invalid email addresses
var invalidEmails = _.reject(emails, this._validateEmail.bind(this));
if (invalidEmails.length) {
this.do_warn(_.str.sprintf(_t('The following email addresses are invalid: %s.'), invalidEmails.join(', ')));
}
emails = _.difference(emails, invalidEmails);
// filter out already processed or pending addresses
var pendingEmails = _.map(this.data.pending_users, function (info) {
return info[1];
});
var existingEmails = _.intersection(emails, this.emails.concat(pendingEmails));
if (existingEmails.length) {
this.do_warn(_.str.sprintf(_t('The following email addresses already exist: %s.'), existingEmails.join(', ')));
}
emails = _.difference(emails, existingEmails);
// add valid email addresses, if any
if (emails.length) {
this.emails = this.emails.concat(emails);
$userEmails.before(QWeb.render('EmailBadge', {emails: emails}));
$userEmails.val('');
}
}
else {
this.do_warn(_t("Please provide valid email addresses"), "");
}
},
/**
* Removes a given badge from the DOM, and its associated email address
*
* @private
* @param {jQueryElement} $badge
*/
_removeBadge: function ($badge) {
var email = $badge.text().trim();
this.emails = _.without(this.emails, email);
$badge.remove();
},
/**
* @private
* @param {string} email
* @returns {boolean} true iff the given email address is valid
*/
_validateEmail: function (email) {
var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,63}(?:\.[a-z]{2})?)$/i;
return re.test(email);
},
on_access_rights_clicked: function (e) {
var self = this;
@@ -138,11 +169,13 @@ var DashboardInvitations = Widget.extend({
var self = this;
e.preventDefault();
var action = {
name: _t('Users'),
type: 'ir.actions.act_window',
view_type: 'form',
view_mode: 'tree,form',
res_model: 'res.users',
domain: [['log_ids', '=', false]],
context: {search_default_no_share: true},
views: [[false, 'list'], [false, 'form']],
};
this.do_action(action,{
@@ -152,6 +185,58 @@ var DashboardInvitations = Widget.extend({
reload:function(){
return this.parent.load(['invitations']);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onClickBadgeRemove: function (ev) {
var $badge = $(ev.target).closest('.badge');
this._removeBadge($badge);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickInvite: function (ev) {
var self = this;
this._createBadges();
if (this.emails.length) {
var $button = $(ev.target);
$button.button('loading');
this._rpc({
model: 'res.users',
method: 'web_dashboard_create_users',
args: [this.emails],
})
.then(function () {
self.reload();
})
.fail(function () {
$button.button('reset');
});
}
},
/**
* @private
* @param {KeyboardEvent} ev
*/
_onKeydownUserEmails: function (ev) {
var $userEmails = this.$('.o_user_emails');
var keyCodes = [$.ui.keyCode.TAB, $.ui.keyCode.COMMA, $.ui.keyCode.ENTER, $.ui.keyCode.SPACE];
if (_.contains(keyCodes, ev.which)) {
ev.preventDefault();
this._createBadges();
}
// remove last badge on backspace
if (ev.which === $.ui.keyCode.BACKSPACE && this.emails.length && !$userEmails.val()) {
this._removeBadge(this.$('.o_web_settings_dashboard_invitation_form .badge:last'));
}
},
});
var DashboardApps = Widget.extend({
@@ -68,9 +68,6 @@
width: 100%;
}
}
a.o_web_settings_dashboard_user {
line-height: 24px;
}
}
.o_web_settings_dashboard_planner {
.progress {
@@ -46,6 +46,16 @@
<div class="clearfix"/>
</div>
</t>
<t t-name="EmailBadge">
<span t-foreach="emails" t-as="email" class="badge">
<span class="o_badge_text">
<t t-esc='email'/>
</span>
<span class="fa fa-times o_badge_remove" title="Remove Email" role="button"/>
</span>
</t>
<t t-name="DashboardInvitations">
<div class="text-center o_web_settings_dashboard_invitations">
<i class="fa fa-users fa-4x text-muted o_web_settings_dashboard_access_rights" style="cursor: pointer;"></i>
@@ -63,24 +73,21 @@
</div>
<hr/>
<div class="o_web_settings_dashboard_invitation_form">
<strong>Invite new users:</strong>
<textarea rows="3" id="user_emails" placeholder="Enter e-mail addresses (one per line)"></textarea>
<button class="btn btn-primary btn-block o_web_settings_dashboard_invitations" role="button"> <strong><i class="fa fa-cog fa-spin hidden"/> Invite</strong></button>
<p><strong>Invite new users:</strong></p>
<input class="o_user_emails o_input mt8" type="text" placeholder="Enter e-mail addresses"/>
<button class="btn btn-primary btn-block o_web_settings_dashboard_invite mt16" role="button" data-loading-text="Inviting..."><strong>Invite</strong></button>
</div>
<div>
<small class="o_web_settings_dashboard_pending_users text-muted text-center">
<t t-if="pending_users.length">
<div class="o_web_settings_dashboard_pending_users mt8">
<t t-if="pending_users.length">
<p><strong>Pending invitations:</strong></p>
<span t-foreach="pending_users" t-as="pending">
<a href="#" class="badge o_web_settings_dashboard_user" t-att-data-user-id="pending[0]"> <t t-esc="pending[1]"/> </a>
</span>
<t t-if="pending_users.length &lt; widget.data.pending_count">
<br/>
<strong>Pending invitations:</strong><br/>
<t t-foreach="pending_users" t-as="pending">
<a href="#"><div class="o_web_settings_dashboard_user" t-att-data-user-id="pending[0]"> <t t-esc="pending[1]"/></div></a>
</t>
<t t-if="pending_users.length &lt; widget.data.pending_count">
<br/>
<a href="#"><div class="o_web_settings_dashboard_more"><t t-esc="widget.data.pending_count - pending_users.length"/> more</div></a>
</t>
<a href="#" class="o_web_settings_dashboard_more"><t t-esc="widget.data.pending_count - pending_users.length"/> more </a>
</t>
</small>
</t>
</div>
</div>
</t>
@@ -0,0 +1,188 @@
odoo.define('web_settings_dashboard.settings_dashboard_tests', function (require) {
"use strict";
var webSettingsDashboard = require('web_settings_dashboard');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var Dashboard = webSettingsDashboard.Dashboard;
function createDashboard(params) {
var widget = new Widget();
var dashboard = new Dashboard(widget);
dashboard.all_dashboards = ['invitations']; // test only user invitations
testUtils.addMockEnvironment(widget, params);
var originalDestroy = Dashboard.prototype.destroy;
Dashboard.destroy = function () {
dashboard.destroy = originalDestroy;
widget.destroy();
};
dashboard.appendTo($('#qunit-fixture'));
return dashboard;
}
QUnit.module('settings_dashboard', function () {
QUnit.test('Dashboard: Invite new user', function (assert) {
assert.expect(4);
var dashboardData = {
active_users: 4,
pending_counts: 0,
pending_users: [],
};
var dashboard = createDashboard({
mockRPC: function (route, args) {
if (route === '/web_settings_dashboard/data') {
return $.when({
share: {},
users_info: dashboardData,
});
}
if (route === '/web/dataset/call_kw/res.users/web_dashboard_create_users') {
dashboardData.pending_counts++;
dashboardData.pending_users.push([5, args.args[0][0]]);
return $.when(true);
}
return this._super.apply(this, arguments);
},
});
// add email to invite
dashboard.$('.o_user_emails').val('lagan@odoo.com').trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual(dashboard.$('.o_badge_text').text().trim(), 'lagan@odoo.com',
'should generate a badge with provided email');
assert.strictEqual(dashboard.$('.o_user_emails').val(), '',
'input should have been cleared');
// send invitation
dashboard.$('.o_web_settings_dashboard_invite').click();
assert.strictEqual(dashboard.$('.o_web_settings_dashboard_user').text().trim(), 'lagan@odoo.com',
'should have created a badge in pending invitations');
assert.strictEqual(dashboard.$('.o_badge_text').length, 0,
'should have removed the badge from the invite area');
dashboard.destroy();
});
QUnit.test('Dashboard: Invite new user (warnings)', function (assert) {
assert.expect(8);
var dashboard = createDashboard({
mockRPC: function (route) {
if (route === '/web_settings_dashboard/data') {
return $.when({
share: {},
users_info: {
active_users: 4,
pending_counts: 1,
pending_users: [[3, 'xyz@odoo.com']],
},
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: assert.step.bind(assert, 'warning'),
},
});
// enter an invalid email address to invite
dashboard.$('.o_user_emails').val('x@y').trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual(dashboard.$('.o_badge_text').length, 0,
'should not have generated any badge');
assert.strictEqual(dashboard.$('.o_user_emails').val(), 'x@y',
'input should not have been cleared');
assert.verifySteps(['warning']);
// enter an already pending address
dashboard.$('.o_user_emails').val('xyz@odoo.com').trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual(dashboard.$('.o_badge_text').length, 0,
'should not have generated any badge');
assert.strictEqual(dashboard.$('.o_user_emails').val(), 'xyz@odoo.com',
'input should not have been cleared');
assert.verifySteps(['warning', 'warning']);
dashboard.destroy();
});
QUnit.test('Dashboard: Invite a list of users', function (assert) {
assert.expect(2);
var dashboard = createDashboard({
mockRPC: function (route) {
if (route === '/web_settings_dashboard/data') {
return $.when({
share: {},
users_info: {
active_users: 4,
pending_counts: 1,
pending_users: [],
},
});
}
return this._super.apply(this, arguments);
},
});
// simulate a copy paste of multiple email addresses
var emails = ['a@odoo.com', 'b@odoo.com', 'c@odoo.com', 'd@odoo.com'];
dashboard.$('.o_user_emails').val(emails.join(' ')).trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual(dashboard.$('.o_badge_text').length, 4,
'should have generated 4 badges');
assert.strictEqual(dashboard.$('.o_user_emails').val(), '',
'input have been cleared');
dashboard.destroy();
});
QUnit.test('Dashboard: Invite a list of users (with warnings)', function (assert) {
assert.expect(5);
var dashboard = createDashboard({
mockRPC: function (route) {
if (route === '/web_settings_dashboard/data') {
return $.when({
share: {},
users_info: {
active_users: 4,
pending_counts: 1,
pending_users: [[4, 'a@odoo.com'], [5, 'd@odoo.com']],
},
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: assert.step.bind(assert, 'warning'),
},
});
// simulate a copy paste of multiple email addresses
var emails = ['a@odoo.com', 'b@odoo.com', 'x@y', 'd@odoo.com'];
dashboard.$('.o_user_emails').val(emails.join(' ')).trigger($.Event('keydown', {
which: $.ui.keyCode.ENTER,
}));
assert.strictEqual(dashboard.$('.o_badge_text').length, 1,
'should have generated 1 badge');
assert.strictEqual(dashboard.$('.o_user_emails').val(), '',
'input have been cleared');
assert.verifySteps(['warning', 'warning'], "should have triggered 2 warnings");
dashboard.destroy();
});
});
});
@@ -6,4 +6,9 @@
<script type="text/javascript" src="/web_settings_dashboard/static/src/js/dashboard.js"></script>
</xpath>
</template>
<template id="dashboard_qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/web_settings_dashboard/static/tests/dashboard_tests.js"></script>
</xpath>
</template>
</odoo>