1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
5 openerp.web.views = function(instance) {
6 var QWeb = instance.web.qweb,
9 instance.web.ActionManager = instance.web.Widget.extend({
10 init: function(parent) {
12 this.inner_action = null;
13 this.inner_viewmanager = null;
15 this.dialog_viewmanager = null;
16 this.client_widget = null;
19 this._super.apply(this, arguments);
20 this.bread_crumb = new instance.web.BreadCrumb(this);
22 dialog_stop: function () {
24 this.dialog_viewmanager.destroy();
25 this.dialog_viewmanager = null;
26 this.dialog.destroy();
30 content_stop: function () {
31 if (this.inner_viewmanager) {
32 this.inner_viewmanager.destroy();
33 this.inner_viewmanager = null;
35 if (this.client_widget) {
36 this.client_widget.destroy();
37 this.client_widget = null;
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;
49 this.getParent().do_push_state(state);
52 do_load_state: function(state, warm) {
55 if (state.action_id) {
56 var run_action = (!this.inner_viewmanager) || this.inner_viewmanager.action.id !== state.action_id;
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);
64 } else if (state.model && state.id) {
65 // TODO handle context & domain ?
68 res_model: state.model,
70 type: 'ir.actions.act_window',
71 views: [[false, 'form']]
73 action_loaded = this.do_action(action);
74 } else if (state.sa) {
75 // load session action
78 action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).pipe(function(action) {
80 return self.do_action(action);
83 } else if (state.client_action) {
85 var action = state.client_action;
86 if(_.isString(action)) {
92 this.ir_actions_client(action);
95 $.when(action_loaded || null).then(function() {
96 if (self.inner_viewmanager) {
97 self.inner_viewmanager.do_load_state(state, warm);
101 do_action: function(action, on_close) {
102 if (_.isNumber(action)) {
104 return self.rpc("/web/action/load", { action_id: action }, function(result) {
105 self.do_action(result.result, on_close);
109 console.error("No type for action", action);
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);
127 return this[type](action, on_close);
129 null_action: function() {
133 ir_actions_act_window: function (action, on_close) {
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);
142 if (action.target === 'new') {
143 if (this.dialog === null) {
144 this.dialog = new instance.web.Dialog(this, { width: '80%' });
146 this.dialog.on_close.add(on_close);
148 this.dialog_viewmanager.destroy();
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);
157 //this.content_stop();
158 this.bread_crumb.hide_all();
160 return this.getParent().do_action(action, function () {
161 instance.webclient.menu.open_menu(action.menu_id);
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,
175 ir_actions_act_window_close: function (action, on_closed) {
176 if (!this.dialog && on_closed) {
181 ir_actions_server: function (action, on_closed) {
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)
190 ir_actions_client: function (action) {
193 var ClientWidget = instance.web.client_actions.get_object(action.tag);
194 (this.client_widget = new ClientWidget(this, action.params)).appendTo(this.$element);
196 ir_actions_report_xml: function(action, on_closed) {
199 self.rpc("/web/session/eval_domain_and_context", {
200 contexts: [action.context],
202 }).then(function(res) {
203 action = _.clone(action);
204 action.context = res.context;
205 self.session.get_file({
207 data: {action: JSON.stringify(action)},
208 complete: $.unblockUI,
210 if (!self.dialog && on_closed) {
215 error: instance.webclient.crashmanager.on_rpc_error
219 ir_actions_act_url: function (action) {
220 window.open(action.url, action.target === 'self' ? '_self' : '_blank');
222 ir_ui_menu: function (action) {
223 this.getParent().do_action(action);
227 instance.web.BreadCrumb = instance.web.CallbackEnabled.extend({
228 init: function(parent) {
230 this.action_manager = parent;
232 this.action_manager.$element.on('click', '.oe_breadcrumb_item', this.on_item_clicked);
234 add: function(item) {
235 this.items.push(item);
237 hide_all: function() {
238 _.each(this.items, function(i) {
239 i.view_manager.$element.hide();
242 get_title: function() {
243 return QWeb.render('BreadCrumb', { widget: this });
245 on_item_clicked: function(ev) {
246 var $e = $(ev.target);
247 var index = $e.data('index');
248 this.select_item(index);
250 select_item: function(index) {
251 for (var i = index + 1; i < this.items.length; i += 1) {
254 this.items[index].view_manager.$element.show();
256 remove_item: function(index) {
257 var item = this.items.splice(index, 1)[0];
259 item.view_manager.destroy();
264 instance.web.ViewManager = instance.web.Widget.extend({
265 template: "ViewManager",
266 init: function(parent, dataset, views, flags) {
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);
278 label: View ? View.prototype.display_name : (void 'nope')
285 this.flags = flags || {};
286 this.registry = instance.web.views;
287 this.views_history = [];
290 * @returns {jQuery.Deferred} initial view loading promise
295 this.$element.find('.oe_view_manager_switch a').click(function() {
296 self.on_mode_switch($(this).data('view-type'));
299 _.each(this.views_src, function(view) {
300 self.views[view.view_type] = $.extend({}, view, {
301 deferred : $.Deferred(),
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 || {})
311 views_ids[view.view_type] = view.view_id;
313 if (this.flags.views_switcher === false) {
314 this.$element.find('.oe_view_manager_switch').hide();
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);
321 * Asks the view manager to switch visualization mode.
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
327 on_mode_switch: function(view_type, no_store, view_options) {
329 var view = this.views[view_type];
332 return $.Deferred().reject();
335 this.views_history.push(view_type);
337 this.active_view = view_type;
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) {
347 options.initial_mode = 'edit';
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);
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);
364 && self.flags.auto_search
365 && view.controller.searchable !== false) {
366 self.searchview.ready.then(self.searchview.do_search);
369 } else if (this.searchview
370 && self.flags.auto_search
371 && view.controller.searchable !== false) {
372 this.searchview.ready.then(this.searchview.do_search);
375 if (this.searchview) {
376 this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show']();
380 .find('.oe_view_manager_switch a').parent().removeClass('active');
382 .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
383 .parent().addClass('active');
385 $.when(view_promise).then(function () {
386 _.each(_.keys(self.views), function(view_name) {
387 var controller = self.views[view_name].controller;
389 var container = self.$element.find(".oe_view_manager_view_" + view_name + ":first");
390 if (view_name === view_type) {
392 controller.do_show(view_options || {});
395 controller.do_hide();
400 self.$element.find('.oe_view_title_text:first').html(
401 self.display_title());
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.
410 switch_view: function(view_type, no_store, options) {
411 return this.on_mode_switch(view_type, no_store, options);
414 * Returns to the view preceding the caller view in this manager's
415 * navigation history (the navigation history is appended to via
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
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');
435 return this.on_mode_switch(previous_view, true);
438 * Sets up the current viewmanager's search view.
440 * @param {Number|false} view_id the view to use or false for a default one
441 * @returns {jQuery.Deferred} search view startup deferred
443 setup_search_view: function(view_id, search_defaults) {
445 if (this.searchview) {
446 this.searchview.destroy();
448 this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, this.flags.search_view === false);
450 this.searchview.on_search.add(this.do_searchview_search);
451 return this.searchview.appendTo(this.$element.find(".oe_view_manager_view_search"));
453 do_searchview_search: function(domains, contexts, groupbys) {
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
466 : action_context.group_by;
467 if (_.isString(groupby)) {
470 controller.do_search(results.domain, results.context, groupby || []);
474 * Event launched when a controller has been inited.
476 * @param {String} view_type type of view
477 * @param {String} view the inited controller
479 on_controller_inited: function(view_type, view) {
482 * Called when one of the view want to execute an action
484 on_action: function(action) {
486 on_create: function() {
488 on_remove: function() {
490 on_edit: function() {
493 * Called by children view after executing an action
495 on_action_executed: function () {
497 display_title: function () {
498 var view = this.views[this.active_view];
501 return view.controller.fields_view.arch.attrs.string;
507 instance.web.ViewManagerAction = instance.web.ViewManager.extend({
508 template:"ViewManagerAction",
510 * @constructs instance.web.ViewManagerAction
511 * @extends instance.web.ViewManager
513 * @param {instance.web.ActionManager} parent parent object/widget
514 * @param {Object} action descriptor for the action this viewmanager needs to manage its views.
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;
524 if (action.res_model == 'board.board' && action.view_mode === 'form') {
525 // Special case for Dashboards
527 views_switcher : false,
528 display_title : false,
532 action_buttons : false
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);
540 dataset.ids.push(action.res_id);
543 this.dataset = dataset;
545 // setup storage for session-wise menu hiding
546 if (this.session.hidden_menutips) {
549 this.session.hidden_menutips = {}
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.
560 search_defaults = {};
561 _.each(this.action.context, function (value, key) {
562 var match = /^search_default_(.*)$/.exec(key);
564 search_defaults[match[1]] = value;
568 var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
570 searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
572 var main_view_loaded = this._super();
574 var manager_ready = $.when(searchview_loaded, main_view_loaded);
576 this.$element.find('.oe_debug_view').change(this.on_debug_changed);
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() {
583 //noinspection FallthroughInSwitchStatementJS
584 switch ($this.attr('name')) {
586 Users.write(self.session.uid, {menu_tips:false});
588 $this.closest('blockquote').hide();
589 self.session.hidden_menutips[self.action.id] = true;
592 if (!(self.action.id in self.session.hidden_menutips)) {
593 Users.read_ids([this.session.uid], ['menu_tips']).then(function(users) {
595 if (!(user && user.id === self.session.uid)) {
598 $tips.find('blockquote').toggle(user.menu_tips);
603 return manager_ready;
605 on_debug_changed: function (evt) {
607 $sel = $(evt.currentTarget),
608 $option = $sel.find('option:selected'),
610 current_view = this.views[this.active_view].controller;
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);
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),
623 }, QWeb.render('ViewManagerDebugViewLog', {
625 format : instance.web.format_value
630 case 'toggle_layout_outline':
631 current_view.rendering_engine.toggle_layout_debugging();
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);
646 .append($('<dt>').text(name))
647 .append($('<dd style="white-space: pre-wrap;">').text(def));
650 new instance.web.Dialog(self, {
651 title: _.str.sprintf(_t("Model %s fields"),
653 width: '95%'}, $root).open();
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);
661 this.do_warn(_t("Manage Views"),
662 _t("Could not find current view declaration"));
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',
676 this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
678 case 'manage_filters':
680 res_model: 'ir.filters',
681 views: [[false, 'list'], [false, 'form']],
682 type: 'ir.actions.act_window',
684 search_default_my_filters: true,
685 search_default_model_id: this.dataset.model
691 console.log("No debug handler for ", val);
694 evt.currentTarget.selectedIndex = 0;
696 do_edit_resource: function(model, id, action) {
697 var action = _.extend({
700 type : 'ir.actions.act_window',
703 views : [[false, 'form']],
706 action_buttons : true,
708 resize_textareas : true
712 this.do_action(action);
714 on_mode_switch: function (view_type, no_store, options) {
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', {
725 if (!self.action.name && fvg) {
726 self.$element.find('.oe_view_title_text').text(fvg.arch.attrs.string || fvg.name);
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);
737 do_load_state: function(state, warm) {
740 if (state.view_type && state.view_type !== this.active_view) {
742 this.views[this.active_view].deferred.pipe(function() {
743 return self.on_mode_switch(state.view_type, true);
748 $.when(defs).then(function() {
749 self.views[self.active_view].controller.do_load_state(state, warm);
752 display_title: function () {
753 var am = this.getParent();
755 return am.bread_crumb.get_title();
757 return _.escape(this.action.name);
762 instance.web.Sidebar = instance.web.Widget.extend({
763 init: function(parent) {
766 var view = this.getParent();
768 { 'name' : 'print', 'label' : _t('Print'), },
769 { 'name' : 'files', 'label' : _t('Attachment'), },
770 { 'name' : 'other', 'label' : _t('More'), }
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);
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);
787 self.do_attachement_update(self.dataset, self.model_id);
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];
801 item.callback.apply(self, [item]);
802 } else if (item.action) {
803 self.on_item_action_clicked(item);
804 } else if (item.url) {
807 event.preventDefault();
812 self.$element.html(QWeb.render('Sidebar', {widget: self}));
814 // Hides Sidebar sections when item list is empty
815 this.$('.oe_form_dropdown_section').each(function() {
816 $(this).toggle(!!$(this).find('li').length);
820 * For each item added to the section:
823 * will be used as the item's name in the sidebar, can be html
826 * descriptor for the action which will be executed, ``action`` and
827 * ``callback`` should be exclusive
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
835 * ``classname`` (optional)
836 * ``@class`` set on the sidebar serialization of the item
838 * ``title`` (optional)
839 * will be set as the item's ``@title`` (tooltip)
841 * @param {String} section_code
842 * @param {Array<{label, action | callback[, classname][, title]}>} items
844 add_items: function(section_code, items) {
847 this.items[section_code].push.apply(this.items[section_code],items);
851 add_toolbar: function(toolbar) {
853 _.each(['print','action','relate'], function(type) {
854 var items = toolbar[type];
856 for (var i = 0; i < items.length; i++) {
858 label: items[i]['name'],
860 classname: 'oe_sidebar_' + type
863 self.add_items(type=='print' ? 'print' : 'other', items);
867 on_item_action_clicked: function(item) {
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 });
875 var additional_context = _.extend({
878 active_model: self.getParent().dataset.model
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 || {},
886 result.result.flags = result.result.flags || {};
887 result.result.flags.new_window = true;
888 self.do_action(result.result, function () {
890 self.getParent().reload();
895 do_attachement_update: function(dataset, model_id) {
896 this.dataset = dataset;
897 this.model_id = model_id;
899 this.on_attachments_loaded([]);
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);
906 on_attachments_loaded: function(attachments) {
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) {
912 if(a.type === "binary") {
913 a.url = prefix + a.id + '&t=' + (new Date().getTime());
916 self.items['files'] = attachments;
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);
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...'));
931 on_attachment_delete: function(e) {
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);
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;
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);
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')));
980 on_languages_loaded: function(langs) {
981 this.languages = langs;
982 this.languages_loaded.resolve();
984 do_load_fields_values: function(callback) {
987 this.$fields_form.find('.oe_trad_field').val('').removeClass('touched');
988 _.each(self.languages, function(lg) {
989 var deff = $.Deferred();
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] || '');
997 if (lg.code === self.view_language) {
999 _.each(self.translatable_fields_keys, function(field) {
1000 values[field] = self.view.fields[field].get_value();
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({
1013 $.when.apply(null, deffered).then(callback);
1015 open: function(field) {
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() {
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();
1033 on_btn_save: function() {
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]] = {};
1042 trads[field[0]][field[1]] = $(this).val();
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);
1050 trads_mutex.exec(function() {
1051 return self.view.dataset.write(self.view.datarecord.id, data, { context : { 'lang': code } });
1056 on_btn_close: function() {
1061 instance.web.View = instance.web.Widget.extend({
1062 template: "EmptyComponent",
1063 // name displayed in view switchers
1066 * Define a view type for each view to allow automatic call to fields_view_get.
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);
1076 return this.load_view();
1078 load_view: function() {
1079 if (this.embedded_view) {
1080 var def = $.Deferred();
1082 $.async_when().then(function() {def.resolve(self.embedded_view);});
1083 return def.pipe(this.on_loaded);
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,
1094 }).pipe(this.on_loaded);
1098 * Called after a successful call to fields_view_get.
1099 * Must return a promise.
1101 on_loaded: function(fields_view_get) {
1103 set_default_options: function(options) {
1104 this.options = options || {};
1105 _.defaults(this.options, {
1106 // All possible views options should be defaulted here
1110 action_views_ids: {}
1113 open_translate_dialog: function(field) {
1114 if (!this.translate_dialog) {
1115 this.translate_dialog = new instance.web.TranslateDialog(this).start();
1117 this.translate_dialog.open(field);
1120 * Fetches and executes the action identified by ``action_data``.
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)
1131 do_execute_action: function (action_data, dataset, record_id, on_closed) {
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);
1139 var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
1141 var handler = function (r) {
1142 var action = r.result;
1143 if (action && action.constructor == Object) {
1144 var ncontext = new instance.web.CompoundContext(context);
1147 active_id: record_id,
1148 active_ids: [record_id],
1149 active_model: dataset.model
1152 ncontext.add(action.context || {});
1153 return self.rpc('/web/session/eval_domain_and_context', {
1154 contexts: [ncontext],
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
1161 return self.do_action(action, result_handler);
1164 return result_handler();
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) {
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);
1179 console.error("Could not JSON.parse arguments", action_data.args);
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);
1187 return dataset.exec_workflow(record_id, action_data.name, handler);
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.
1195 * @param embedded_view A view.
1197 set_embedded_view: function(embedded_view) {
1198 this.embedded_view = embedded_view;
1200 do_show: function () {
1201 this.$element.show();
1203 do_hide: function () {
1204 this.$element.hide();
1206 do_push_state: function(state) {
1207 if (this.getParent() && this.getParent().do_push_state) {
1208 this.getParent().do_push_state(state);
1211 do_load_state: function(state, warm) {
1214 * Switches to a specific view type
1216 * @param {String} view view type to switch to
1218 do_switch_view: function(view) {
1221 * Cancels the switch to the current view, switches to the previous one
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
1227 do_prev_view: function (options) {
1229 do_search: function(view) {
1231 on_sidebar_import: function() {
1232 var import_view = new instance.web.DataImport(this, this.dataset);
1233 import_view.start();
1235 on_sidebar_export: function() {
1236 var export_view = new instance.web.DataExport(this, this.dataset);
1237 export_view.start();
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',
1249 sidebar_context: function () {
1253 * Asks the view to reload itself, if the reloading is asynchronous should
1254 * return a {$.Deferred} indicating when the reloading is done.
1256 reload: function () {
1261 instance.web.xml_to_json = function(node) {
1262 switch (node.nodeType) {
1268 var attrs = $(node).getAttributes();
1269 _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
1272 attrs[key] = JSON.parse(attrs[key]);
1277 tag: node.tagName.toLowerCase(),
1279 children: _.map(node.childNodes, instance.web.xml_to_json)
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' : '';
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");
1296 for (var attr in node.attrs) {
1297 var vattr = node.attrs[attr];
1298 if (typeof(vattr) !== 'string') {
1300 vattr = JSON.stringify(vattr);
1302 vattr = vattr.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
1303 if (human_readable) {
1304 vattr = vattr.replace(/"/g, "'");
1306 r += ' ' + attr + '="' + vattr + '"';
1308 if (node.children && node.children.length) {
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));
1314 r += childs.join(cr);
1315 r += cr + sindent + '</' + node.tag + '>';
1321 instance.web.xml_to_str = function(node) {
1322 if (window.ActiveXObject) {
1325 return (new XMLSerializer()).serializeToString(node);
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");
1339 xDoc = new ActiveXObject("MSXML2.DOMDocument");
1341 throw new Error("Could not find a DOM Parser: " + e.message);
1344 xDoc.preserveWhiteSpace = true;
1350 * Registry for all the main views
1352 instance.web.views = new instance.web.Registry();
1356 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: