[ADD] web, *: add rainbowification
This commit introduces the 'rainbowification' feature to the web client
(and to some affected addons). This feature is essentially a way to
display a nice friendly message when some business event happens. For
example, a encouraging message is displayed when the user clear her/his
inbox, or when a salesman closes a deal.
The mechanism currently only displays a 'rainbowman', but could be
extended later to add other kind of animation.
There are a few different ways to display a 'rainbowman':
- any JS code can simply trigger_up an event ('show_effect') with some
options: for example
this.trigger_up('show_effect', {
type: 'rainbow_man',
fadeout: 'no',
message: $done,
click_close: false,
});
- an action returned by the server can have an 'effect' key, with some
options
- the do_execute_action method accepts a 'effect' option. This is
useful when one wants to specify a rainbow on an action button in a
form view
Joint work with: dbh <dbh@odoo.com>, ged <ged@odoo.com>
This commit is contained in:
committed by
Géry Debongnie
parent
578edf73b9
commit
4330ea0267
@@ -109,8 +109,15 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
|
||||
'number': state.valuenow,
|
||||
'timePerTransaction': Math.round(dt/1000/state.valuemax)
|
||||
}));
|
||||
$done.appendTo(this.$el.first());
|
||||
this.$('.o_automatic_reconciliation').hide();
|
||||
this.$el.children().hide();
|
||||
// display rainbowman after full reconciliation
|
||||
this.trigger_up('show_effect', {
|
||||
type: 'rainbow_man',
|
||||
fadeout: 'no',
|
||||
message: $done,
|
||||
click_close: false,
|
||||
});
|
||||
this.$el.css('min-height', '450px');
|
||||
}
|
||||
|
||||
if (state.notifications) {
|
||||
|
||||
@@ -35,16 +35,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.done_message {
|
||||
padding: 70px 20% 10px 20%;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
.fa {
|
||||
position: absolute;
|
||||
right: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
.notification_area {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,8 @@
|
||||
</t>
|
||||
|
||||
<div t-name="reconciliation.done" class="done_message">
|
||||
<i class="fa fa-4x fa-thumbs-o-up congrats_icon pull-right"></i>
|
||||
<h2>Congrats, you're all done !</h2>
|
||||
<p>It took you <strong><t t-esc="duration"/></strong> to reconcile <strong><t t-esc="number"/> transactions.</strong><br/>That's on average <t t-esc="timePerTransaction"/> seconds per transaction.</p>
|
||||
<h2>Congrats, you're all done!</h2>
|
||||
<p>You reconciled <strong><t t-esc="number"/></strong> transactions in <strong><t t-esc="duration"/></strong>.<br/>That's on average <t t-esc="timePerTransaction"/> seconds per transaction.</p>
|
||||
</div>
|
||||
|
||||
<t t-name="reconciliation.line">
|
||||
|
||||
@@ -354,7 +354,52 @@ class Lead(models.Model):
|
||||
for lead in self:
|
||||
stage_id = lead._stage_find(domain=[('probability', '=', 100.0), ('on_change', '=', True)])
|
||||
lead.write({'stage_id': stage_id.id, 'probability': 100})
|
||||
return True
|
||||
if lead.user_id and lead.team_id and lead.planned_revenue:
|
||||
query = """
|
||||
SELECT
|
||||
SUM(CASE WHEN user_id = %(user_id)s THEN 1 ELSE 0 END) as total_won,
|
||||
MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '30 days' AND user_id = %(user_id)s THEN planned_revenue ELSE 0 END) as max_user_30,
|
||||
MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '7 days' AND user_id = %(user_id)s THEN planned_revenue ELSE 0 END) as max_user_7,
|
||||
MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '30 days' AND team_id = %(team_id)s THEN planned_revenue ELSE 0 END) as max_team_30,
|
||||
MAX(CASE WHEN date_closed >= CURRENT_DATE - INTERVAL '7 days' AND team_id = %(team_id)s THEN planned_revenue ELSE 0 END) as max_team_7
|
||||
FROM crm_lead
|
||||
WHERE
|
||||
type = 'opportunity'
|
||||
AND
|
||||
active = True
|
||||
AND
|
||||
probability = 100
|
||||
AND
|
||||
DATE_TRUNC('year', date_closed) = DATE_TRUNC('year', CURRENT_DATE)
|
||||
AND
|
||||
(user_id = %(user_id)s OR team_id = %(team_id)s)
|
||||
"""
|
||||
lead.env.cr.execute(query, {'user_id': lead.user_id.id,
|
||||
'team_id': lead.team_id.id})
|
||||
query_result = self.env.cr.dictfetchone()
|
||||
|
||||
message = False
|
||||
if query_result['total_won'] == 1:
|
||||
message = _('Go, go, go! Congrats for your first deal.')
|
||||
elif query_result['max_team_30'] == lead.planned_revenue:
|
||||
message = _('Boom! Team record for the past 30 days.')
|
||||
elif query_result['max_team_7'] == lead.planned_revenue:
|
||||
message = _('Yeah! Deal of the last 7 days for the team.')
|
||||
elif query_result['max_user_30'] == lead.planned_revenue:
|
||||
message = _('You just beat your personal record for the past 30 days.')
|
||||
elif query_result['max_user_7'] == lead.planned_revenue:
|
||||
message = _('You just beat your personal record for the past 7 days.')
|
||||
|
||||
if message:
|
||||
return {
|
||||
'effect': {
|
||||
'fadeout': 'slow',
|
||||
'message': message,
|
||||
'img_url': '/web/image/%s/%s/image' % (lead.team_id.user_id._name, lead.team_id.user_id.id) if lead.team_id.user_id.image else '/web/static/src/img/smile.svg',
|
||||
'type': 'rainbow',
|
||||
}
|
||||
}
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_schedule_meeting(self):
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
<form string="Opportunities" class="o_opportunity_form">
|
||||
<header>
|
||||
<button name="action_set_won" string="Mark Won"
|
||||
type="object" class="oe_highlight o_wow"
|
||||
type="object" class="oe_highlight"
|
||||
attrs="{'invisible': ['|', ('active','=',False), ('probability', '=', 100)]}"/>
|
||||
<button name="%(crm.crm_lead_lost_action)d" string="Mark Lost"
|
||||
type="action" class="oe_highlight"
|
||||
|
||||
@@ -511,7 +511,7 @@ function on_mark_as_read_notification (data) {
|
||||
if (message) {
|
||||
invalidate_caches(message.channel_ids);
|
||||
remove_message_from_channel("channel_inbox", message);
|
||||
chat_manager.bus.trigger('update_message', message);
|
||||
chat_manager.bus.trigger('update_message', message, data.type);
|
||||
}
|
||||
});
|
||||
if (data.channel_ids) {
|
||||
|
||||
@@ -511,11 +511,19 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
});
|
||||
},
|
||||
|
||||
update_button_status: function (disabled) {
|
||||
update_button_status: function (disabled, type) {
|
||||
if (this.channel.id === "channel_inbox") {
|
||||
this.$buttons
|
||||
.find('.o_mail_chat_button_mark_read')
|
||||
.toggleClass('disabled', disabled);
|
||||
// Display Rainbowman when all inbox messages are read through
|
||||
// 'MARK ALL READ' or marking last inbox message as read
|
||||
if (disabled && type === 'mark_as_read') {
|
||||
this.trigger_up('show_effect', {
|
||||
message: _t('Congratulations, your inbox is empty!'),
|
||||
type: 'rainbow_man',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.channel.id === "channel_starred") {
|
||||
this.$buttons
|
||||
@@ -607,7 +615,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
// Dump scroll position of channels in which the new message arrived
|
||||
this.channels_scrolltop = _.omit(this.channels_scrolltop, message.channel_ids);
|
||||
},
|
||||
on_update_message: function (message) {
|
||||
on_update_message: function (message, type) {
|
||||
// To identify 'update_button_status' method is called from from 'Mark as read / Mark all read'
|
||||
// button, we are passing type to this method so that rainbowman will only appear on marking all
|
||||
// messages as read (because the same method is also called from 'fetch_and_render_thread' when
|
||||
// clicking @inbox, and we don't want rainbowman to appear everytime on empty inbox)
|
||||
var self = this;
|
||||
var current_channel_id = this.channel.id;
|
||||
if ((current_channel_id === "channel_starred" && !message.is_starred) ||
|
||||
@@ -615,7 +627,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
chat_manager.get_messages({channel_id: this.channel.id, domain: this.domain}).then(function (messages) {
|
||||
var options = self.get_thread_rendering_options(messages);
|
||||
self.thread.remove_message_and_render(message.id, messages, options).then(function () {
|
||||
self.update_button_status(messages.length === 0);
|
||||
self.update_button_status(messages.length === 0, type);
|
||||
});
|
||||
});
|
||||
} else if (_.contains(message.channel_ids, current_channel_id)) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 136 136"><title>face</title><path d="M68 134A66 66 0 1 0 2 68a66 66 0 0 0 66 66z" fill="#fbfcfd" stroke="#a1acba" stroke-width="4"/><path d="M109.4 19.2A64.7 64.7 0 0 1 132 68.5 63.8 63.8 0 0 1 68.5 132a64.7 64.7 0 0 1-49.3-22.6A64.8 64.8 0 0 0 61.5 125 63.8 63.8 0 0 0 125 61.5a64.8 64.8 0 0 0-15.6-42.3z" style="isolation:isolate" fill="#ecf1ff"/><path d="M49.1 62.1a10 10 0 0 0-14.1 0M101.1 62.1a10 10 0 0 0-14.1 0" fill="none" stroke="#a1acba" stroke-linecap="round" stroke-width="5"/><path d="M42 88a36.8 36.8 0 0 0 52 0" fill="none" stroke="#a1acba" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/></svg>
|
||||
|
After Width: | Height: | Size: 681 B |
@@ -21,6 +21,7 @@ var Dialog = require('web.Dialog');
|
||||
var Loading = require('web.Loading');
|
||||
var mixins = require('web.mixins');
|
||||
var NotificationManager = require('web.notification').NotificationManager;
|
||||
var RainbowMan = require('web.rainbow_man');
|
||||
var session = require('web.session');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
@@ -43,13 +44,13 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
// downstream widgets. Mainly side effects, such as rpcs, notifications
|
||||
// or cache.
|
||||
|
||||
// notifications and warnings
|
||||
// notifications, warnings and effects
|
||||
notification: function (e) {
|
||||
if(this.notification_manager) {
|
||||
this.notification_manager.notify(e.data.title, e.data.message, e.data.sticky);
|
||||
}
|
||||
},
|
||||
warning: '_displayWarning',
|
||||
warning: '_onDisplayWarning',
|
||||
load_views: function (event) {
|
||||
var params = {
|
||||
model: event.data.modelName,
|
||||
@@ -65,6 +66,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
.load_filters(event.data.dataset, event.data.action_id)
|
||||
.then(event.data.on_success);
|
||||
},
|
||||
show_effect: '_onShowEffect',
|
||||
// session
|
||||
get_session: function (event) {
|
||||
if (event.data.callback) {
|
||||
@@ -82,7 +84,6 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
}
|
||||
});
|
||||
},
|
||||
show_wow: '_onShowWow',
|
||||
},
|
||||
init: function (parent) {
|
||||
this.client_options = {};
|
||||
@@ -300,7 +301,8 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
|
||||
/**
|
||||
* Displays a warning in a dialog of with the NotificationManager
|
||||
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} e
|
||||
* @param {string} e.data.message the warning's message
|
||||
* @param {string} e.data.title the warning's title
|
||||
@@ -308,7 +310,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
* @param {boolean} [e.data.sticky] whether or not the warning should be
|
||||
* sticky (if displayed with the NotificationManager)
|
||||
*/
|
||||
_displayWarning: function (e) {
|
||||
_onDisplayWarning: function (e) {
|
||||
var data = e.data;
|
||||
if (data.type === 'dialog') {
|
||||
new Dialog(this, {
|
||||
@@ -321,20 +323,21 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Displays a thumb up, heart or peace image (randomly) for a moment (e.g.
|
||||
* used when an opportunity is won)
|
||||
* Displays a visual effect (for example, a rainbowman0
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} e
|
||||
* @param {Object} [e.data] - key-value options to decide rainbowman
|
||||
* behavior / appearance
|
||||
*/
|
||||
_onShowWow: function () {
|
||||
var className = 'o_wow_thumbs';
|
||||
if (Math.random() > 0.9) {
|
||||
var otherClasses = ['o_wow_peace', 'o_wow_heart'];
|
||||
className = otherClasses[Math.floor(Math.random()*otherClasses.length)];
|
||||
_onShowEffect: function (e) {
|
||||
var data = e.data || {};
|
||||
var type = data.type || 'rainbow_man';
|
||||
if (type === 'rainbow_man') {
|
||||
new RainbowMan(data).appendTo(this.$el);
|
||||
} else {
|
||||
throw new Error('Unknown effect type: ' + type);
|
||||
}
|
||||
var $body = $('body');
|
||||
$body.addClass(className);
|
||||
setTimeout($body.removeClass.bind($body, className), 1000);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -862,6 +862,11 @@ var ActionManager = Widget.extend({
|
||||
options.on_close();
|
||||
}
|
||||
this.dialog_stop();
|
||||
// Display rainbowman on appropriate actions
|
||||
if (action.effect) {
|
||||
this.trigger_up('show_effect', action.effect);
|
||||
}
|
||||
|
||||
return $.when();
|
||||
},
|
||||
ir_actions_server: function (action, options) {
|
||||
|
||||
@@ -550,6 +550,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
* @param {String} [action_data.special=null] special action handlers, closes the dialog if set
|
||||
* @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
|
||||
* @param {Object} [action_data.context=null] additional action context, to add to the current context
|
||||
* @param {string} [action_data.effect] if given, a visual effect (a
|
||||
* rainbowman by default) will be displayed when the action is complete,
|
||||
* with the string (evaluated) given as options.
|
||||
* @param {DataSet} dataset a dataset object used to communicate with the server
|
||||
* @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set)
|
||||
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
|
||||
@@ -562,6 +565,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
|
||||
// response handler
|
||||
var handler = function (action) {
|
||||
// show effect if button have effect attribute
|
||||
// Rainbowman can be displayed from two places : from attribute on a button, or from python.
|
||||
// Code below handles the first case i.e 'effect' attribute on button.
|
||||
var effect = false;
|
||||
if (action_data.effect) {
|
||||
effect = pyeval.py_eval(action_data.effect);
|
||||
};
|
||||
|
||||
if (action && action.constructor === Object) {
|
||||
// filter out context keys that are specific to the current action.
|
||||
// Wrong default_* and search_default_* values will no give the expected result
|
||||
@@ -580,11 +591,16 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
}
|
||||
ncontext.add(action.context || {});
|
||||
action.context = ncontext;
|
||||
// In case effect data is returned from python and also there is rainbow
|
||||
// attribute on button, priority is given to button attribute
|
||||
action.effect = effect || action.effect;
|
||||
return self.do_action(action, {
|
||||
on_close: result_handler,
|
||||
});
|
||||
} else {
|
||||
self.do_action({"type":"ir.actions.act_window_close"});
|
||||
// If action doesn't return anything, but have effect
|
||||
// attribute on button, display rainbowman
|
||||
self.do_action({"type":"ir.actions.act_window_close", 'effect': effect});
|
||||
return result_handler();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -244,7 +244,7 @@ var FormController = BasicController.extend({
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_disableButtons: function () {
|
||||
_disableButtons: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.renderer.disableButtons();
|
||||
},
|
||||
@@ -254,7 +254,7 @@ var FormController = BasicController.extend({
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_enableButtons: function () {
|
||||
_enableButtons: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.renderer.enableButtons();
|
||||
},
|
||||
@@ -380,10 +380,6 @@ var FormController = BasicController.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (event.data.showWow) {
|
||||
def.then(this.trigger_up.bind(this, 'show_wow'));
|
||||
}
|
||||
|
||||
def.always(this._enableButtons.bind(this));
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -117,14 +117,14 @@ var FormRenderer = BasicRenderer.extend({
|
||||
* Disable stat buttons so that they can't be clicked anymore
|
||||
*
|
||||
*/
|
||||
disableButtons: function() {
|
||||
disableButtons: function() {
|
||||
this.$('.oe_button_box button').attr('disabled', true);
|
||||
},
|
||||
/**
|
||||
* Enable stat buttons so they can be clicked again
|
||||
*
|
||||
*/
|
||||
enableButtons: function() {
|
||||
enableButtons: function() {
|
||||
this.$('.oe_button_box button').removeAttr('disabled');
|
||||
},
|
||||
/**
|
||||
@@ -206,7 +206,6 @@ var FormRenderer = BasicRenderer.extend({
|
||||
self.trigger_up('button_clicked', {
|
||||
attrs: node.attrs,
|
||||
record: self.state,
|
||||
showWow: $el.hasClass('o_wow'),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
odoo.define('web.rainbow_man', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require("web.ajax");
|
||||
var Widget = require('web.Widget');
|
||||
var core = require('web.core');
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var RainbowMan = Widget.extend({
|
||||
template: 'rainbow_man.notification',
|
||||
/**
|
||||
* @override
|
||||
* @constructor
|
||||
* @param {Object} [options] - key-value options to decide rainbowman behavior / appearance
|
||||
* @param {string} [options.message] - Message to be displayed on rainbowman card
|
||||
* @param {string} [options.fadeout]
|
||||
* Delay for rainbowman to disappear - [options.fadeout='fast'] will make rainbowman
|
||||
* dissapear quickly, [options.fadeout='medium'] and [options.fadeout='slow'] will
|
||||
* wait little longer before disappearing (can be used when [options.message]
|
||||
* is longer), [options.fadeout='no'] will keep rainbowman on screen until
|
||||
* user clicks anywhere outside rainbowman
|
||||
* @param {string} [options.img_url] - URL of the image to be displayed
|
||||
* @param {Boolean} [options.click_close] - If true, destroys rainbowman on click outside
|
||||
*/
|
||||
init: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
var rainbowDelay = {slow: 4500, medium: 3500, fast:2000, no: false };
|
||||
this.options = _.defaults(options || {}, {
|
||||
fadeout: 'medium',
|
||||
img_url: '/web/static/src/img/smile.svg',
|
||||
message: _t('Well Done!'),
|
||||
click_close: true,
|
||||
});
|
||||
this.delay = rainbowDelay[this.options.fadeout];
|
||||
core.bus.on('clear_uncommitted_changes', this, this.destroy);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
willStart: function () {
|
||||
var def;
|
||||
if (!qweb.has_template(this.template)) {
|
||||
// we need to manually load the templates when the rainbon_man is used in
|
||||
// the frontend, for example, it may be displayed at the end of a tour.
|
||||
def = ajax.loadXML("/web/static/src/xml/rainbow_man.xml", qweb);
|
||||
}
|
||||
return $.when(this._super.apply(this, arguments), def);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
if (this.options.click_close) {
|
||||
core.bus.on('click', this, function (ev) {
|
||||
if (ev.originalEvent && ev.target.className.indexOf('o_reward') === -1) {
|
||||
this.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.delay) {
|
||||
setTimeout(function () {
|
||||
self.$el.addClass('o_reward_fading');
|
||||
setTimeout(function () {
|
||||
self.destroy();
|
||||
}, 600); // destroy only after fadeout animation is completed
|
||||
}, this.delay);
|
||||
}
|
||||
this.$('.o_reward_msg_content').append(this.options.message);
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
return RainbowMan;
|
||||
|
||||
});
|
||||
@@ -160,17 +160,3 @@
|
||||
font-size: 30vw;
|
||||
}
|
||||
}
|
||||
|
||||
/************** USAGE of ANIMATIONS **************/
|
||||
// Animation on wow button
|
||||
// TODO the only difference here is the "content" property, just make an
|
||||
// 'o_wow' class for the other properties to avoid generating useless CSS
|
||||
.o_wow_thumbs:after {
|
||||
.o-display-marked("\f164", 20%, 40%, @odoo-brand-optional);
|
||||
}
|
||||
.o_wow_peace:after {
|
||||
.o-display-marked("\f118", 20%, 40%, @odoo-brand-optional);
|
||||
}
|
||||
.o_wow_heart:after {
|
||||
.o-display-marked("\f004", 20%, 40%, @odoo-brand-optional);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/************** rainbow *****************/
|
||||
|
||||
// Variables
|
||||
@reward-size: 400px;
|
||||
@reward-text-color: #727880;
|
||||
@reward-base-time: 1.4s;
|
||||
|
||||
// o_reward
|
||||
.o_reward {
|
||||
will-change: transform;
|
||||
z-index: @zindex-modal + 1;
|
||||
padding: 50px;
|
||||
margin: -5% auto 0 -(@reward-size / 2);
|
||||
background-image: -webkit-radial-gradient(#EDEFF4 30%, transparent 70%, transparent);
|
||||
background-image: -o-radial-gradient(#EDEFF4 30%, transparent 70%, transparent);
|
||||
background-image: radial-gradient(#EDEFF4 30%, transparent 70%, transparent);
|
||||
|
||||
.o-animation(reward-fading, @reward-base-time * 0.5, ease-in-out, 0s, 1, normal, forwards);
|
||||
.o-position-absolute(20%, auto, auto, 50%);
|
||||
.size(@reward-size, @reward-size);
|
||||
|
||||
&.o_reward_fading {
|
||||
display: block;
|
||||
.o-animation(reward-fading-reverse, (@reward-base-time * 0.4), ease-in-out, 0s, 1, normal, forwards);
|
||||
|
||||
.o_reward_face_group {
|
||||
.o-animation(reward-jump-reverse, (@reward-base-time * 0.4), ease-in-out, 0s, 1, normal, forwards);
|
||||
}
|
||||
|
||||
.o_reward_rainbow {
|
||||
path {
|
||||
.o-animation(reward-rainbow-reverse, (@reward-base-time * 0.5), ease-out, 0s, 1, normal, forwards);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_reward_face, .o_reward_stars, .o_reward_shadow, .o_reward_thumbup {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.o_reward_rainbow {
|
||||
path {
|
||||
stroke-dasharray: 600;
|
||||
stroke-dashoffset: 0;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 21px;
|
||||
.o-animation(reward-rainbow, (@reward-base-time ), ease-out, 0s, 1, normal, forwards);
|
||||
}
|
||||
}
|
||||
|
||||
.o_reward_face_group {
|
||||
transform-origin: center;
|
||||
.o-animation(reward-jump, @reward-base-time * 0.8, ease-in-out);
|
||||
.o-position-absolute(6%, 0, 0, 0);
|
||||
.size(100%, 60%);
|
||||
}
|
||||
|
||||
.o_reward_face {
|
||||
display: block;
|
||||
top: 42%;
|
||||
position: relative;
|
||||
border-radius: 100%;
|
||||
background: center center / contain no-repeat;
|
||||
.o-animation(reward-float, @reward-base-time, ease-in-out, @reward-base-time, infinite, alternate);
|
||||
.size(34%, 56.67%);
|
||||
}
|
||||
|
||||
.o_reward_stars {
|
||||
display: block;
|
||||
.size(@reward-size * 0.75, @reward-size / 2);
|
||||
.o-position-absolute(18%, 7%);
|
||||
|
||||
svg {
|
||||
transform-origin: center center;
|
||||
.o-position-absolute(28%, @left:3%);
|
||||
.o-animation(reward-stars, @reward-base-time, ease-in-out, 0s, infinite, alternate-reverse);
|
||||
|
||||
&.star2, &.star4 {
|
||||
.o-animation(reward-stars, (@reward-base-time*1.2), ease-in-out, 0s, infinite, alternate);
|
||||
}
|
||||
|
||||
&.star2 {
|
||||
left: 20%;
|
||||
top: 2%;
|
||||
}
|
||||
|
||||
&.star3 {
|
||||
left: 49%;
|
||||
top: 6%;
|
||||
}
|
||||
|
||||
&.star4 {
|
||||
left: 70%;
|
||||
top: 27%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.o_reward_thumbup {
|
||||
width: 40px;
|
||||
display: block;
|
||||
.o-animation(reward-scale, @reward-base-time * 0.5, ease-in-out, 0s, infinite, alternate);
|
||||
.o-position-absolute(63%, auto, auto, 65%);
|
||||
}
|
||||
|
||||
.o_reward_msg_container {
|
||||
will-change: transform;
|
||||
padding-top: 11%;
|
||||
width: 70%;
|
||||
margin-left: 15%;
|
||||
|
||||
// Translate before animate
|
||||
.o-transform(translateY(5px));
|
||||
|
||||
.o-animation(reward-float, @reward-base-time, ease-in-out, @reward-base-time, infinite, alternate-reverse);
|
||||
.o-position-absolute(85%, auto, auto, 0%);
|
||||
|
||||
.o_reward_thumb_right {
|
||||
height: 40px;
|
||||
z-index: 1;
|
||||
.o-position-absolute(0, auto, auto, 16%);
|
||||
}
|
||||
|
||||
.o_reward_msg {
|
||||
margin-left: 7%;
|
||||
margin-top: -9.5%;
|
||||
padding: 25px 15px 20px;
|
||||
background: white;
|
||||
border: 1px solid #ecf1ff;
|
||||
border-top-width: 0;
|
||||
display: inline-block;
|
||||
|
||||
// Reset margins for first and penultimate childs (the last one is shadow)
|
||||
*:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
*:nth-last-child(2) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_reward_msg_content {
|
||||
position: relative;
|
||||
font-family: sans-serif;
|
||||
text-align: left;
|
||||
color: @reward-text-color;
|
||||
}
|
||||
|
||||
.o_reward_shadow_container {
|
||||
.o-transform(~"translateY(0px) rotateZ(0)");
|
||||
.o-animation(reward-float, @reward-base-time, ease-in-out, @reward-base-time, infinite, alternate);
|
||||
}
|
||||
|
||||
.o_reward_shadow {
|
||||
.size(100%, 12px);
|
||||
background-color: #e7eaf0;
|
||||
border-radius: 100%;
|
||||
.o-transform(~"scale(0.8) rotateZ(0)");
|
||||
.o-animation(reward-scale, @reward-base-time, ease-in-out, @reward-base-time, infinite, alternate);
|
||||
.o-position-absolute(auto, auto, -40px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-fading {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-fading-reverse {
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-jump {
|
||||
0% {
|
||||
.o-transform(~"scale(0.5)");
|
||||
}
|
||||
50% {
|
||||
.o-transform(~"scale(1.05)");
|
||||
}
|
||||
to {
|
||||
.o-transform(~"scale(1)");
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-jump-reverse {
|
||||
0% {
|
||||
.o-transform(~"scale(1)");
|
||||
}
|
||||
50% {
|
||||
.o-transform(~"scale(1.05)");
|
||||
}
|
||||
to {
|
||||
.o-transform(~"scale(0.5)");
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-rainbow {
|
||||
0% {
|
||||
stroke-dashoffset: -500;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-rainbow-reverse {
|
||||
to {
|
||||
stroke-dashoffset: -500;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-float {
|
||||
from {
|
||||
.o-transform(~"translateY(0px)");
|
||||
}
|
||||
to {
|
||||
.o-transform(~"translateY(5px)");
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-stars {
|
||||
from {
|
||||
.o-transform(~"scale(0.3) rotate(0deg)");
|
||||
}
|
||||
50% {
|
||||
.o-transform(~"scale(1.0) rotate(20deg)");
|
||||
}
|
||||
to {
|
||||
.o-transform(~"scale(0.3) rotate(80deg)");
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reward-scale {
|
||||
from {
|
||||
.o-transform(~"scale(0.8)");
|
||||
}
|
||||
to {
|
||||
.o-transform(~"scale(1.0)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="rainbow_man.notification">
|
||||
<div class="o_reward">
|
||||
<svg class="o_reward_rainbow" viewBox="0 0 340 180">
|
||||
<path d="M270,170a100,100,0,0,0-200,0" style="stroke:#FF9E80;"/>
|
||||
<path d="M290,170a120,120,0,0,0-240,0" style="stroke:#FFE57F;"/>
|
||||
<path d="M310,170a140,140,0,0,0-280,0" style="stroke:#80D8FF;"/>
|
||||
<path d="M330,170a160,160,0,0,0-320,0" style="stroke:#c794ba;"/>
|
||||
</svg>
|
||||
<div class="o_reward_stars" t-foreach="[1, 2, 3, 4]" t-as="star">
|
||||
<t t-call="rainbow_man.star">
|
||||
<t t-set="class" t-value="star"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_reward_face_group">
|
||||
<svg style="display:none">
|
||||
<symbol id="thumb">
|
||||
<path d="M10,52 C6,51 3,48 3,44 C2,42 3,39 5,38 C3,36 2,34 2,32 C2,29 3,27 5,26 C3,24 2,21 2,19 C2,15 7,12 10,12 L23,12 C23,11 23,11 23,11 L23,10 C23,8 24,6 25,4 C27,2 29,2 31,2 C33,2 35,2 36,4 C38,5 39,7 39,10 L39,38 C39,41 37,45 35,47 C32,50 28,51 25,52 L10,52 L10,52 Z" fill="#FBFBFC"/>
|
||||
<polygon fill="#ECF1FF" points="25 11 25 51 5 52 5 12"/>
|
||||
<path d="M31,0 C28,0 26,1 24,3 C22,5 21,7 21,10 L10,10 C8,10 6,11 4,12 C2,14 1,16 1,19 C1,21 1,24 2,26 C1,27 1,29 1,32 C1,34 1,36 2,38 C1,40 0,42 1,45 C1,50 5,53 10,54 L25,54 C29,54 33,52 36,49 C39,46 41,42 41,38 L41,10 C41,4 36,3.38176876e-16 31,0 M31,4 C34,4 37,6 37,10 L37,38 C37,41 35,44 33,46 C31,48 28,49 25,50 L10,50 C7,49 5,47 5,44 C4,41 6,38 9,37 L9,37 C6,37 5,35 5,32 C5,28 6,26 9,26 L9,26 C6,26 5,22 5,19 C5,16 8,14 11,14 L23,14 C24,14 25,12 25,11 L25,10 C25,7 28,4 31,4" id="Shape" fill="#A1ACBA"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<span class="o_reward_face" t-attf-style="background-image:url(#{widget.options.img_url});"/>
|
||||
|
||||
<svg class="o_reward_thumbup" viewBox="0 0 42 60">
|
||||
<use href="#thumb"/>
|
||||
</svg>
|
||||
|
||||
<div class="o_reward_msg_container">
|
||||
<svg class="o_reward_thumb_right" viewBox="0 0 100 55">
|
||||
<use href="#thumb" transform="scale(-1, 1.2) rotate(-90) translate(-42,-60)"/>
|
||||
</svg>
|
||||
<div class="o_reward_msg">
|
||||
<div class="o_reward_msg_card">
|
||||
<div class="o_reward_msg_content"/>
|
||||
<div class="o_reward_shadow_container">
|
||||
<div class="o_reward_shadow"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="rainbow_man.star">
|
||||
<svg t-attf-class="star{{ star }}" width="35px" height="34px" viewBox="0 0 35 34">
|
||||
<path fill="#fff" d="M33,15.9 C26.3557814,13.6951256 21.1575294,8.45974313 19,1.8 C19,1.24771525 18.5522847,0.8 18,0.8 C17.4477153,0.8 17,1.24771525 17,1.8 C14.6431206,8.69377078 9.02624222,13.9736364 2,15.9 C1.36487254,15.9 0.85,16.4148725 0.85,17.05 C0.85,17.6851275 1.36487254,18.2 2,18.2 C8.6215326,20.3844521 13.8155479,25.5784674 16,32.2 C16,32.7522847 16.4477153,33.2 17,33.2 C17.5522847,33.2 18,32.7522847 18,32.2 C20.3568794,25.3062292 25.9737578,20.0263636 33,18.1 C33.6351275,18.1 34.15,17.5851275 34.15,16.95 C34.15,16.3148725 33.6351275,15.8 33,15.8"></path>
|
||||
</svg>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -4,6 +4,8 @@ odoo.define('web.form_tests', function (require) {
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var FormView = require('web.FormView');
|
||||
var pyeval = require('web.pyeval');
|
||||
var RainbowMan = require('web.rainbow_man');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var _t = core._t;
|
||||
@@ -5200,42 +5202,6 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('wow effect', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.fields.model_name = { string: "Model name", type: "char" };
|
||||
|
||||
var clickCount = 0;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<button name="test" string="Mark Won" type="object" class="o_wow"/>' +
|
||||
'</form>',
|
||||
intercepts: {
|
||||
execute_action: function (event) {
|
||||
if (clickCount === 0) {
|
||||
event.data.on_success();
|
||||
} else {
|
||||
event.data.on_fail();
|
||||
}
|
||||
clickCount++;
|
||||
},
|
||||
show_wow: function () {
|
||||
assert.step('wow');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
form.$('button.o_wow').click();
|
||||
assert.verifySteps(['wow']);
|
||||
form.$('button.o_wow').click();
|
||||
assert.verifySteps(['wow']);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('readonly fields are not sent when saving', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
@@ -5377,5 +5343,30 @@ QUnit.module('Views', {
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('rainbowman attributes correctly passed on button click', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<header>' +
|
||||
'<button name="action_won" string="Won" type="object" effect="{\'message\': \'Congrats!\'}"/>' +
|
||||
'</header>' +
|
||||
'</form>',
|
||||
intercepts: {
|
||||
execute_action: function (event) {
|
||||
var effectDescription = pyeval.py_eval(event.data.action_data.effect);
|
||||
assert.deepEqual(effectDescription, {message: 'Congrats!'}, "should have correct effect description");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
form.$('.o_form_statusbar .btn-default').click();
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
odoo.define('web.rainbow_man_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var RainbowMan = require('web.rainbow_man');
|
||||
|
||||
QUnit.module('widgets', {}, function () {
|
||||
|
||||
QUnit.module('RainbowMan', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
message: 'Congrats!',
|
||||
click_close: false
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.test("rendering a rainbowman", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var $target = $("#qunit-fixture");
|
||||
|
||||
// Create and display rainbowman
|
||||
var rainbowman = new RainbowMan(this.data);
|
||||
rainbowman.appendTo($target);
|
||||
|
||||
var $rainbow = rainbowman.$(".o_reward_rainbow");
|
||||
assert.strictEqual($rainbow.length, 1,
|
||||
"Should have displayed rainbow effect");
|
||||
|
||||
assert.ok(rainbowman.$('.o_reward_msg_content').html() === 'Congrats!',
|
||||
"Card on the rainbowman should display 'Congrats!' message");
|
||||
|
||||
rainbowman.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/lib/bootstrap/less/variables.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/lib/bootstrap/less/mixins/vendor-prefixes.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/lib/bootstrap/less/mixins/buttons.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/lib/bootstrap/less/mixins/size.less"/>
|
||||
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/variables.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/utils.less"/>
|
||||
@@ -23,6 +24,7 @@
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/navbar.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/mimetypes.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/animation.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/rainbow.less"/>
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/less/datepicker.less"/>
|
||||
|
||||
<script type="text/javascript" src="/web/static/lib/es5-shim/es5-shim.min.js"></script>
|
||||
@@ -84,6 +86,7 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/core/dialog.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/local_storage.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/services/local_storage_service.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/rainbow_man.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="web.assets_backend" name="Backend Assets (used in backend interface)">
|
||||
@@ -468,6 +471,7 @@
|
||||
<script type="text/javascript" src="/web/static/tests/widgets/data_export_tests.js"/>
|
||||
<script type="text/javascript" src="/web/static/tests/widgets/domain_selector_tests.js"/>
|
||||
<script type="text/javascript" src="/web/static/tests/widgets/model_field_selector_tests.js"/>
|
||||
<script type="text/javascript" src="/web/static/tests/widgets/rainbow_man_tests.js"/>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/boot_tests.js"></script>
|
||||
</t>
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web_tour.TourManager', function(require) {
|
||||
var core = require('web.core');
|
||||
var local_storage = require('web.local_storage');
|
||||
var mixins = require('web.mixins');
|
||||
var RainbowMan = require('web.rainbow_man');
|
||||
var ServicesMixin = require('web.ServicesMixin');
|
||||
var session = require('web.session');
|
||||
var Tip = require('web_tour.Tip');
|
||||
@@ -403,6 +404,14 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
},
|
||||
_consume_tour: function (tour_name, error) {
|
||||
delete this.active_tooltips[tour_name];
|
||||
//display rainbow at the end of any tour
|
||||
if (this.tours[tour_name].current_step === this.tours[tour_name].steps.length){
|
||||
var $rainbow_message = $('<strong>' +
|
||||
'<b>Good job!</b>' +
|
||||
' You went through all steps of this tour.' +
|
||||
'</strong>');
|
||||
new RainbowMan({message: $rainbow_message, click_close: false}).appendTo(this.$body);
|
||||
};
|
||||
this.tours[tour_name].current_step = 0;
|
||||
local_storage.removeItem(get_step_key(tour_name));
|
||||
if (this.running_tour === tour_name) {
|
||||
|
||||
Reference in New Issue
Block a user