d623c889e55dafefac3b617eda1127b500bbeecd
[odoo/odoo.git] / addons / web / static / src / js / views.js
1 /*---------------------------------------------------------
2  * OpenERP web library
3  *---------------------------------------------------------*/
4
5 openerp.web.views = function(instance) {
6 var QWeb = instance.web.qweb,
7     _t = instance.web._t;
8
9 instance.web.ActionManager = instance.web.Widget.extend({
10     init: function(parent) {
11         this._super(parent);
12         this.inner_action = null;
13         this.inner_viewmanager = null;
14         this.dialog = null;
15         this.dialog_viewmanager = null;
16         this.client_widget = null;
17     },
18     start: function() {
19         this._super.apply(this, arguments);
20         this.bread_crumb = new instance.web.BreadCrumb(this);
21     },
22     dialog_stop: function () {
23         if (this.dialog) {
24             this.dialog_viewmanager.destroy();
25             this.dialog_viewmanager = null;
26             this.dialog.destroy();
27             this.dialog = null;
28         }
29     },
30     content_stop: function () {
31         if (this.inner_viewmanager) {
32             this.inner_viewmanager.destroy();
33             this.inner_viewmanager = null;
34         }
35         if (this.client_widget) {
36             this.client_widget.destroy();
37             this.client_widget = null;
38         }
39     },
40     do_push_state: function(state) {
41         if (this.getParent() && this.getParent().do_push_state) {
42             if (this.inner_action) {
43                 state['title'] = this.inner_action.name;
44                 state['model'] = this.inner_action.res_model;
45                 if (this.inner_action.id) {
46                     state['action_id'] = this.inner_action.id;
47                 }
48             }
49             this.getParent().do_push_state(state);
50         }
51     },
52     do_load_state: function(state, warm) {
53         var self = this,
54             action_loaded;
55         if (state.action_id) {
56             var run_action = (!this.inner_viewmanager) || this.inner_viewmanager.action.id !== state.action_id;
57             if (run_action) {
58                 this.null_action();
59                 action_loaded = this.do_action(state.action_id);
60                 instance.webclient.menu.has_been_loaded.then(function() {
61                     instance.webclient.menu.open_action(state.action_id);
62                 });
63             }
64         } else if (state.model && state.id) {
65             // TODO handle context & domain ?
66             this.null_action();
67             var action = {
68                 res_model: state.model,
69                 res_id: state.id,
70                 type: 'ir.actions.act_window',
71                 views: [[false, 'form']]
72             };
73             action_loaded = this.do_action(action);
74         } else if (state.sa) {
75             // load session action
76             var self = this;
77             this.null_action();
78             action_loaded = this.rpc('/web/session/get_session_action',  {key: state.sa}).pipe(function(action) {
79                 if (action) {
80                     return self.do_action(action);
81                 }
82             });
83         } else if (state.client_action) {
84             this.null_action();
85             var action = state.client_action;
86             if(_.isString(action)) {
87                 action = {
88                     tag: action,
89                     params: state,
90                 };
91             }
92             this.ir_actions_client(action);
93         }
94
95         $.when(action_loaded || null).then(function() {
96             if (self.inner_viewmanager) {
97                 self.inner_viewmanager.do_load_state(state, warm);
98             }
99         });
100     },
101     do_action: function(action, on_close) {
102         if (_.isNumber(action)) {
103             var self = this;
104             return self.rpc("/web/action/load", { action_id: action }, function(result) {
105                 self.do_action(result.result, on_close);
106             });
107         }
108         if (!action.type) {
109             console.error("No type for action", action);
110             return;
111         }
112         var type = action.type.replace(/\./g,'_');
113         var popup = action.target === 'new';
114         var inline = action.target === 'inline';
115         action.flags = _.extend({
116             views_switcher : !popup && !inline,
117             search_view : !popup && !inline,
118             action_buttons : !popup && !inline,
119             sidebar : !popup && !inline,
120             pager : !popup && !inline,
121             display_title : !popup
122         }, action.flags || {});
123         if (!(type in this)) {
124             console.error("Action manager can't handle action of type " + action.type, action);
125             return;
126         }
127         return this[type](action, on_close);
128     },
129     null_action: function() {
130         this.dialog_stop();
131         this.content_stop();
132     },
133     ir_actions_act_window: function (action, on_close) {
134         var self = this;
135         if (_(['base.module.upgrade', 'base.setup.installer'])
136                 .contains(action.res_model)) {
137             var old_close = on_close;
138             on_close = function () {
139                 instance.webclient.do_reload().then(old_close);
140             };
141         }
142         if (action.target === 'new') {
143             if (this.dialog === null) {
144                 this.dialog = new instance.web.Dialog(this, { width: '80%' });
145                 if(on_close)
146                     this.dialog.on_close.add(on_close);
147             } else {
148                 this.dialog_viewmanager.destroy();
149             }
150             this.dialog.dialog_title = action.name;
151             this.dialog_viewmanager = new instance.web.ViewManagerAction(this.dialog, action);
152             this.dialog_viewmanager.appendTo(this.dialog.$element);
153             this.dialog_viewmanager.$element.addClass("oe_view_manager_" + action.target);
154             this.dialog.open();
155         } else  {
156             this.dialog_stop();
157             //this.content_stop();
158             this.bread_crumb.hide_all();
159             if(action.menu_id) {
160                 return this.getParent().do_action(action, function () {
161                     instance.webclient.menu.open_menu(action.menu_id);
162                 });
163             }
164             this.inner_action = action;
165             this.inner_viewmanager = new instance.web.ViewManagerAction(this, action);
166             this.inner_viewmanager.appendTo(this.$element);
167             this.inner_viewmanager.$element.addClass("oe_view_manager_" + action.target);
168             this.bread_crumb.add({
169                 view_manager: this.inner_viewmanager,
170                 action: action,
171                 title: action.name
172             });
173         }
174     },
175     ir_actions_act_window_close: function (action, on_closed) {
176         if (!this.dialog && on_closed) {
177             on_closed();
178         }
179         this.dialog_stop();
180     },
181     ir_actions_server: function (action, on_closed) {
182         var self = this;
183         this.rpc('/web/action/run', {
184             action_id: action.id,
185             context: action.context || {}
186         }).then(function (action) {
187             self.do_action(action, on_closed)
188         });
189     },
190     ir_actions_client: function (action) {
191         this.content_stop();
192         this.dialog_stop();
193         var ClientWidget = instance.web.client_actions.get_object(action.tag);
194         (this.client_widget = new ClientWidget(this, action.params)).appendTo(this.$element);
195     },
196     ir_actions_report_xml: function(action, on_closed) {
197         var self = this;
198         $.blockUI();
199         self.rpc("/web/session/eval_domain_and_context", {
200             contexts: [action.context],
201             domains: []
202         }).then(function(res) {
203             action = _.clone(action);
204             action.context = res.context;
205             self.session.get_file({
206                 url: '/web/report',
207                 data: {action: JSON.stringify(action)},
208                 complete: $.unblockUI,
209                 success: function(){
210                     if (!self.dialog && on_closed) {
211                         on_closed();
212                     }
213                     self.dialog_stop();
214                 },
215                 error: instance.webclient.crashmanager.on_rpc_error
216             })
217         });
218     },
219     ir_actions_act_url: function (action) {
220         window.open(action.url, action.target === 'self' ? '_self' : '_blank');
221     },
222     ir_ui_menu: function (action) {
223         this.getParent().do_action(action);
224     }
225 });
226
227 instance.web.BreadCrumb = instance.web.CallbackEnabled.extend({
228     init: function(parent) {
229         this._super(parent);
230         this.action_manager = parent;
231         this.items = [];
232         this.action_manager.$element.on('click', '.oe_breadcrumb_item', this.on_item_clicked);
233     },
234     add: function(item) {
235         this.items.push(item);
236     },
237     hide_all: function() {
238         _.each(this.items, function(i) {
239             i.view_manager.$element.hide();
240         });
241     },
242     get_title: function() {
243         return QWeb.render('BreadCrumb', { widget: this });
244     },
245     on_item_clicked: function(ev) {
246         var $e = $(ev.target);
247         var index = $e.data('index');
248         this.select_item(index);
249     },
250     select_item: function(index) {
251         for (var i = index + 1; i < this.items.length; i += 1) {
252             this.remove_item(i);
253         }
254         this.items[index].view_manager.$element.show();
255     },
256     remove_item: function(index) {
257         var item = this.items.splice(index, 1)[0];
258         if (item) {
259             item.view_manager.destroy();
260         }
261     }
262 });
263
264 instance.web.ViewManager =  instance.web.Widget.extend({
265     template: "ViewManager",
266     init: function(parent, dataset, views, flags) {
267         this._super(parent);
268         this.model = dataset ? dataset.model : undefined;
269         this.dataset = dataset;
270         this.searchview = null;
271         this.active_view = null;
272         this.views_src = _.map(views, function(x) {
273             if (x instanceof Array) {
274                 var View = instance.web.views.get_object(x[1], true);
275                 return {
276                     view_id: x[0],
277                     view_type: x[1],
278                     label: View ? View.prototype.display_name : (void 'nope')
279                 };
280             } else {
281                 return x;
282             }
283         });
284         this.views = {};
285         this.flags = flags || {};
286         this.registry = instance.web.views;
287         this.views_history = [];
288     },
289     /**
290      * @returns {jQuery.Deferred} initial view loading promise
291      */
292     start: function() {
293         this._super();
294         var self = this;
295         this.$element.find('.oe_view_manager_switch a').click(function() {
296             self.on_mode_switch($(this).data('view-type'));
297         }).tipsy();
298         var views_ids = {};
299         _.each(this.views_src, function(view) {
300             self.views[view.view_type] = $.extend({}, view, {
301                 deferred : $.Deferred(),
302                 controller : null,
303                 options : _.extend({
304                     $buttons : self.$element.find('.oe_view_manager_buttons'),
305                     $sidebar : self.flags.sidebar ? self.$element.find('.oe_view_manager_sidebar') : undefined,
306                     $pager : self.$element.find('.oe_view_manager_pager'),
307                     action : self.action,
308                     action_views_ids : views_ids
309                 }, self.flags, self.flags[view.view_type] || {}, view.options || {})
310             });
311             views_ids[view.view_type] = view.view_id;
312         });
313         if (this.flags.views_switcher === false) {
314             this.$element.find('.oe_view_manager_switch').hide();
315         }
316         // If no default view defined, switch to the first one in sequence
317         var default_view = this.flags.default_view || this.views_src[0].view_type;
318         return this.on_mode_switch(default_view);
319     },
320     /**
321      * Asks the view manager to switch visualization mode.
322      *
323      * @param {String} view_type type of view to display
324      * @param {Boolean} [no_store=false] don't store the view being switched to on the switch stack
325      * @returns {jQuery.Deferred} new view loading promise
326      */
327     on_mode_switch: function(view_type, no_store, view_options) {
328         var self = this;
329         var view = this.views[view_type];
330         var view_promise;
331         if(!view)
332             return $.Deferred().reject();
333
334         if (!no_store) {
335             this.views_history.push(view_type);
336         }
337         this.active_view = view_type;
338
339         if (!view.controller) {
340             // Lazy loading of views
341             var controllerclass = this.registry.get_object(view_type);
342             var options = _.clone(view.options);
343             if (view_type === "form" && this.action) {
344                 switch (this.action.target) {
345                     case 'new':
346                     case 'inline':
347                         options.initial_mode = 'edit';
348                         break;
349                 }
350             }
351             var controller = new controllerclass(this, this.dataset, view.view_id, options);
352             if (view.embedded_view) {
353                 controller.set_embedded_view(view.embedded_view);
354             }
355             controller.do_switch_view.add_last(_.bind(this.switch_view, this));
356             controller.do_prev_view.add_last(this.on_prev_view);
357             var container = this.$element.find(".oe_view_manager_view_" + view_type);
358             view_promise = controller.appendTo(container);
359             this.views[view_type].controller = controller;
360             this.views[view_type].deferred.resolve(view_type);
361             $.when(view_promise).then(function() {
362                 self.on_controller_inited(view_type, controller);
363                 if (self.searchview
364                         && self.flags.auto_search
365                         && view.controller.searchable !== false) {
366                     self.searchview.ready.then(self.searchview.do_search);
367                 }
368             });
369         } else if (this.searchview
370                 && self.flags.auto_search
371                 && view.controller.searchable !== false) {
372             this.searchview.ready.then(this.searchview.do_search);
373         }
374
375         if (this.searchview) {
376             this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show']();
377         }
378
379         this.$element
380             .find('.oe_view_manager_switch a').parent().removeClass('active');
381         this.$element
382             .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
383             .parent().addClass('active');
384
385         $.when(view_promise).then(function () {
386             _.each(_.keys(self.views), function(view_name) {
387                 var controller = self.views[view_name].controller;
388                 if (controller) {
389                     var container = self.$element.find(".oe_view_manager_view_" + view_name + ":first");
390                     if (view_name === view_type) {
391                         container.show();
392                         controller.do_show(view_options || {});
393                     } else {
394                         container.hide();
395                         controller.do_hide();
396                     }
397                 }
398             });
399
400             self.$element.find('.oe_view_title_text:first').html(
401                     self.display_title());
402         });
403         return view_promise;
404     },
405     /**
406      * Method used internally when a view asks to switch view. This method is meant
407      * to be extended by child classes to change the default behavior, which simply
408      * consist to switch to the asked view.
409      */
410     switch_view: function(view_type, no_store, options) {
411         return this.on_mode_switch(view_type, no_store, options);
412     },
413     /**
414      * Returns to the view preceding the caller view in this manager's
415      * navigation history (the navigation history is appended to via
416      * on_mode_switch)
417      *
418      * @param {Object} [options]
419      * @param {Boolean} [options.created=false] resource was created
420      * @param {String} [options.default=null] view to switch to if no previous view
421      * @returns {$.Deferred} switching end signal
422      */
423     on_prev_view: function (options) {
424         options = options || {};
425         var current_view = this.views_history.pop();
426         var previous_view = this.views_history[this.views_history.length - 1] || options['default'];
427         if (options.created && current_view === 'form' && previous_view === 'list') {
428             // APR special case: "If creation mode from list (and only from a list),
429             // after saving, go to page view (don't come back in list)"
430             return this.on_mode_switch('form');
431         } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') {
432             // APR special case: "If creation from dashboard, we have no previous view
433             return this.on_mode_switch('form');
434         }
435         return this.on_mode_switch(previous_view, true);
436     },
437     /**
438      * Sets up the current viewmanager's search view.
439      *
440      * @param {Number|false} view_id the view to use or false for a default one
441      * @returns {jQuery.Deferred} search view startup deferred
442      */
443     setup_search_view: function(view_id, search_defaults) {
444         var self = this;
445         if (this.searchview) {
446             this.searchview.destroy();
447         }
448         this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, this.flags.search_view === false);
449
450         this.searchview.on_search.add(this.do_searchview_search);
451         return this.searchview.appendTo(this.$element.find(".oe_view_manager_view_search"));
452     },
453     do_searchview_search: function(domains, contexts, groupbys) {
454         var self = this,
455             controller = this.views[this.active_view].controller,
456             action_context = this.action.context || {};
457         this.rpc('/web/session/eval_domain_and_context', {
458             domains: [this.action.domain || []].concat(domains || []),
459             contexts: [action_context].concat(contexts || []),
460             group_by_seq: groupbys || []
461         }, function (results) {
462             self.dataset._model = new instance.web.Model(
463                 self.dataset.model, results.context, results.domain);
464             var groupby = results.group_by.length
465                         ? results.group_by
466                         : action_context.group_by;
467             if (_.isString(groupby)) {
468                 groupby = [groupby];
469             }
470             controller.do_search(results.domain, results.context, groupby || []);
471         });
472     },
473     /**
474      * Event launched when a controller has been inited.
475      *
476      * @param {String} view_type type of view
477      * @param {String} view the inited controller
478      */
479     on_controller_inited: function(view_type, view) {
480     },
481     /**
482      * Called when one of the view want to execute an action
483      */
484     on_action: function(action) {
485     },
486     on_create: function() {
487     },
488     on_remove: function() {
489     },
490     on_edit: function() {
491     },
492     /**
493      * Called by children view after executing an action
494      */
495     on_action_executed: function () {
496     },
497     display_title: function () {
498         var view = this.views[this.active_view];
499         if (view) {
500             // ick
501             return view.controller.fields_view.arch.attrs.string;
502         }
503         return '';
504     }
505 });
506
507 instance.web.ViewManagerAction = instance.web.ViewManager.extend({
508     template:"ViewManagerAction",
509     /**
510      * @constructs instance.web.ViewManagerAction
511      * @extends instance.web.ViewManager
512      *
513      * @param {instance.web.ActionManager} parent parent object/widget
514      * @param {Object} action descriptor for the action this viewmanager needs to manage its views.
515      */
516     init: function(parent, action) {
517         // dataset initialization will take the session from ``this``, so if we
518         // do not have it yet (and we don't, because we've not called our own
519         // ``_super()``) rpc requests will blow up.
520         var flags = action.flags || {};
521         if (!('auto_search' in flags)) {
522             flags.auto_search = action.auto_search !== false;
523         }
524         if (action.res_model == 'board.board' && action.view_mode === 'form') {
525             // Special case for Dashboards
526             _.extend(flags, {
527                 views_switcher : false,
528                 display_title : false,
529                 search_view : false,
530                 pager : false,
531                 sidebar : false,
532                 action_buttons : false
533             });
534         }
535         this._super(parent, null, action.views, flags);
536         this.session = parent.session;
537         this.action = action;
538         var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
539         if (action.res_id) {
540             dataset.ids.push(action.res_id);
541             dataset.index = 0;
542         }
543         this.dataset = dataset;
544
545         // setup storage for session-wise menu hiding
546         if (this.session.hidden_menutips) {
547             return;
548         }
549         this.session.hidden_menutips = {}
550     },
551     /**
552      * Initializes the ViewManagerAction: sets up the searchview (if the
553      * searchview is enabled in the manager's action flags), calls into the
554      * parent to initialize the primary view and (if the VMA has a searchview)
555      * launches an initial search after both views are done rendering.
556      */
557     start: function() {
558         var self = this,
559             searchview_loaded,
560             search_defaults = {};
561         _.each(this.action.context, function (value, key) {
562             var match = /^search_default_(.*)$/.exec(key);
563             if (match) {
564                 search_defaults[match[1]] = value;
565             }
566         });
567         // init search view
568         var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
569
570         searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
571
572         var main_view_loaded = this._super();
573
574         var manager_ready = $.when(searchview_loaded, main_view_loaded);
575
576         this.$element.find('.oe_debug_view').change(this.on_debug_changed);
577
578         if (this.action.help && !this.flags.low_profile) {
579             var Users = new instance.web.DataSet(self, 'res.users'),
580                 $tips = this.$element.find('.oe_view_manager_menu_tips');
581             $tips.delegate('blockquote button', 'click', function() {
582                 var $this = $(this);
583                 //noinspection FallthroughInSwitchStatementJS
584                 switch ($this.attr('name')) {
585                 case 'disable':
586                     Users.write(self.session.uid, {menu_tips:false});
587                 case 'hide':
588                     $this.closest('blockquote').hide();
589                     self.session.hidden_menutips[self.action.id] = true;
590                 }
591             });
592             if (!(self.action.id in self.session.hidden_menutips)) {
593                 Users.read_ids([this.session.uid], ['menu_tips']).then(function(users) {
594                     var user = users[0];
595                     if (!(user && user.id === self.session.uid)) {
596                         return;
597                     }
598                     $tips.find('blockquote').toggle(user.menu_tips);
599                 });
600             }
601         }
602
603         return manager_ready;
604     },
605     on_debug_changed: function (evt) {
606         var self = this,
607             $sel = $(evt.currentTarget),
608             $option = $sel.find('option:selected'),
609             val = $sel.val(),
610             current_view = this.views[this.active_view].controller;
611         switch (val) {
612             case 'fvg':
613                 var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open();
614                 $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$element);
615                 break;
616             case 'perm_read':
617                 var ids = current_view.get_selected_ids();
618                 if (ids.length === 1) {
619                     this.dataset.call('perm_read', [ids]).then(function(result) {
620                         var dialog = new instance.web.Dialog(this, {
621                             title: _.str.sprintf(_t("View Log (%s)"), self.dataset.model),
622                             width: 400
623                         }, QWeb.render('ViewManagerDebugViewLog', {
624                             perm : result[0],
625                             format : instance.web.format_value
626                         })).open();
627                     });
628                 }
629                 break;
630             case 'toggle_layout_outline':
631                 current_view.rendering_engine.toggle_layout_debugging();
632                 break;
633             case 'fields':
634                 this.dataset.call_and_eval(
635                         'fields_get', [false, {}], null, 1).then(function (fields) {
636                     var $root = $('<dl>');
637                     _(fields).each(function (attributes, name) {
638                         $root.append($('<dt>').append($('<h4>').text(name)));
639                         var $attrs = $('<dl>').appendTo(
640                                 $('<dd>').appendTo($root));
641                         _(attributes).each(function (def, name) {
642                             if (def instanceof Object) {
643                                 def = JSON.stringify(def);
644                             }
645                             $attrs
646                                 .append($('<dt>').text(name))
647                                 .append($('<dd style="white-space: pre-wrap;">').text(def));
648                         });
649                     });
650                     new instance.web.Dialog(self, {
651                         title: _.str.sprintf(_t("Model %s fields"),
652                                              self.dataset.model),
653                         width: '95%'}, $root).open();
654                 });
655                 break;
656             case 'manage_views':
657                 if (current_view.fields_view && current_view.fields_view.arch) {
658                     var view_editor = new instance.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch);
659                     view_editor.start();
660                 } else {
661                     this.do_warn(_t("Manage Views"),
662                             _t("Could not find current view declaration"));
663                 }
664                 break;
665             case 'edit_workflow':
666                 return this.do_action({
667                     res_model : 'workflow',
668                     domain : [['osv', '=', this.dataset.model]],
669                     views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
670                     type : 'ir.actions.act_window',
671                     view_type : 'list',
672                     view_mode : 'list'
673                 });
674                 break;
675             case 'edit':
676                 this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
677                 break;
678             case 'manage_filters':
679                 this.do_action({
680                     res_model: 'ir.filters',
681                     views: [[false, 'list'], [false, 'form']],
682                     type: 'ir.actions.act_window',
683                     context: {
684                         search_default_my_filters: true,
685                         search_default_model_id: this.dataset.model
686                     }
687                 });
688                 break;
689             default:
690                 if (val) {
691                     console.log("No debug handler for ", val);
692                 }
693         }
694         evt.currentTarget.selectedIndex = 0;
695     },
696     do_edit_resource: function(model, id, action) {
697         var action = _.extend({
698             res_model : model,
699             res_id : id,
700             type : 'ir.actions.act_window',
701             view_type : 'form',
702             view_mode : 'form',
703             views : [[false, 'form']],
704             target : 'new',
705             flags : {
706                 action_buttons : true,
707                 form : {
708                     resize_textareas : true
709                 }
710             }
711         }, action || {});
712         this.do_action(action);
713     },
714     on_mode_switch: function (view_type, no_store, options) {
715         var self = this;
716
717         return $.when(this._super.apply(this, arguments)).then(function () {
718             var controller = self.views[self.active_view].controller,
719                 fvg = controller.fields_view,
720                 view_id = (fvg && fvg.view_id) || '--';
721             self.$element.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
722                 view: controller,
723                 view_manager: self
724             }));
725             if (!self.action.name && fvg) {
726                 self.$element.find('.oe_view_title_text').text(fvg.arch.attrs.string || fvg.name);
727             }
728
729         });
730     },
731     do_push_state: function(state) {
732         if (this.getParent() && this.getParent().do_push_state) {
733             state["view_type"] = this.active_view;
734             this.getParent().do_push_state(state);
735         }
736     },
737     do_load_state: function(state, warm) {
738         var self = this,
739             defs = [];
740         if (state.view_type && state.view_type !== this.active_view) {
741             defs.push(
742                 this.views[this.active_view].deferred.pipe(function() {
743                     return self.on_mode_switch(state.view_type, true);
744                 })
745             );
746         } 
747
748         $.when(defs).then(function() {
749             self.views[self.active_view].controller.do_load_state(state, warm);
750         });
751     },
752     display_title: function () {
753         var am = this.getParent();
754         if (am) {
755             return am.bread_crumb.get_title();
756         } else {
757             return _.escape(this.action.name);
758         }
759     }
760 });
761
762 instance.web.Sidebar = instance.web.Widget.extend({
763     init: function(parent) {
764         var self = this;
765         this._super(parent);
766         var view = this.getParent();
767         this.sections = [
768             { 'name' : 'print', 'label' : _t('Print'), },
769             { 'name' : 'files', 'label' : _t('Attachment'), },
770             { 'name' : 'other', 'label' : _t('More'), }
771         ];
772         this.items = {
773             'print' : [],
774             'files' : [],
775             'other' : []
776         };
777         if (this.session.uid === 1) {
778             var item = { label: _t("Translate"), callback: view.on_sidebar_translate, title: _t("Technical translation") };
779             this.items.other.push(item);
780         }
781         this.fileupload_id = _.uniqueId('oe_fileupload');
782         $(window).on(this.fileupload_id, function() {
783             var args = [].slice.call(arguments).slice(1);
784             if (args[0] && args[0].error) {
785                 alert(args[0].error);
786             } else {
787                 self.do_attachement_update(self.dataset, self.model_id);
788             }
789             $.unblockUI();
790         });
791     },
792     start: function() {
793         var self = this;
794         this._super(this);
795         this.redraw();
796         this.$element.on('click','.oe_dropdown_menu li a', function(event) {
797             var section = $(this).data('section');
798             var index = $(this).data('index');
799             var item = self.items[section][index];
800             if (item.callback) {
801                 item.callback.apply(self, [item]);
802             } else if (item.action) {
803                 self.on_item_action_clicked(item);
804             } else if (item.url) {
805                 return true;
806             }
807             event.preventDefault();
808         });
809     },
810     redraw: function() {
811         var self = this;
812         self.$element.html(QWeb.render('Sidebar', {widget: self}));
813
814         // Hides Sidebar sections when item list is empty
815         this.$('.oe_form_dropdown_section').each(function() {
816             $(this).toggle(!!$(this).find('li').length);
817         });
818     },
819     /**
820      * For each item added to the section:
821      *
822      * ``label``
823      *     will be used as the item's name in the sidebar, can be html
824      *
825      * ``action``
826      *     descriptor for the action which will be executed, ``action`` and
827      *     ``callback`` should be exclusive
828      *
829      * ``callback``
830      *     function to call when the item is clicked in the sidebar, called
831      *     with the item descriptor as its first argument (so information
832      *     can be stored as additional keys on the object passed to
833      *     ``add_items``)
834      *
835      * ``classname`` (optional)
836      *     ``@class`` set on the sidebar serialization of the item
837      *
838      * ``title`` (optional)
839      *     will be set as the item's ``@title`` (tooltip)
840      *
841      * @param {String} section_code
842      * @param {Array<{label, action | callback[, classname][, title]}>} items
843      */
844     add_items: function(section_code, items) {
845         var self = this;
846         if (items) {
847             this.items[section_code].push.apply(this.items[section_code],items);
848             this.redraw();
849         }
850     },
851     add_toolbar: function(toolbar) {
852         var self = this;
853         _.each(['print','action','relate'], function(type) {
854             var items = toolbar[type];
855             if (items) {
856                 for (var i = 0; i < items.length; i++) {
857                     items[i] = {
858                         label: items[i]['name'],
859                         action: items[i],
860                         classname: 'oe_sidebar_' + type
861                     }
862                 }
863                 self.add_items(type=='print' ? 'print' : 'other', items);
864             }
865         });
866     },
867     on_item_action_clicked: function(item) {
868         var self = this;
869         self.getParent().sidebar_context().then(function (context) {
870             var ids = self.getParent().get_selected_ids();
871             if (ids.length == 0) {
872                 instance.web.dialog($("<div />").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true });
873                 return false;
874             }
875             var additional_context = _.extend({
876                 active_id: ids[0],
877                 active_ids: ids,
878                 active_model: self.getParent().dataset.model
879             }, context);
880             self.rpc("/web/action/load", {
881                 action_id: item.action.id,
882                 context: additional_context
883             }, function(result) {
884                 result.result.context = _.extend(result.result.context || {},
885                     additional_context);
886                 result.result.flags = result.result.flags || {};
887                 result.result.flags.new_window = true;
888                 self.do_action(result.result, function () {
889                     // reload view
890                     self.getParent().reload();
891                 });
892             });
893         });
894     },
895     do_attachement_update: function(dataset, model_id) {
896         this.dataset = dataset;
897         this.model_id = model_id;
898         if (!model_id) {
899             this.on_attachments_loaded([]);
900         } else {
901             var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
902             var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
903             ds.read_slice(['name', 'url', 'type'], {}).then(this.on_attachments_loaded);
904         }
905     },
906     on_attachments_loaded: function(attachments) {
907         var self = this;
908         var items = [];
909         var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id=';
910         _.each(attachments,function(a) {
911             a.label = a.name;
912             if(a.type === "binary") {
913                 a.url = prefix  + a.id + '&t=' + (new Date().getTime());
914             }
915         });
916         self.items['files'] = attachments;
917         self.redraw();
918         this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
919         this.$element.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
920     },
921     on_attachment_changed: function(e) {
922         var $e = $(e.target);
923         if ($e.val() !== '') {
924             this.$element.find('form.oe_form_binary_form').submit();
925             $e.parent().find('input[type=file]').prop('disabled', true);
926             $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
927             this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...'));
928             $.blockUI();
929         }
930     },
931     on_attachment_delete: function(e) {
932         var self = this;
933         e.preventDefault();
934         e.stopPropagation();
935         var self = this;
936         var $e = $(e.currentTarget);
937         if (confirm(_t("Do you really want to delete this attachment ?"))) {
938             (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).then(function() {
939                 self.do_attachement_update(self.dataset, self.model_id);
940             });
941         }
942     }
943 });
944
945 instance.web.TranslateDialog = instance.web.Dialog.extend({
946     dialog_title: {toString: function () { return _t("Translations"); }},
947     init: function(view) {
948         // TODO fme: should add the language to fields_view_get because between the fields view get
949         // and the moment the user opens the translation dialog, the user language could have been changed
950         this.view_language = view.session.user_context.lang;
951         this['on_button_' + _t("Save")] = this.on_btn_save;
952         this['on_button_' + _t("Close")] = this.on_btn_close;
953         this._super(view, {
954             width: '80%',
955             height: '80%'
956         });
957         this.view = view;
958         this.view_type = view.fields_view.type || '';
959         this.$fields_form = null;
960         this.$view_form = null;
961         this.$sidebar_form = null;
962         this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name });
963         this.languages = null;
964         this.languages_loaded = $.Deferred();
965         (new instance.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(),
966             [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }).then(this.on_languages_loaded);
967     },
968     start: function() {
969         var self = this;
970         this._super();
971         $.when(this.languages_loaded).then(function() {
972             self.$element.html(instance.web.qweb.render('TranslateDialog', { widget: self }));
973             self.$fields_form = self.$element.find('.oe_translation_form');
974             self.$fields_form.find('.oe_trad_field').change(function() {
975                 $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value')));
976             });
977         });
978         return this;
979     },
980     on_languages_loaded: function(langs) {
981         this.languages = langs;
982         this.languages_loaded.resolve();
983     },
984     do_load_fields_values: function(callback) {
985         var self = this,
986             deffered = [];
987         this.$fields_form.find('.oe_trad_field').val('').removeClass('touched');
988         _.each(self.languages, function(lg) {
989             var deff = $.Deferred();
990             deffered.push(deff);
991             var callback = function(values) {
992                 _.each(self.translatable_fields_keys, function(f) {
993                     self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || '');
994                 });
995                 deff.resolve();
996             };
997             if (lg.code === self.view_language) {
998                 var values = {};
999                 _.each(self.translatable_fields_keys, function(field) {
1000                     values[field] = self.view.fields[field].get_value();
1001                 });
1002                 callback([values]);
1003             } else {
1004                 self.rpc('/web/dataset/get', {
1005                     model: self.view.dataset.model,
1006                     ids: [self.view.datarecord.id],
1007                     fields: self.translatable_fields_keys,
1008                     context: self.view.dataset.get_context({
1009                         'lang': lg.code
1010                     })}, callback);
1011             }
1012         });
1013         $.when.apply(null, deffered).then(callback);
1014     },
1015     open: function(field) {
1016         var self = this,
1017             sup = this._super;
1018         $.when(this.languages_loaded).then(function() {
1019             if (self.view.translatable_fields && self.view.translatable_fields.length) {
1020                 self.do_load_fields_values(function() {
1021                     sup.call(self);
1022                     if (field) {
1023                         var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child');
1024                         self.$element.scrollTo($field_input);
1025                         $field_input.focus();
1026                     }
1027                 });
1028             } else {
1029                 sup.call(self);
1030             }
1031         });
1032     },
1033     on_btn_save: function() {
1034         var trads = {},
1035             self = this,
1036             trads_mutex = new $.Mutex();
1037         self.$fields_form.find('.oe_trad_field.touched').each(function() {
1038             var field = $(this).attr('name').split('-');
1039             if (!trads[field[0]]) {
1040                 trads[field[0]] = {};
1041             }
1042             trads[field[0]][field[1]] = $(this).val();
1043         });
1044         _.each(trads, function(data, code) {
1045             if (code === self.view_language) {
1046                 _.each(data, function(value, field) {
1047                     self.view.fields[field].set_value(value);
1048                 });
1049             }
1050             trads_mutex.exec(function() {
1051                 return self.view.dataset.write(self.view.datarecord.id, data, { context : { 'lang': code } });
1052             });
1053         });
1054         this.close();
1055     },
1056     on_btn_close: function() {
1057         this.close();
1058     }
1059 });
1060
1061 instance.web.View = instance.web.Widget.extend({
1062     template: "EmptyComponent",
1063     // name displayed in view switchers
1064     display_name: '',
1065     /**
1066      * Define a view type for each view to allow automatic call to fields_view_get.
1067      */
1068     view_type: undefined,
1069     init: function(parent, dataset, view_id, options) {
1070         this._super(parent);
1071         this.dataset = dataset;
1072         this.view_id = view_id;
1073         this.set_default_options(options);
1074     },
1075     start: function() {
1076         return this.load_view();
1077     },
1078     load_view: function() {
1079         if (this.embedded_view) {
1080             var def = $.Deferred();
1081             var self = this;
1082             $.async_when().then(function() {def.resolve(self.embedded_view);});
1083             return def.pipe(this.on_loaded);
1084         } else {
1085             var context = new instance.web.CompoundContext(this.dataset.get_context());
1086             if (! this.view_type)
1087                 console.warn("view_type is not defined", this);
1088             return this.rpc("/web/view/load", {
1089                 "model": this.dataset.model,
1090                 "view_id": this.view_id,
1091                 "view_type": this.view_type,
1092                 toolbar: !!this.options.$sidebar,
1093                 context: context
1094                 }).pipe(this.on_loaded);
1095         }
1096     },
1097     /**
1098      * Called after a successful call to fields_view_get.
1099      * Must return a promise.
1100      */
1101     on_loaded: function(fields_view_get) {
1102     },
1103     set_default_options: function(options) {
1104         this.options = options || {};
1105         _.defaults(this.options, {
1106             // All possible views options should be defaulted here
1107             $sidebar: null,
1108             sidebar_id: null,
1109             action: null,
1110             action_views_ids: {}
1111         });
1112     },
1113     open_translate_dialog: function(field) {
1114         if (!this.translate_dialog) {
1115             this.translate_dialog = new instance.web.TranslateDialog(this).start();
1116         }
1117         this.translate_dialog.open(field);
1118     },
1119     /**
1120      * Fetches and executes the action identified by ``action_data``.
1121      *
1122      * @param {Object} action_data the action descriptor data
1123      * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
1124      * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
1125      * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
1126      * @param {Object} [action_data.context=null] additional action context, to add to the current context
1127      * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
1128      * @param {Object} [record_id] the identifier of the object on which the action is to be applied
1129      * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
1130      */
1131     do_execute_action: function (action_data, dataset, record_id, on_closed) {
1132         var self = this;
1133         var result_handler = function () {
1134             if (on_closed) { on_closed.apply(null, arguments); }
1135             if (self.getParent() && self.getParent().on_action_executed) {
1136                 return self.getParent().on_action_executed.apply(null, arguments);
1137             }
1138         };
1139         var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
1140
1141         var handler = function (r) {
1142             var action = r.result;
1143             if (action && action.constructor == Object) {
1144                 var ncontext = new instance.web.CompoundContext(context);
1145                 if (record_id) {
1146                     ncontext.add({
1147                         active_id: record_id,
1148                         active_ids: [record_id],
1149                         active_model: dataset.model
1150                     });
1151                 }
1152                 ncontext.add(action.context || {});
1153                 return self.rpc('/web/session/eval_domain_and_context', {
1154                     contexts: [ncontext],
1155                     domains: []
1156                 }).pipe(function (results) {
1157                     action.context = results.context;
1158                     /* niv: previously we were overriding once more with action_data.context,
1159                      * I assumed this was not a correct behavior and removed it
1160                      */
1161                     return self.do_action(action, result_handler);
1162                 }, null);
1163             } else {
1164                 return result_handler();
1165             }
1166         };
1167
1168         if (action_data.special) {
1169             return handler({result: {"type":"ir.actions.act_window_close"}});
1170         } else if (action_data.type=="object") {
1171             var args = [[record_id]], additional_args = [];
1172             if (action_data.args) {
1173                 try {
1174                     // Warning: quotes and double quotes problem due to json and xml clash
1175                     // Maybe we should force escaping in xml or do a better parse of the args array
1176                     additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
1177                     args = args.concat(additional_args);
1178                 } catch(e) {
1179                     console.error("Could not JSON.parse arguments", action_data.args);
1180                 }
1181             }
1182             args.push(context);
1183             return dataset.call_button(action_data.name, args, handler);
1184         } else if (action_data.type=="action") {
1185             return this.rpc('/web/action/load', { action_id: parseInt(action_data.name, 10), context: context, do_not_eval: true}, handler);
1186         } else  {
1187             return dataset.exec_workflow(record_id, action_data.name, handler);
1188         }
1189     },
1190     /**
1191      * Directly set a view to use instead of calling fields_view_get. This method must
1192      * be called before start(). When an embedded view is set, underlying implementations
1193      * of instance.web.View must use the provided view instead of any other one.
1194      *
1195      * @param embedded_view A view.
1196      */
1197     set_embedded_view: function(embedded_view) {
1198         this.embedded_view = embedded_view;
1199     },
1200     do_show: function () {
1201         this.$element.show();
1202     },
1203     do_hide: function () {
1204         this.$element.hide();
1205     },
1206     do_push_state: function(state) {
1207         if (this.getParent() && this.getParent().do_push_state) {
1208             this.getParent().do_push_state(state);
1209         }
1210     },
1211     do_load_state: function(state, warm) {
1212     },
1213     /**
1214      * Switches to a specific view type
1215      *
1216      * @param {String} view view type to switch to
1217      */
1218     do_switch_view: function(view) { 
1219     },
1220     /**
1221      * Cancels the switch to the current view, switches to the previous one
1222      *
1223      * @param {Object} [options]
1224      * @param {Boolean} [options.created=false] resource was created
1225      * @param {String} [options.default=null] view to switch to if no previous view
1226      */
1227     do_prev_view: function (options) {
1228     },
1229     do_search: function(view) {
1230     },
1231     on_sidebar_import: function() {
1232         var import_view = new instance.web.DataImport(this, this.dataset);
1233         import_view.start();
1234     },
1235     on_sidebar_export: function() {
1236         var export_view = new instance.web.DataExport(this, this.dataset);
1237         export_view.start();
1238     },
1239     on_sidebar_translate: function() {
1240         return this.do_action({
1241             res_model : 'ir.translation',
1242             domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
1243             views: [[false, 'list'], [false, 'form']],
1244             type : 'ir.actions.act_window',
1245             view_type : "list",
1246             view_mode : "list"
1247         });
1248     },
1249     sidebar_context: function () {
1250         return $.when();
1251     },
1252     /**
1253      * Asks the view to reload itself, if the reloading is asynchronous should
1254      * return a {$.Deferred} indicating when the reloading is done.
1255      */
1256     reload: function () {
1257         return $.when();
1258     }
1259 });
1260
1261 instance.web.xml_to_json = function(node) {
1262     switch (node.nodeType) {
1263         case 3:
1264         case 4:
1265             return node.data;
1266         break;
1267         case 1:
1268             var attrs = $(node).getAttributes();
1269             _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
1270                 if (attrs[key]) {
1271                     try {
1272                         attrs[key] = JSON.parse(attrs[key]);
1273                     } catch(e) { }
1274                 }
1275             });
1276             return {
1277                 tag: node.tagName.toLowerCase(),
1278                 attrs: attrs,
1279                 children: _.map(node.childNodes, instance.web.xml_to_json)
1280             }
1281     }
1282 }
1283 instance.web.json_node_to_xml = function(node, human_readable, indent) {
1284     // For debugging purpose, this function will convert a json node back to xml
1285     // Maybe useful for xml view editor
1286     indent = indent || 0;
1287     var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
1288         r = sindent + '<' + node.tag,
1289         cr = human_readable ? '\n' : '';
1290
1291     if (typeof(node) === 'string') {
1292         return sindent + node;
1293     } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
1294         throw("Node a json node");
1295     }
1296     for (var attr in node.attrs) {
1297         var vattr = node.attrs[attr];
1298         if (typeof(vattr) !== 'string') {
1299             // domains, ...
1300             vattr = JSON.stringify(vattr);
1301         }
1302         vattr = vattr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1303         if (human_readable) {
1304             vattr = vattr.replace(/&quot;/g, "'");
1305         }
1306         r += ' ' + attr + '="' + vattr + '"';
1307     }
1308     if (node.children && node.children.length) {
1309         r += '>' + cr;
1310         var childs = [];
1311         for (var i = 0, ii = node.children.length; i < ii; i++) {
1312             childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1));
1313         }
1314         r += childs.join(cr);
1315         r += cr + sindent + '</' + node.tag + '>';
1316         return r;
1317     } else {
1318         return r + '/>';
1319     }
1320 }
1321 instance.web.xml_to_str = function(node) {
1322     if (window.ActiveXObject) {
1323         return node.xml;
1324     } else {
1325         return (new XMLSerializer()).serializeToString(node);
1326     }
1327 }
1328 instance.web.str_to_xml = function(s) {
1329     if (window.DOMParser) {
1330         var dp = new DOMParser();
1331         var r = dp.parseFromString(s, "text/xml");
1332         if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
1333             throw new Error("Could not parse string to xml");
1334         }
1335         return r;
1336     }
1337     var xDoc;
1338     try {
1339         xDoc = new ActiveXObject("MSXML2.DOMDocument");
1340     } catch (e) {
1341         throw new Error("Could not find a DOM Parser: " + e.message);
1342     }
1343     xDoc.async = false;
1344     xDoc.preserveWhiteSpace = true;
1345     xDoc.loadXML(s);
1346     return xDoc;
1347 }
1348
1349 /**
1350  * Registry for all the main views
1351  */
1352 instance.web.views = new instance.web.Registry();
1353
1354 };
1355
1356 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: