[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:
committed by
Aaron Bohy
co-authored by
Aaron Bohy
parent
7dc30bf735
commit
89050f61b7
@@ -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,
|
||||
|
||||
@@ -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 < 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 < 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>
|
||||
|
||||
Reference in New Issue
Block a user