[FIX] web: add missing icon, add warning, fix deferred
[odoo/odoo.git] / addons / web / static / src / js / views.js
index 47a56ca..f8e368a 100644 (file)
@@ -11,6 +11,7 @@ var QWeb = instance.web.qweb,
     _t = instance.web._t;
 
 instance.web.ActionManager = instance.web.Widget.extend({
+    template: "ActionManager",
     init: function(parent) {
         this._super(parent);
         this.inner_action = null;
@@ -18,7 +19,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
         this.webclient = parent;
         this.dialog = null;
         this.dialog_widget = null;
-        this.view_managers = [];
+        this.widgets = [];
         this.on('history_back', this, this.proxy('history_back'));
     },
     dialog_stop: function (reason) {
@@ -28,95 +29,120 @@ instance.web.ActionManager = instance.web.Widget.extend({
         this.dialog = null;
     },
     /**
-     * Add a new item to the breadcrumb
+     * Add a new widget to the action manager
      *
-     * If the title of an item is an array, the multiple title mode is in use.
-     * (eg: a widget with multiple views might need to display a title for each view)
-     * In multiple title mode, the show() callback can check the index it receives
-     * in order to detect which of its titles has been clicked on by the user.
-     *
-     * @param {Object} item breadcrumb item
-     * @param {Object} item.widget widget containing the view(s) to be added to the breadcrumb added
-     * @param {Function} [item.show] triggered whenever the widget should be shown back
-     * @param {Function} [item.hide] triggered whenever the widget should be shown hidden
-     * @param {Function} [item.destroy] triggered whenever the widget should be destroyed
-     * @param {String|Array} [item.title] title(s) of the view(s) to be displayed in the breadcrumb
-     * @param {Function} [item.get_title] should return the title(s) of the view(s) to be displayed in the breadcrumb
+     * widget: typically, widgets added are instance.web.ViewManager.  The action manager
+     *      uses this list of widget to handle the breadcrumbs.
+     * action: new action
+     * options.on_reverse_breadcrumb: will be called when breadcrumb is selected
+     * options.clear_breadcrumbs: boolean, if true, current widgets are destroyed
+     * options.replace_breadcrumb: boolean, if true, replace current breadcrumb
      */
-    push_view_manager: function(widget, action, clear_breadcrumbs) {
+    push_widget: function(widget, action, options) {
         var self = this,
+            to_destroy,
+            options = options || {},
             old_widget = this.inner_widget;
 
-        if (clear_breadcrumbs) {
-            var to_destroy = this.view_managers;
-            this.view_managers = [];
+        if (options.clear_breadcrumbs) {
+            to_destroy = this.widgets;
+            this.widgets = [];
+        } else if (options.replace_breadcrumb) {
+            to_destroy = _.last(this.widgets);
+            this.widgets = _.initial(this.widgets);
         }
-        if (widget instanceof instance.web.ViewManager) {
-            this.view_managers.push(widget);
+        if (widget instanceof instance.web.Widget) {
+            var title = widget.get('title') || action.display_name || action.name;
+            widget.set('title', title);
+            this.widgets.push(widget);
         } else {
-            this.view_managers.push({
+            this.widgets.push({
                 view_stack: [{
                     controller: {get: function () {return action.display_name || action.name; }},
                 }],
                 destroy: function () {},
             });
         }
+        _.last(this.widgets).__on_reverse_breadcrumb = options.on_reverse_breadcrumb;
         this.inner_action = action;
         this.inner_widget = widget;
         return $.when(this.inner_widget.appendTo(this.$el)).done(function () {
             (action.target !== 'inline') && (!action.flags.headless) && widget.$header && widget.$header.show();
             old_widget && old_widget.$el.hide();
-            if (clear_breadcrumbs) {
-                self.clear_view_managers(to_destroy)
+            if (options.clear_breadcrumbs) {
+                self.clear_widgets(to_destroy)
             }
         });
     },
     get_breadcrumbs: function () {
-        return _.flatten(_.map(this.view_managers, function (vm) {
-            return vm.view_stack.map(function (view, index) { 
-                return {
-                    title: view.controller.get('title') || vm.title,
-                    index: index,
-                    view_manager: vm,
-                }; 
-            });
+        return _.flatten(_.map(this.widgets, function (widget) {
+            if (widget instanceof instance.web.ViewManager) {
+                return widget.view_stack.map(function (view, index) { 
+                    return {
+                        title: view.controller.get('title') || widget.title,
+                        index: index,
+                        widget: widget,
+                    }; 
+                });
+            } else {
+                return {title: widget.get('title'), widget: widget };
+            }
         }), true);
     },
+    get_title: function () {
+        if (this.widgets.length === 1) {
+            // horrible hack to display the action title instead of "New" for the actions
+            // that use a form view to edit something that do not correspond to a real model
+            // for example, point of sale "Your Session" or most settings form,
+            var widget = this.widgets[0];
+            if (widget instanceof instance.web.ViewManager && widget.view_stack.length === 1) {
+                return widget.title;
+            }
+        }
+        return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
+    },
+    get_widgets: function () {
+        return this.widgets.slice(0);
+    },
     history_back: function() {
-        var view_manager = _.last(this.view_managers),
-            nbr_views = view_manager.view_stack.length;
-        if (nbr_views > 1) {
-            this.select_view_manager(view_manager, nbr_views - 2);
-        } else if (this.view_managers.length > 1) {
-            view_manager = this.view_managers[this.view_managers.length -2];
-            nbr_views = view_manager.view_stack.length;
-            this.select_view(view_managers, nbr_views - 2)
+        var widget = _.last(this.widgets);
+        if (widget instanceof instance.web.ViewManager) {
+            var nbr_views = widget.view_stack.length;
+            if (nbr_views > 1) {
+                return this.select_widget(widget, nbr_views - 2);
+            } 
+        } 
+        if (this.widgets.length > 1) {
+            widget = this.widgets[this.widgets.length - 2];
+            var index = widget.view_stack && widget.view_stack.length - 1;
+            this.select_widget(widget, index);
         }
     },
-    select_view_manager: function(view_manager, index) {
+    select_widget: function(widget, index) {
         var self = this;
         if (this.webclient.has_uncommitted_changes()) {
             return false;
         }
-        var vm_index = this.view_managers.indexOf(view_manager);
-        if (view_manager.select_view) {
-            view_manager.select_view(index).done(function () {
-                _.each(self.view_managers.splice(vm_index + 1), function (vm) {
-                    vm.destroy();
-                });
-                self.inner_widget = _.last(self.view_managers);
-                self.inner_widget.display_breadcrumbs();
-                self.inner_widget.$el.show();
+        var widget_index = this.widgets.indexOf(widget),
+            def = $.when(widget.select_view && widget.select_view(index));
+
+        def.done(function () {
+            if (widget.__on_reverse_breadcrumb) {
+                widget.__on_reverse_breadcrumb();
+            }
+            _.each(self.widgets.splice(widget_index + 1), function (w) {
+                w.destroy();
             });
-        }
-    },
-    clear_view_managers: function(vms) {
-        _.each(vms || this.view_managers, function (vm) {
-            vm.destroy();
+            self.inner_widget = _.last(self.widgets);
+            self.inner_widget.display_breadcrumbs && self.inner_widget.display_breadcrumbs();
+            self.inner_widget.do_show && self.inner_widget.do_show();
         });
-        if (!vms) {
-            this.view_managers = [];
-            this.inner_widget = null;            
+    },
+    clear_widgets: function(widgets) {
+        _.invoke(widgets || this.widgets, 'destroy');
+        if (!widgets) {
+            this.widgets = [];
+            this.inner_widget = null;
         }
     },
     do_push_state: function(state) {
@@ -129,7 +155,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
                 // this action has been explicitly marked as not pushable
                 return;
             }
-            state.title = this.inner_action.name;
+            state.title = this.get_title(); 
             if(this.inner_action.type == 'ir.actions.act_window') {
                 state.model = this.inner_action.res_model;
             }
@@ -245,6 +271,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
      * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
      * @param {Object} [options]
      * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
+     * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action
      * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
      * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
      * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
@@ -302,12 +329,13 @@ instance.web.ActionManager = instance.web.Widget.extend({
         var type = action.type.replace(/\./g,'_');
         var popup = action.target === 'new';
         var inline = action.target === 'inline' || action.target === 'inlineview';
+        var form = _.str.startsWith(action.view_mode, 'form');
         action.flags = _.defaults(action.flags || {}, {
             views_switcher : !popup && !inline,
             search_view : !popup && !inline,
             action_buttons : !popup && !inline,
             sidebar : !popup && !inline,
-            pager : !popup && !inline,
+            pager : (!popup || !form) && !inline,
             display_title : !popup,
             search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
         });
@@ -320,7 +348,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
     },
     null_action: function() {
         this.dialog_stop();
-        this.clear_view_managers();
+        this.clear_widgets();
     },
     /**
      *
@@ -379,14 +407,14 @@ instance.web.ActionManager = instance.web.Widget.extend({
             this.dialog_widget.setParent(this.dialog);
             var initialized = this.dialog_widget.appendTo(this.dialog.$el);
             this.dialog.open();
-            return initialized;
+            return $.when(initialized);
         }
         if (this.inner_widget && this.webclient.has_uncommitted_changes()) {
             return $.Deferred().reject();
         }
         widget = executor.widget();
         this.dialog_stop(executor.action);
-        return this.push_view_manager(widget, executor.action, options.clear_breadcrumbs);
+        return this.push_widget(widget, executor.action, options);
     },
     ir_actions_act_window: function (action, options) {
         var self = this;
@@ -610,7 +638,7 @@ instance.web.ViewManager =  instance.web.Widget.extend({
         if (!view) {
             return $.Deferred().reject();
         }
-        if (view_type !== 'form') {
+        if ((view_type !== 'form') && (view_type !== 'diagram')) {
             this.view_stack = [];
         } 
 
@@ -671,10 +699,11 @@ instance.web.ViewManager =  instance.web.Widget.extend({
         var self = this;
         if (!this.action_manager) return;
         var breadcrumbs = this.action_manager.get_breadcrumbs();
+        if (!breadcrumbs.length) return;
         var $breadcrumbs = _.map(_.initial(breadcrumbs), function (bc) {
             var $link = $('<a>').text(bc.title);
             $link.click(function () {
-                self.action_manager.select_view_manager(bc.view_manager, bc.index);
+                self.action_manager.select_widget(bc.widget, bc.index);
             });
             return $('<li>').append($link);
         });
@@ -712,6 +741,7 @@ instance.web.ViewManager =  instance.web.Widget.extend({
         controller.on('view_loaded', this, function () {
             view_loaded.resolve();
         });
+        this.$('.oe-view-manager-pager > span').hide();
         return $.when(controller.appendTo($container), view_loaded)
                 .done(function () { 
                     self.trigger("controller_inited", view.type, controller);
@@ -723,6 +753,12 @@ instance.web.ViewManager =  instance.web.Widget.extend({
         return this.switch_mode(view_type);
     },
     /**
+     * @returns {Number|Boolean} the view id of the given type, false if not found
+     */
+    get_view_id: function(view_type) {
+        return this.views[view_type] && this.views[view_type].view_id || false;
+    },    
+    /**
      * Sets up the current viewmanager's search view.
      *
      * @param {Number|false} view_id the view to use or false for a default one
@@ -750,6 +786,7 @@ instance.web.ViewManager =  instance.web.Widget.extend({
             hidden: this.flags.search_view === false,
             disable_custom_filters: this.flags.search_disable_custom_filters,
             $buttons: this.$('.oe-search-options'),
+            action: this.action,
         };
         var SearchView = instance.web.SearchView;
         this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options);
@@ -791,16 +828,15 @@ instance.web.ViewManager =  instance.web.Widget.extend({
         }
     },    
     do_load_state: function(state, warm) {
-        var self = this,
-            def = this.active_view.created;
         if (state.view_type && state.view_type !== this.active_view.type) {
-            def = def.then(function() {
-                return self.switch_mode(state.view_type, true);
-            });
+            // warning: this code relies on the fact that switch_mode has an immediate side
+            // effect (setting the 'active_view' to its new value) AND an async effect (the
+            // view is created/loaded).  So, the next statement (do_load_state) is executed 
+            // on the new view, after it was initialized, but before it is fully loaded and 
+            // in particular, before the do_show method is called.
+            this.switch_mode(state.view_type, true);
         } 
-        def.done(function() {
-            self.active_view.controller.do_load_state(state, warm);
-        });
+        this.active_view.controller.do_load_state(state, warm);
     },
     on_debug_changed: function (evt) {
         var self = this,
@@ -827,6 +863,9 @@ instance.web.ViewManager =  instance.web.Widget.extend({
                         var dialog = new instance.web.Dialog(this, {
                             title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model),
                             size: 'medium',
+                            buttons: {
+                                Ok: function() { this.parents('.modal').modal('hide');}
+                            },
                         }, QWeb.render('ViewManagerDebugViewLog', {
                             perm : result[0],
                             format : instance.web.format_value
@@ -869,6 +908,9 @@ instance.web.ViewManager =  instance.web.Widget.extend({
                     new instance.web.Dialog(self, {
                         title: _.str.sprintf(_t("Model %s fields"),
                                              self.dataset.model),
+                        buttons: {
+                            Ok: function() { this.parents('.modal').modal('hide');}
+                        },
                         }, $root).open();
                 });
                 break;
@@ -914,6 +956,8 @@ instance.web.ViewManager =  instance.web.Widget.extend({
                         data: {action: JSON.stringify(action)},
                         complete: instance.web.unblockUI
                     });
+                } else {
+                    self.do_warn("Warning", "No record selected.");
                 }
                 break;
             case 'leave_debug':