[ADD] web_tip: module for tip definition and display
authorJulien Legros <jle@odoo.com>
Fri, 25 Jul 2014 16:12:36 +0000 (18:12 +0200)
committerJulien Legros <jle@odoo.com>
Wed, 6 Aug 2014 08:51:41 +0000 (10:51 +0200)
Also added with this commit:
crm: tip data
web: hooks (events) for web_tip
web_kanban: hooks (events) for web_tip

13 files changed:
addons/crm/__openerp__.py
addons/crm/crm_tip_data.xml [new file with mode: 0644]
addons/web/static/src/js/view_form.js
addons/web/static/src/js/views.js
addons/web_kanban/static/src/js/kanban.js
addons/web_tip/__init__.py [new file with mode: 0644]
addons/web_tip/__openerp__.py [new file with mode: 0644]
addons/web_tip/security/ir.model.access.csv [new file with mode: 0644]
addons/web_tip/static/src/css/tip.css [new file with mode: 0644]
addons/web_tip/static/src/js/tip.js [new file with mode: 0644]
addons/web_tip/views/tip.xml [new file with mode: 0644]
addons/web_tip/web_tip.py [new file with mode: 0644]
addons/web_tip/web_tip_view.xml [new file with mode: 0644]

index 15544dc..e83b8c9 100644 (file)
@@ -63,6 +63,7 @@ Dashboard for CRM will include:
         'crm_data.xml',
         'crm_lead_data.xml',
         'crm_phonecall_data.xml',
+        'crm_tip_data.xml',
 
         'security/crm_security.xml',
         'security/ir.model.access.csv',
diff --git a/addons/crm/crm_tip_data.xml b/addons/crm/crm_tip_data.xml
new file mode 100644 (file)
index 0000000..7ddeb54
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+    <data>
+        <record model="web.tip" id="crm_tip_1">
+            <field name="title">Import from LinkedIn</field>
+            <field name="description">Import your contacts directly from LinkedIn. It is much easier than creating them manually</field>
+            <field name="action_id" ref="base.action_partner_form"/>
+            <field name="model">res.partner</field>
+            <field name="mode">kanban</field>
+            <!-- TODO Add relevant selectors when linkedin button is implemented -->
+            <field name="trigger_selector"></field>
+            <field name="highlight_selector"></field>
+            <field name="placement">bottom</field>
+        </record>
+
+        <record model="web.tip" id="crm_tip_2">
+            <field name="title"></field>
+            <field name="description">Switch to the opportunities pipeline for this contact. Here you can access important related documents for this customer</field>
+            <field name="action_id" ref="base.action_partner_form"/>
+            <field name="model">res.partner</field>
+            <field name="mode">form</field>
+            <field name="trigger_selector">.oe_form_buttons_view:visible,div.oe_right.oe_button_box > button</field>
+            <field name="highlight_selector">div.oe_right.oe_button_box:visible > button:nth-child(1)</field>
+            <field name="placement">bottom</field>
+        </record>
+
+        <record model="web.tip" id="crm_tip_3">
+            <field name="title"></field>
+            <field name="description">Switch back to the pipeline view. In one screen, view all your opportunities with the expected revenue for each stage.</field>
+            <field name="model">crm.lead</field>
+            <field name="type">opportunity</field>
+            <field name="mode">form</field>
+            <field name="trigger_selector">.oe_form_buttons_view:visible,.oe_breadcrumb_title > a.oe_breadcrumb_item</field>
+            <field name="highlight_selector">.oe_breadcrumb_title > a.oe_breadcrumb_item:last</field>
+            <field name="placement">bottom</field>
+        </record>
+
+        <record model="web.tip" id="crm_tip_4">
+            <field name="title"></field>
+            <field name="description"><![CDATA[<b>Drag and drop</b>]]> your opportunity to the next stage. Our Sales Planner tool can help you define your pipeline stages.</field>
+            <field name="model">crm.lead</field>
+            <field name="mode">kanban</field>
+            <field name="trigger_selector">.oe_kanban_record:visible</field>
+            <field name="highlight_selector">table.oe_kanban_groups:last</field>
+            <field name="end_selector">.oe_kanban_record</field>
+            <field name="end_event">mouseup</field>
+            <field name="placement">auto top</field>
+        </record>
+    </data>
+</openerp>
index 66e5005..95c6520 100644 (file)
@@ -302,6 +302,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
                 opacity: '1',
                 filter: 'alpha(opacity = 100)'
             });
+            instance.web.bus.trigger('form_view_shown', self);
         });
     },
     do_hide: function () {
@@ -705,6 +706,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
                 if (menu) {
                     menu.do_reload_needaction();
                 }
+                instance.web.bus.trigger('form_view_saved', self);
             });
         }).always(function(){
             $(e.target).attr("disabled", false);
index ac51a34..93ca3fe 100644 (file)
@@ -344,6 +344,8 @@ instance.web.ActionManager = instance.web.Widget.extend({
             });
         }
 
+        instance.web.bus.trigger('action', action);
+
         // Ensure context & domain are evaluated and can be manipulated/used
         var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
         action.context = instance.web.pyeval.eval('context', ncontext);
@@ -725,6 +727,7 @@ instance.web.ViewManager =  instance.web.Widget.extend({
                 }
                 views.push(mode);
             }
+            instance.web.bus.trigger('view_switch_mode', self, mode);
         });
         var item = _.extend({
             widget: this,
@@ -1481,6 +1484,7 @@ instance.web.View = instance.web.Widget.extend({
     },
     do_show: function () {
         this.$el.show();
+        instance.web.bus.trigger('view_shown', this);
     },
     do_hide: function () {
         this.$el.hide();
index e6a4b0d..5395434 100644 (file)
@@ -296,6 +296,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
                 if(!self.nb_records) {
                     self.no_result();
                 }
+                self.trigger('kanban_groups_processed');
             });
         });
     },
@@ -312,6 +313,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
                     if (_.isEmpty(records)) {
                         self.no_result();
                     }
+                    self.trigger('kanban_dataset_processed');
                     def.resolve();
                 });
             }).done(null, function() {
diff --git a/addons/web_tip/__init__.py b/addons/web_tip/__init__.py
new file mode 100644 (file)
index 0000000..f8f6f9d
--- /dev/null
@@ -0,0 +1 @@
+import web_tip
diff --git a/addons/web_tip/__openerp__.py b/addons/web_tip/__openerp__.py
new file mode 100644 (file)
index 0000000..4ccc8e6
--- /dev/null
@@ -0,0 +1,37 @@
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2013 OpenERP SA (<http://openerp.com>).
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+{
+    'name': 'Tips',
+    'category': 'Usability',
+    'description': """
+OpenERP Web tips.
+========================
+
+""",
+    'version': '0.1',
+    'author': 'OpenERP SA',
+    'depends': ['web'],
+    'data': [
+        'security/ir.model.access.csv',
+        'views/tip.xml',
+        'web_tip_view.xml'
+    ],
+    'auto_install': True
+}
diff --git a/addons/web_tip/security/ir.model.access.csv b/addons/web_tip/security/ir.model.access.csv
new file mode 100644 (file)
index 0000000..b042f3a
--- /dev/null
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_web_tip,access_web_tip,model_web_tip,,1,0,0,0
diff --git a/addons/web_tip/static/src/css/tip.css b/addons/web_tip/static/src/css/tip.css
new file mode 100644 (file)
index 0000000..089b713
--- /dev/null
@@ -0,0 +1,56 @@
+.oe_tip_overlay {
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    position: absolute;
+    opacity: 0.8;
+    z-index: 1001;
+    background-color: #000;
+    background: -moz-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
+    background: -webkit-gradient(radial,center center,0px,center center,100%,color-stop(0%,rgba(0,0,0,0.4)),color-stop(100%,rgba(0,0,0,0.9)));
+    background: -webkit-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
+    background: -o-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
+    background: -ms-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
+    background: radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#66000000',endColorstr='#e6000000',GradientType=1);
+    -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+    filter: alpha(opacity=50);
+    -webkit-transition: all 0.3s ease-out;
+       -moz-transition: all 0.3s ease-out;
+        -ms-transition: all 0.3s ease-out;
+         -o-transition: all 0.3s ease-out;
+            transition: all 0.3s ease-out;
+}
+
+.oe_tip_helper {
+    position: absolute;
+    z-index: 1008;
+    background-color: #FFF;
+    background-color: rgba(255,255,255,.9);
+    border: 1px solid #777;
+    border: 1px solid rgba(0,0,0,.5);
+    border-radius: 4px;
+    box-shadow: 0 2px 15px rgba(0,0,0,.4);
+    -webkit-transition: all 0.3s ease-out;
+       -moz-transition: all 0.3s ease-out;
+        -ms-transition: all 0.3s ease-out;
+         -o-transition: all 0.3s ease-out;
+            transition: all 0.3s ease-out;
+}
+
+.oe_tip_close {
+    padding: 5px 5px 10px 10px !important;
+    margin-top: -15px;
+    margin-right: -15px;
+}
+
+.oe_tip_fix_parent {
+    z-index: auto !important;
+    opacity: 1.0 !important;
+}
+
+.oe_tip_show_element {
+    z-index: 1011 !important;
+    position: relative;
+}
diff --git a/addons/web_tip/static/src/js/tip.js b/addons/web_tip/static/src/js/tip.js
new file mode 100644 (file)
index 0000000..b91e89f
--- /dev/null
@@ -0,0 +1,250 @@
+(function() {
+
+    var instance = openerp;
+
+    instance.web.Tip = instance.web.Class.extend({
+        init: function() {
+            var self = this;
+            self.tips = [];
+            self.tip_mutex = new $.Mutex();
+            self.$overlay = null;
+            self.$element = null;
+
+            var Tips = new instance.web.Model('web.tip');
+            Tips.query(['title', 'description', 'action_id', 'model', 'type', 'mode', 'trigger_selector',
+                'highlight_selector', 'end_selector', 'end_event', 'placement', 'is_consumed'])
+                .all().then(function(tips) {
+                    self.tips = tips;
+                })
+            ;
+
+            instance.web.bus.on('action', this, function(action) {
+                self.on_action(action);
+            });
+
+            instance.web.bus.on('view_shown', this, function(view) {
+                if (_.keys(view.fields_view).length === 0) {
+                    view.on('view_loaded', this, function(fields_view) {
+                        self.on_view(view);
+                    });
+                } else {
+                    self.on_view(view);
+                }
+
+                view.on('switch_mode', this, function() {
+                });
+            });
+
+            instance.web.bus.on('view_switch_mode', this, function(viewManager, mode) {
+                self.on_switch(viewManager, mode);
+            });
+
+            instance.web.bus.on('form_view_shown', this, function(formView) {
+                self.on_form_view(formView);
+            });
+
+            instance.web.bus.on('form_view_saved', this, function(formView) {
+                self.on_form_view(formView);
+            });
+        },
+
+        // stub
+        on_action: function(action) {
+            var self = this;
+            var action_id = action.id;
+            var model = action.res_model;
+        },
+
+        on_view: function(view) {
+            var self = this;
+            var fields_view = view.fields_view;
+            var action_id = view.ViewManager.action ? view.ViewManager.action.id : null;
+            var model = fields_view.model;
+
+            // kanban
+            if(fields_view.type === 'kanban') {
+                var dataset_def = $.Deferred();
+                var groups_def = $.Deferred();
+                view.on("kanban_dataset_processed", self, function() {
+                    var length = view.dataset.ids.length;
+                    dataset_def.resolve(length);
+                });
+                view.on('kanban_groups_processed', self, function() {
+                    groups_def.resolve();
+                });
+                dataset_def.done(function(length) {
+                    self.eval_tip(action_id, model, fields_view.type);
+                });
+                groups_def.done(function() {
+                    self.eval_tip(action_id, model, fields_view.type);
+                });
+            }
+        },
+
+        on_form_view: function(formView) {
+            var self = this;
+            var model = formView.model;
+            var type = formView.datarecord.type ? formView.datarecord.type : null;
+            var mode = 'form';
+            self.eval_tip(null, model, mode, type);
+        },
+
+        // stub
+        on_switch: function (viewManager, mode) {
+            var self = this;
+            var action = viewManager.action;
+            var action_id = action.id;
+            var model = action.res_model;
+        },
+
+        eval_tip: function(action_id, model, mode, type) {
+            var self = this;
+            var filter = {};
+            var valid_tips = [];
+            var tips = [];
+            if (action_id) {
+                valid_tips = _.filter(self.tips, function (tip) {
+                    return tip.action_id[0] === action_id;
+                });
+            }
+
+            filter.model = model;
+            filter.mode = mode;
+            tips = _.where(self.tips, filter);
+            if (type) {
+                tips = _.filter(tips, function(tip) {
+                    if (!tip.type) {
+                        return true;
+                    }
+                    return tip.type === type;
+                });
+            }
+
+            valid_tips = _.uniq(valid_tips.concat(tips));
+            _.each(valid_tips, function(tip) {
+                if (!tip.is_consumed) {
+                    self.add_tip(tip);
+                }
+            });
+        },
+
+
+        add_tip: function(tip) {
+            var self = this;
+            self.tip_mutex.exec(function() {
+                return $.when(self.do_tip(tip));
+            });
+        },
+
+        do_tip: function (tip) {
+            var self = this;
+            var def = $.Deferred();
+            var Tips = new instance.web.Model('web.tip');
+            var highlight_selector = tip.highlight_selector;
+            var triggers = tip.trigger_selector ? tip.trigger_selector.split(',') : [];
+            var trigger_tip = true;
+
+            if(!$(highlight_selector).length > 0) {
+                return def.reject();
+            }
+            for (var i = 0; i < triggers.length; i++) {
+                if(!$(triggers[i]).length > 0) {
+                    trigger_tip = false;
+                }
+            }
+
+            if (trigger_tip) {
+                self.$element = $(highlight_selector).first();
+                var _top = self.$element.offset().top -5;
+                var _left = self.$element.offset().left -5;
+                var _width = self.$element.outerWidth() + 10;
+                var _height = self.$element.outerHeight() + 10;
+
+                self.$helper = $("<div>", { class: 'oe_tip_helper' });
+                self.$element.after(self.$helper);
+                self.$helper.offset({top: _top , left: _left});
+                self.$helper.width(_width);
+                self.$helper.height(_height);
+
+                self.$overlay = $("<div>", { class: 'oe_tip_overlay' });
+                $('body').append(self.$overlay);
+                self.$element.addClass('oe_tip_show_element');
+
+                // fix the stacking context problem
+                _.each(self.$element.parentsUntil('body'), function(el) {
+                    var zIndex = $(el).css('z-index');
+                    var opacity = parseFloat($(el).css('opacity'));
+
+                    if (/[0-9]+/.test(zIndex) || opacity < 1) {
+                        $(el).addClass('oe_tip_fix_parent');
+                    }
+                });
+
+                self.$element.popover({
+                    placement: tip.placement,
+                    title: tip.title,
+                    content: tip.description,
+                    html: true,
+                    container: 'body',
+                }).popover("show");
+
+                var $cross = $('<button type="button" class="close">&times;</button>');
+                $cross.addClass('oe_tip_close');
+
+                if (tip.title) {
+                    $('.popover-title').prepend($cross);
+                } else {
+                    $('.popover-content').prepend($cross);
+                }
+
+                // consume tip
+                tip.end_selector = tip.end_selector ? tip.end_selector : tip.highlight_selector;
+                $(tip.end_selector).one(tip.end_event, function($ev) {
+                    self.end_tip(tip);
+                    def.resolve();
+                });
+
+                // dismiss tip
+                $cross.on('click', function($ev) {
+                    self.end_tip(tip);
+                    def.resolve();
+                });
+                self.$overlay.on('click', function($ev) {
+                    self.end_tip(tip);
+                    def.resolve();
+                });
+                $(document).on('keyup.web_tip', function($ev) {
+                    if ($ev.which === 27) { // esc
+                        self.end_tip(tip);
+                        def.resolve();
+                    }
+                });
+            } else {
+               def.reject();
+            }
+            return def;
+        },
+
+        end_tip: function(tip) {
+            var self = this;
+            var Tips = new instance.web.Model('web.tip');
+            self.$element.popover('destroy');
+            self.$overlay.remove();
+            self.$helper.remove();
+            self.$element.removeClass('oe_tip_show_element');
+            _.each($('.oe_tip_fix_parent'), function(el) {
+                $(el).removeClass('oe_tip_fix_parent');
+            });
+            $(document).off('keyup.web_tip');
+            Tips.call('consume', [tip.id], {});
+            tip.is_consumed = true;
+        }
+    });
+
+    instance.web.WebClient = instance.web.WebClient.extend({
+        show_application: function() {
+            this._super();
+            this.tip_handler = new instance.web.Tip();
+        }
+    });
+})();
diff --git a/addons/web_tip/views/tip.xml b/addons/web_tip/views/tip.xml
new file mode 100644 (file)
index 0000000..49159b3
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data>
+        <template id="assets_backend" name="tip assets" inherit_id="web.assets_backend">
+            <xpath expr="." position="inside">
+                <link rel="stylesheet" href="/web_tip/static/src/css/tip.css"/>
+                <script type="text/javascript" src="/web_tip/static/src/js/tip.js"></script>
+            </xpath>
+        </template>
+    </data>
+</openerp>
diff --git a/addons/web_tip/web_tip.py b/addons/web_tip/web_tip.py
new file mode 100644 (file)
index 0000000..18673cf
--- /dev/null
@@ -0,0 +1,63 @@
+##############################################################################
+#
+#    OpenERP, Open Source Management Solution
+#    Copyright (C) 2009-Today OpenERP SA (<http://www.openerp.com>)
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>
+#
+##############################################################################
+
+from openerp.osv import osv, fields
+
+
+class tip(osv.Model):
+    _name = 'web.tip'
+    _description = 'Tips'
+
+    def _is_consumed(self, cr, uid, ids, fields, arg, context=None):
+        results = {}
+        records = self.read(cr, uid, ids, ['user_ids'])
+        for rec in records:
+            if uid in rec['user_ids']:
+                results[rec['id']] = True
+            else:
+                results[rec['id']] = False
+        return results
+
+    _columns = {
+        'title': fields.char('Tip title'),
+        'description': fields.html('Tip Description', required=True),
+        'action_id': fields.many2one('ir.actions.act_window', string="Action",
+            help="The action that will trigger the tip"),
+        'model': fields.char("Model", help="Model name on which to trigger the tip, e.g. 'res.partner'."),
+        'type': fields.char("Type", help="Model type, e.g. lead or opportunity for crm.lead"),
+        'mode': fields.char("Mode", help="Mode, e.g. kanban, form"),
+        'trigger_selector': fields.char('Trigger selector', help='CSS selectors used to trigger the tip, separated by a comma (ANDed).'),
+        'highlight_selector': fields.char('Highlight selector', help='CSS selector for the element to highlight'),
+        'end_selector': fields.char('End selector', help='CSS selector used to end the tip'),
+        'end_event': fields.char('End event', help='Event to end the tip'),
+        'placement': fields.char('Placement', help='Popover placement, bottom, top, left or right'),
+        'user_ids': fields.many2many('res.users', string='Consumed by'),
+        'is_consumed': fields.function(_is_consumed, type='boolean', string='Tip consumed')
+    }
+
+    _defaults = {
+        'placement': 'auto',
+        'end_event': 'click'
+    }
+
+    def consume(self, cr, uid, ids, context=None):
+        return self.write(cr, uid, ids, {
+            'user_ids': [(4, uid)]
+        }, context=context)
diff --git a/addons/web_tip/web_tip_view.xml b/addons/web_tip/web_tip_view.xml
new file mode 100644 (file)
index 0000000..246116f
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data>
+        <record id="edit_tip_form" model="ir.ui.view">
+            <field name="model">web.tip</field>
+            <field name="arch" type="xml">
+                <form string="Menu">
+                    <sheet>
+                        <group>
+                            <group>
+                                <field name="title"/>
+                                <field name="description"/>
+                                <field name="action_id"/>
+                            </group>
+                            <group>
+                                <field name="model"/>
+                                <field name="type"/>
+                                <field name="mode"/>
+                            </group>
+                        </group>
+                        <group>
+                            <field name="trigger_selector"/>
+                            <field name="highlight_selector"/>
+                            <field name="end_selector"/>
+                            <field name="end_event"/>
+                            <field name="placement"/>
+                        </group>
+                        <notebook>
+                            <page string="Consumed by">
+                                <field name="user_ids"/>
+                            </page>
+                        </notebook>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+        <record id="edit_tip_list" model="ir.ui.view">
+            <field name="model">web.tip</field>
+            <field eval="8" name="priority"/>
+            <field name="arch" type="xml">
+                <tree string="Menu">
+                    <field name="model"/>
+                    <field name="mode"/>
+                </tree>
+            </field>
+        </record>
+        <record id="edit_tip_search" model="ir.ui.view">
+            <field name="name">web.tip.search</field>
+            <field name="model">web.tip</field>
+            <field name="arch" type="xml">
+                <search string="Tip">
+                    <field name="model"/>
+                    <field name="mode"/>
+                </search>
+            </field>
+        </record>
+        <record id="edit_tip_action" model="ir.actions.act_window">
+            <field name="name">Tips</field>
+            <field name="res_model">web.tip</field>
+            <field name="view_type">form</field>
+            <field name="view_id" ref="edit_tip_list"/>
+            <field name="search_view_id" ref="edit_tip_search"/>
+        </record>
+        <menuitem action="edit_tip_action" id="menu_tip_action" parent="base.next_id_2" sequence="5"/>
+    </data>
+</openerp>