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_widget = null;
15
this.dialog_widget = null;
16
this.breadcrumbs = [];
17
this.on('history_back', this, function() {
18
return this.history_back();
22
this._super.apply(this, arguments);
23
this.$el.on('click', 'a.oe_breadcrumb_item', this.on_breadcrumb_clicked);
25
dialog_stop: function () {
27
this.dialog.destroy();
32
* Add a new item to the breadcrumb
34
* If the title of an item is an array, the multiple title mode is in use.
35
* (eg: a widget with multiple views might need to display a title for each view)
36
* In multiple title mode, the show() callback can check the index it receives
37
* in order to detect which of its titles has been clicked on by the user.
39
* @param {Object} item breadcrumb item
40
* @param {Object} item.widget widget containing the view(s) to be added to the breadcrumb added
41
* @param {Function} [item.show] triggered whenever the widget should be shown back
42
* @param {Function} [item.hide] triggered whenever the widget should be shown hidden
43
* @param {Function} [item.destroy] triggered whenever the widget should be destroyed
44
* @param {String|Array} [item.title] title(s) of the view(s) to be displayed in the breadcrumb
45
* @param {Function} [item.get_title] should return the title(s) of the view(s) to be displayed in the breadcrumb
47
push_breadcrumb: function(item) {
48
var last = this.breadcrumbs.slice(-1)[0];
53
show: function(index) {
54
this.widget.$el.show();
57
this.widget.$el.hide();
60
this.widget.destroy();
62
get_title: function() {
63
return this.title || this.widget.get('title');
66
item.id = _.uniqueId('breadcrumb_');
67
this.breadcrumbs.push(item);
69
history_back: function() {
70
var last = this.breadcrumbs.slice(-1)[0];
74
var title = last.get_title();
75
if (_.isArray(title) && title.length > 1) {
76
return this.select_breadcrumb(this.breadcrumbs.length - 1, title.length - 2);
77
} else if (this.breadcrumbs.length === 1) {
78
// Only one single titled item in breadcrumb, most of the time you want to trigger back to home
81
var prev = this.breadcrumbs[this.breadcrumbs.length - 2];
82
title = prev.get_title();
83
return this.select_breadcrumb(this.breadcrumbs.length - 2, _.isArray(title) ? title.length - 1 : undefined);
86
on_breadcrumb_clicked: function(ev) {
87
var $e = $(ev.target);
88
var id = $e.data('id');
90
for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
91
if (this.breadcrumbs[i].id == id) {
96
var subindex = $e.parent().find('a.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e);
97
this.select_breadcrumb(index, subindex);
99
select_breadcrumb: function(index, subindex) {
100
var next_item = this.breadcrumbs[index + 1];
101
if (next_item && next_item.on_reverse_breadcrumb) {
102
next_item.on_reverse_breadcrumb(this.breadcrumbs[index].widget);
104
for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
106
if (this.remove_breadcrumb(i) === false) {
111
var item = this.breadcrumbs[index];
113
this.inner_widget = item.widget;
114
this.inner_action = item.action;
117
clear_breadcrumbs: function() {
118
for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
119
if (this.remove_breadcrumb(0) === false) {
124
remove_breadcrumb: function(index) {
125
var item = this.breadcrumbs.splice(index, 1)[0];
127
var dups = _.filter(this.breadcrumbs, function(it) {
128
return item.widget === it.widget;
131
if (this.getParent().has_uncommitted_changes()) {
132
this.inner_widget = item.widget;
133
this.inner_action = item.action;
134
this.breadcrumbs.splice(index, 0, item);
141
var last_widget = this.breadcrumbs.slice(-1)[0];
143
this.inner_widget = last_widget.widget;
144
this.inner_action = last_widget.action;
147
get_title: function() {
149
for (var i = 0; i < this.breadcrumbs.length; i += 1) {
150
var item = this.breadcrumbs[i];
151
var tit = item.get_title();
152
if (item.hide_breadcrumb) {
155
if (!_.isArray(tit)) {
158
for (var j = 0; j < tit.length; j += 1) {
159
var label = _.escape(tit[j]);
160
if (i === this.breadcrumbs.length - 1 && j === tit.length - 1) {
161
titles.push(_.str.sprintf('<span class="oe_breadcrumb_item">%s</span>', label));
163
titles.push(_.str.sprintf('<a href="#" class="oe_breadcrumb_item" data-id="%s">%s</a>', item.id, label));
167
return titles.join(' <span class="oe_fade">/</span> ');
169
do_push_state: function(state) {
171
if (this.getParent() && this.getParent().do_push_state) {
172
if (this.inner_action) {
173
if (this.inner_action._push_me === false) {
174
// this action has been explicitly marked as not pushable
177
state['title'] = this.inner_action.name;
178
if(this.inner_action.type == 'ir.actions.act_window') {
179
state['model'] = this.inner_action.res_model;
181
if (this.inner_action.menu_id) {
182
state['menu_id'] = this.inner_action.menu_id;
184
if (this.inner_action.id) {
185
state['action'] = this.inner_action.id;
186
} else if (this.inner_action.type == 'ir.actions.client') {
187
state['action'] = this.inner_action.tag;
189
_.each(this.inner_action.params, function(v, k) {
190
if(_.isString(v) || _.isNumber(v)) {
194
state = _.extend(params || {}, state);
196
if (this.inner_action.context) {
197
var active_id = this.inner_action.context.active_id;
199
state["active_id"] = active_id;
201
var active_ids = this.inner_action.context.active_ids;
202
if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
203
// We don't push active_ids if it's a single element array containing the active_id
204
// This makes the url shorter in most cases.
205
state["active_ids"] = this.inner_action.context.active_ids.join(',');
210
this.getParent().do_push_state(state);
214
do_load_state: function(state, warm) {
218
if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) {
219
var action_client = {
220
type: "ir.actions.client",
223
_push_me: state._push_me,
226
action_loaded = this.do_action(action_client);
228
var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
230
var add_context = {};
231
if (state.active_id) {
232
add_context.active_id = state.active_id;
234
if (state.active_ids) {
235
// The jQuery BBQ plugin does some parsing on values that are valid integers.
236
// It means that if there's only one item, it will do parseInt() on it,
237
// otherwise it will keep the comma seperated list as string.
238
add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
239
return parseInt(id, 10) || id;
241
} else if (state.active_id) {
242
add_context.active_ids = [state.active_id];
245
action_loaded = this.do_action(state.action, { additional_context: add_context });
246
$.when(action_loaded || null).done(function() {
247
instance.webclient.menu.has_been_loaded.done(function() {
248
if (self.inner_action && self.inner_action.id) {
249
instance.webclient.menu.open_action(self.inner_action.id);
255
} else if (state.model && state.id) {
256
// TODO handle context & domain ?
259
res_model: state.model,
261
type: 'ir.actions.act_window',
262
views: [[false, 'form']]
264
action_loaded = this.do_action(action);
265
} else if (state.sa) {
266
// load session action
268
action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).then(function(action) {
270
return self.do_action(action);
275
$.when(action_loaded || null).done(function() {
276
if (self.inner_widget && self.inner_widget.do_load_state) {
277
self.inner_widget.do_load_state(state, warm);
282
* Execute an OpenERP action
284
* @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
285
* @param {Object} [options]
286
* @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
287
* @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
288
* @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
289
* @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
290
* @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
291
* @param {Object} [options.additional_context] Additional context to be merged with the action's context.
292
* @return {jQuery.Deferred} Action loaded
294
do_action: function(action, options) {
295
options = _.defaults(options || {}, {
296
clear_breadcrumbs: false,
297
on_reverse_breadcrumb: function() {},
298
hide_breadcrumb: false,
299
on_close: function() {},
300
action_menu_id: null,
301
additional_context: {},
303
if (action === false) {
304
action = { type: 'ir.actions.act_window_close' };
305
} else if (_.isString(action) && instance.web.client_actions.contains(action)) {
306
var action_client = { type: "ir.actions.client", tag: action, params: {} };
307
return this.do_action(action_client, options);
308
} else if (_.isNumber(action) || _.isString(action)) {
310
return self.rpc("/web/action/load", { action_id: action }).then(function(result) {
311
return self.do_action(result, options);
315
// Ensure context & domain are evaluated and can be manipulated/used
316
var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
317
action.context = instance.web.pyeval.eval('context', ncontext);
318
if (action.context.active_id || action.context.active_ids) {
319
// Here we assume that when an `active_id` or `active_ids` is used
320
// in the context, we are in a `related` action, so we disable the
321
// searchview's default custom filters.
322
action.context.search_disable_custom_filters = true;
325
action.domain = instance.web.pyeval.eval(
326
'domain', action.domain, action.context || {});
330
console.error("No type for action", action);
331
return $.Deferred().reject();
333
var type = action.type.replace(/\./g,'_');
334
var popup = action.target === 'new';
335
var inline = action.target === 'inline' || action.target === 'inlineview';
336
action.flags = _.defaults(action.flags || {}, {
337
views_switcher : !popup && !inline,
338
search_view : !popup && !inline,
339
action_buttons : !popup && !inline,
340
sidebar : !popup && !inline,
341
pager : !popup && !inline,
342
display_title : !popup,
343
search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
345
action.menu_id = options.action_menu_id;
346
if (!(type in this)) {
347
console.error("Action manager can't handle action of type " + action.type, action);
348
return $.Deferred().reject();
350
return this[type](action, options);
352
null_action: function() {
354
this.clear_breadcrumbs();
358
* @param {Object} executor
359
* @param {Object} executor.action original action
360
* @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
361
* @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
362
* @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
363
* @param {Object} options
366
ir_actions_common: function(executor, options) {
367
if (this.inner_widget && executor.action.target !== 'new') {
368
if (this.getParent().has_uncommitted_changes()) {
369
return $.Deferred().reject();
370
} else if (options.clear_breadcrumbs) {
371
this.clear_breadcrumbs();
374
var widget = executor.widget();
375
if (executor.action.target === 'new') {
376
if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
377
this.dialog_widget.destroy();
380
this.dialog = new instance.web.Dialog(this, {
381
dialogClass: executor.klass,
383
this.dialog.on("closing", null, options.on_close);
384
this.dialog.dialog_title = executor.action.name;
385
if (widget instanceof instance.web.ViewManager) {
386
_.extend(widget.flags, {
387
$buttons: this.dialog.$buttons,
388
footer_to_buttons: true,
391
this.dialog_widget = widget;
392
this.dialog_widget.setParent(this.dialog);
393
var initialized = this.dialog_widget.appendTo(this.dialog.$el);
398
this.inner_action = executor.action;
399
this.inner_widget = widget;
400
executor.post_process(widget);
401
return this.inner_widget.appendTo(this.$el);
404
ir_actions_act_window: function (action, options) {
407
return this.ir_actions_common({
408
widget: function () { return new instance.web.ViewManagerAction(self, action); },
410
klass: 'oe_act_window',
411
post_process: function (widget) {
412
widget.add_breadcrumb({
413
on_reverse_breadcrumb: options.on_reverse_breadcrumb,
414
hide_breadcrumb: options.hide_breadcrumb,
419
ir_actions_client: function (action, options) {
421
var ClientWidget = instance.web.client_actions.get_object(action.tag);
423
if (!(ClientWidget.prototype instanceof instance.web.Widget)) {
425
if (next = ClientWidget(this, action)) {
426
return this.do_action(next, options);
431
return this.ir_actions_common({
432
widget: function () { return new ClientWidget(self, action); },
434
klass: 'oe_act_client',
435
post_process: function(widget) {
436
self.push_breadcrumb({
439
on_reverse_breadcrumb: options.on_reverse_breadcrumb,
440
hide_breadcrumb: options.hide_breadcrumb,
442
if (action.tag !== 'reload') {
443
self.do_push_state({});
448
ir_actions_act_window_close: function (action, options) {
455
ir_actions_server: function (action, options) {
457
this.rpc('/web/action/run', {
458
action_id: action.id,
459
context: action.context || {}
460
}).done(function (action) {
461
self.do_action(action, options)
464
ir_actions_report_xml: function(action, options) {
466
instance.web.blockUI();
467
return instance.web.pyeval.eval_domains_and_contexts({
468
contexts: [action.context],
470
}).then(function(res) {
471
action = _.clone(action);
472
action.context = res.context;
474
// iOS devices doesn't allow iframe use the way we do it,
475
// opening a new window seems the best way to workaround
476
if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
478
action: JSON.stringify(action),
479
token: new Date().getTime()
481
var url = self.session.url('/web/report', params)
482
instance.web.unblockUI();
483
$('<a href="'+url+'" target="_blank"></a>')[0].click();
487
var c = instance.webclient.crashmanager;
488
return $.Deferred(function (d) {
489
self.session.get_file({
491
data: {action: JSON.stringify(action)},
492
complete: instance.web.unblockUI,
501
c.rpc_error.apply(c, arguments);
508
ir_actions_act_url: function (action) {
509
window.open(action.url, action.target === 'self' ? '_self' : '_blank');
514
instance.web.ViewManager = instance.web.Widget.extend({
515
template: "ViewManager",
516
init: function(parent, dataset, views, flags) {
518
this.url_states = {};
519
this.model = dataset ? dataset.model : undefined;
520
this.dataset = dataset;
521
this.searchview = null;
522
this.active_view = null;
523
this.views_src = _.map(views, function(x) {
524
if (x instanceof Array) {
525
var view_type = x[1];
526
var View = instance.web.views.get_object(view_type, true);
527
var view_label = View ? View.prototype.display_name : (void 'nope');
530
view_type: view_type,
532
button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'),
538
this.ActionManager = parent;
540
this.flags = flags || {};
541
this.registry = instance.web.views;
542
this.views_history = [];
543
this.view_completely_inited = $.Deferred();
546
* @returns {jQuery.Deferred} initial view loading promise
551
this.$el.find('.oe_view_manager_switch a').click(function() {
552
self.switch_mode($(this).data('view-type'));
555
_.each(this.views_src, function(view) {
556
self.views[view.view_type] = $.extend({}, view, {
557
deferred : $.Deferred(),
560
$buttons : self.$el.find('.oe_view_manager_buttons'),
561
$sidebar : self.flags.sidebar ? self.$el.find('.oe_view_manager_sidebar') : undefined,
562
$pager : self.$el.find('.oe_view_manager_pager'),
563
action : self.action,
564
action_views_ids : views_ids
565
}, self.flags, self.flags[view.view_type] || {}, view.options || {})
567
views_ids[view.view_type] = view.view_id;
569
if (this.flags.views_switcher === false) {
570
this.$el.find('.oe_view_manager_switch').hide();
572
// If no default view defined, switch to the first one in sequence
573
var default_view = this.flags.default_view || this.views_src[0].view_type;
574
return this.switch_mode(default_view);
576
switch_mode: function(view_type, no_store, view_options) {
578
var view = this.views[view_type];
580
var form = this.views['form'];
581
if (!view || (form && form.controller && !form.controller.can_be_discarded())) {
582
self.trigger('switch_mode', view_type, no_store, view_options);
583
return $.Deferred().reject();
586
this.views_history.push(view_type);
588
this.active_view = view_type;
590
if (!view.controller) {
591
view_promise = this.do_create_view(view_type);
592
} else if (this.searchview
593
&& self.flags.auto_search
594
&& view.controller.searchable !== false) {
595
this.searchview.ready.done(this.searchview.do_search);
598
if (this.searchview) {
599
this.searchview[(view.controller.searchable === false || this.searchview.options.hidden) ? 'hide' : 'show']();
602
this.$el.find('.oe_view_manager_switch a').parent().removeClass('active');
604
.find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
605
.parent().addClass('active');
607
return $.when(view_promise).done(function () {
608
_.each(_.keys(self.views), function(view_name) {
609
var controller = self.views[view_name].controller;
611
var container = self.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_name);
612
if (view_name === view_type) {
614
controller.do_show(view_options || {});
617
controller.do_hide();
621
self.trigger('switch_mode', view_type, no_store, view_options);
624
do_create_view: function(view_type) {
625
// Lazy loading of views
627
var view = this.views[view_type];
628
var viewclass = this.registry.get_object(view_type);
629
var options = _.clone(view.options);
630
if (view_type === "form" && this.action && (this.action.target == 'new' || this.action.target == 'inline')) {
631
options.initial_mode = 'edit';
633
var controller = new viewclass(this, this.dataset, view.view_id, options);
635
controller.on('history_back', this, function() {
636
var am = self.getParent();
637
if (am && am.trigger) {
638
return am.trigger('history_back');
642
controller.on("change:title", this, function() {
643
if (self.active_view === view_type) {
644
self.set_title(controller.get('title'));
648
if (view.embedded_view) {
649
controller.set_embedded_view(view.embedded_view);
651
controller.on('switch_mode', self, this.switch_mode);
652
controller.on('previous_view', self, this.prev_view);
654
var container = this.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_type);
655
var view_promise = controller.appendTo(container);
656
this.views[view_type].controller = controller;
657
this.views[view_type].deferred.resolve(view_type);
658
return $.when(view_promise).done(function() {
660
&& self.flags.auto_search
661
&& view.controller.searchable !== false) {
662
self.searchview.ready.done(self.searchview.do_search);
664
self.view_completely_inited.resolve();
666
self.trigger("controller_inited",view_type,controller);
670
* @returns {Number|Boolean} the view id of the given type, false if not found
672
get_view_id: function(view_type) {
673
return this.views[view_type] && this.views[view_type].view_id || false;
675
set_title: function(title) {
676
this.$el.find('.oe_view_title_text:first').text(title);
678
add_breadcrumb: function(options) {
679
var options = options || {};
680
// 7.0 backward compatibility
681
if (typeof options == 'function') {
683
on_reverse_breadcrumb: options
686
// end of 7.0 backward compatibility
688
var views = [this.active_view || this.views_src[0].view_type];
689
this.on('switch_mode', self, function(mode) {
690
var last = views.slice(-1)[0];
692
if (mode !== 'form') {
698
var item = _.extend({
701
show: function(index) {
702
var view_to_select = views[index];
703
var state = self.url_states[view_to_select];
704
self.do_push_state(state || {});
705
$.when(self.switch_mode(view_to_select)).done(function() {
709
get_title: function() {
712
_.each(self.getParent().breadcrumbs, function(bc, i) {
713
if (bc.widget === self) {
717
var next = self.getParent().breadcrumbs.slice(currentIndex + 1)[0];
718
var titles = _.map(views, function(v) {
719
var controller = self.views[v].controller;
721
id = controller.datarecord.id;
723
return controller.get('title');
725
if (next && next.action && next.action.res_id && self.dataset &&
726
self.active_view === 'form' && self.dataset.model === next.action.res_model && id === next.action.res_id) {
727
// If the current active view is a formview and the next item in the breadcrumbs
728
// is an action on same object (model / res_id), then we omit the current formview's title
734
this.getParent().push_breadcrumb(item);
737
* Returns to the view preceding the caller view in this manager's
738
* navigation history (the navigation history is appended to via
741
* @param {Object} [options]
742
* @param {Boolean} [options.created=false] resource was created
743
* @param {String} [options.default=null] view to switch to if no previous view
744
* @returns {$.Deferred} switching end signal
746
prev_view: function (options) {
747
options = options || {};
748
var current_view = this.views_history.pop();
749
var previous_view = this.views_history[this.views_history.length - 1] || options['default'];
750
if (options.created && current_view === 'form' && previous_view === 'list') {
751
// APR special case: "If creation mode from list (and only from a list),
752
// after saving, go to page view (don't come back in list)"
753
return this.switch_mode('form');
754
} else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') {
755
// APR special case: "If creation from dashboard, we have no previous view
756
return this.switch_mode('form');
758
return this.switch_mode(previous_view, true);
761
* Sets up the current viewmanager's search view.
763
* @param {Number|false} view_id the view to use or false for a default one
764
* @returns {jQuery.Deferred} search view startup deferred
766
setup_search_view: function(view_id, search_defaults) {
768
if (this.searchview) {
769
this.searchview.destroy();
772
hidden: this.flags.search_view === false,
773
disable_custom_filters: this.flags.search_disable_custom_filters,
775
this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, options);
777
this.searchview.on('search_data', self, this.do_searchview_search);
778
return this.searchview.appendTo(this.$el.find(".oe_view_manager_view_search"));
780
do_searchview_search: function(domains, contexts, groupbys) {
782
controller = this.views[this.active_view].controller,
783
action_context = this.action.context || {};
784
instance.web.pyeval.eval_domains_and_contexts({
785
domains: [this.action.domain || []].concat(domains || []),
786
contexts: [action_context].concat(contexts || []),
787
group_by_seq: groupbys || []
788
}).done(function (results) {
789
self.dataset._model = new instance.web.Model(
790
self.dataset.model, results.context, results.domain);
791
var groupby = results.group_by.length
793
: action_context.group_by;
794
if (_.isString(groupby)) {
797
$.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
798
self.view_completely_inited.resolve();
803
* Called when one of the view want to execute an action
805
on_action: function(action) {
807
on_create: function() {
809
on_remove: function() {
811
on_edit: function() {
814
* Called by children view after executing an action
816
on_action_executed: function () {
820
instance.web.ViewManagerAction = instance.web.ViewManager.extend({
821
template:"ViewManagerAction",
823
* @constructs instance.web.ViewManagerAction
824
* @extends instance.web.ViewManager
826
* @param {instance.web.ActionManager} parent parent object/widget
827
* @param {Object} action descriptor for the action this viewmanager needs to manage its views.
829
init: function(parent, action) {
830
// dataset initialization will take the session from ``this``, so if we
831
// do not have it yet (and we don't, because we've not called our own
832
// ``_super()``) rpc requests will blow up.
833
var flags = action.flags || {};
834
if (!('auto_search' in flags)) {
835
flags.auto_search = action.auto_search !== false;
837
if (action.res_model == 'board.board' && action.view_mode === 'form') {
838
// Special case for Dashboards
840
views_switcher : false,
841
display_title : false,
845
action_buttons : false
848
this._super(parent, null, action.views, flags);
849
this.session = parent.session;
850
this.action = action;
851
var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
853
dataset.ids.push(action.res_id);
856
this.dataset = dataset;
859
* Initializes the ViewManagerAction: sets up the searchview (if the
860
* searchview is enabled in the manager's action flags), calls into the
861
* parent to initialize the primary view and (if the VMA has a searchview)
862
* launches an initial search after both views are done rendering.
867
search_defaults = {};
868
_.each(this.action.context, function (value, key) {
869
var match = /^search_default_(.*)$/.exec(key);
871
search_defaults[match[1]] = value;
875
var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
877
searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
879
var main_view_loaded = this._super();
881
var manager_ready = $.when(searchview_loaded, main_view_loaded, this.view_completely_inited);
883
this.$el.find('.oe_debug_view').change(this.on_debug_changed);
884
this.$el.addClass("oe_view_manager_" + (this.action.target || 'current'));
885
return manager_ready;
887
on_debug_changed: function (evt) {
889
$sel = $(evt.currentTarget),
890
$option = $sel.find('option:selected'),
892
current_view = this.views[this.active_view].controller;
895
var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open();
896
$('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el);
900
name: _t("JS Tests"),
902
type : 'ir.actions.act_url',
903
url: '/web/tests?mod=*'
907
var ids = current_view.get_selected_ids();
908
if (ids.length === 1) {
909
this.dataset.call('perm_read', [ids]).done(function(result) {
910
var dialog = new instance.web.Dialog(this, {
911
title: _.str.sprintf(_t("View Log (%s)"), self.dataset.model),
913
}, QWeb.render('ViewManagerDebugViewLog', {
915
format : instance.web.format_value
920
case 'toggle_layout_outline':
921
current_view.rendering_engine.toggle_layout_debugging();
924
current_view.open_defaults_dialog();
928
name: _t("Technical Translation"),
929
res_model : 'ir.translation',
930
domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
931
views: [[false, 'list'], [false, 'form']],
932
type : 'ir.actions.act_window',
938
this.dataset.call('fields_get', [false, {}]).done(function (fields) {
939
var $root = $('<dl>');
940
_(fields).each(function (attributes, name) {
941
$root.append($('<dt>').append($('<h4>').text(name)));
942
var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root));
943
_(attributes).each(function (def, name) {
944
if (def instanceof Object) {
945
def = JSON.stringify(def);
948
.append($('<dt>').text(name))
949
.append($('<dd style="white-space: pre-wrap;">').text(def));
952
new instance.web.Dialog(self, {
953
title: _.str.sprintf(_t("Model %s fields"),
955
width: '95%'}, $root).open();
958
case 'edit_workflow':
959
return this.do_action({
960
res_model : 'workflow',
961
domain : [['osv', '=', this.dataset.model]],
962
views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
963
type : 'ir.actions.act_window',
969
this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
971
case 'manage_filters':
973
res_model: 'ir.filters',
974
views: [[false, 'list'], [false, 'form']],
975
type: 'ir.actions.act_window',
977
search_default_my_filters: true,
978
search_default_model_id: this.dataset.model
982
case 'print_workflow':
983
if (current_view.get_selected_ids && current_view.get_selected_ids().length == 1) {
984
instance.web.blockUI();
986
context: { active_ids: current_view.get_selected_ids() },
987
report_name: "workflow.instance.graph",
989
model: this.dataset.model,
990
id: current_view.get_selected_ids()[0],
994
this.session.get_file({
996
data: {action: JSON.stringify(action)},
997
complete: instance.web.unblockUI
1003
console.log("No debug handler for ", val);
1006
evt.currentTarget.selectedIndex = 0;
1008
do_edit_resource: function(model, id, action) {
1009
var action = _.extend({
1012
type : 'ir.actions.act_window',
1015
views : [[false, 'form']],
1018
action_buttons : true,
1020
resize_textareas : true
1024
this.do_action(action);
1026
switch_mode: function (view_type, no_store, options) {
1029
return this.alive($.when(this._super.apply(this, arguments))).done(function () {
1030
var controller = self.views[self.active_view].controller;
1031
self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
1038
do_create_view: function(view_type) {
1040
return this._super.apply(this, arguments).then(function() {
1041
var view = self.views[view_type].controller;
1042
view.set({ 'title': self.action.name });
1045
get_action_manager: function() {
1047
while (cur = cur.getParent()) {
1048
if (cur instanceof instance.web.ActionManager) {
1054
set_title: function(title) {
1055
this.$el.find('.oe_breadcrumb_title:first').html(this.get_action_manager().get_title());
1057
do_push_state: function(state) {
1058
if (this.getParent() && this.getParent().do_push_state) {
1059
state["view_type"] = this.active_view;
1060
this.url_states[this.active_view] = state;
1061
this.getParent().do_push_state(state);
1064
do_load_state: function(state, warm) {
1067
if (state.view_type && state.view_type !== this.active_view) {
1069
this.views[this.active_view].deferred.then(function() {
1070
return self.switch_mode(state.view_type, true);
1075
$.when(defs).done(function() {
1076
self.views[self.active_view].controller.do_load_state(state, warm);
1081
instance.web.Sidebar = instance.web.Widget.extend({
1082
init: function(parent) {
1084
this._super(parent);
1085
var view = this.getParent();
1087
{ 'name' : 'print', 'label' : _t('Print'), },
1088
{ 'name' : 'other', 'label' : _t('More'), }
1094
this.fileupload_id = _.uniqueId('oe_fileupload');
1095
$(window).on(this.fileupload_id, function() {
1096
var args = [].slice.call(arguments).slice(1);
1097
self.do_attachement_update(self.dataset, self.model_id,args);
1098
instance.web.unblockUI();
1105
this.$el.on('click','.oe_dropdown_menu li a', function(event) {
1106
var section = $(this).data('section');
1107
var index = $(this).data('index');
1108
var item = self.items[section][index];
1109
if (item.callback) {
1110
item.callback.apply(self, [item]);
1111
} else if (item.action) {
1112
self.on_item_action_clicked(item);
1113
} else if (item.url) {
1116
event.preventDefault();
1119
redraw: function() {
1121
self.$el.html(QWeb.render('Sidebar', {widget: self}));
1123
// Hides Sidebar sections when item list is empty
1124
this.$('.oe_form_dropdown_section').each(function() {
1125
$(this).toggle(!!$(this).find('li').length);
1128
self.$("[title]").tipsy({
1134
* For each item added to the section:
1137
* will be used as the item's name in the sidebar, can be html
1140
* descriptor for the action which will be executed, ``action`` and
1141
* ``callback`` should be exclusive
1144
* function to call when the item is clicked in the sidebar, called
1145
* with the item descriptor as its first argument (so information
1146
* can be stored as additional keys on the object passed to
1149
* ``classname`` (optional)
1150
* ``@class`` set on the sidebar serialization of the item
1152
* ``title`` (optional)
1153
* will be set as the item's ``@title`` (tooltip)
1155
* @param {String} section_code
1156
* @param {Array<{label, action | callback[, classname][, title]}>} items
1158
add_items: function(section_code, items) {
1161
this.items[section_code].push.apply(this.items[section_code],items);
1165
add_toolbar: function(toolbar) {
1167
_.each(['print','action','relate'], function(type) {
1168
var items = toolbar[type];
1170
for (var i = 0; i < items.length; i++) {
1172
label: items[i]['name'],
1174
classname: 'oe_sidebar_' + type
1177
self.add_items(type=='print' ? 'print' : 'other', items);
1181
on_item_action_clicked: function(item) {
1183
self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) {
1184
var ids = self.getParent().get_selected_ids();
1185
if (ids.length == 0) {
1186
instance.web.dialog($("<div />").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true });
1189
var active_ids_context = {
1192
active_model: self.getParent().dataset.model
1194
var c = instance.web.pyeval.eval('context',
1195
new instance.web.CompoundContext(
1196
sidebar_eval_context, active_ids_context));
1197
self.rpc("/web/action/load", {
1198
action_id: item.action.id,
1200
}).done(function(result) {
1201
result.context = new instance.web.CompoundContext(
1202
result.context || {}, active_ids_context)
1203
.set_eval_context(c);
1204
result.flags = result.flags || {};
1205
result.flags.new_window = true;
1206
self.do_action(result, {
1207
on_close: function() {
1209
self.getParent().reload();
1215
do_attachement_update: function(dataset, model_id, args) {
1217
this.dataset = dataset;
1218
this.model_id = model_id;
1219
if (args && args[0].error) {
1220
this.do_warn( instance.web.qweb.render('message_error_uploading'), args[0].error);
1223
this.on_attachments_loaded([]);
1225
var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
1226
var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
1227
ds.read_slice(['name', 'url', 'type', 'create_uid', 'create_date', 'write_uid', 'write_date'], {}).done(this.on_attachments_loaded);
1230
on_attachments_loaded: function(attachments) {
1233
var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
1234
_.each(attachments,function(a) {
1236
if(a.type === "binary") {
1237
a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime());
1240
self.items['files'] = attachments;
1242
this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
1243
this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
1245
on_attachment_changed: function(e) {
1246
var $e = $(e.target);
1247
if ($e.val() !== '') {
1248
this.$el.find('form.oe_form_binary_form').submit();
1249
$e.parent().find('input[type=file]').prop('disabled', true);
1250
$e.parent().find('button').prop('disabled', true).find('img, span').toggle();
1251
this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...'));
1252
instance.web.blockUI();
1255
on_attachment_delete: function(e) {
1257
e.stopPropagation();
1259
var $e = $(e.currentTarget);
1260
if (confirm(_t("Do you really want to delete this attachment ?"))) {
1261
(new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).done(function() {
1262
self.do_attachement_update(self.dataset, self.model_id);
1268
instance.web.View = instance.web.Widget.extend({
1269
// name displayed in view switchers
1272
* Define a view type for each view to allow automatic call to fields_view_get.
1274
view_type: undefined,
1275
init: function(parent, dataset, view_id, options) {
1276
this._super(parent);
1277
this.ViewManager = parent;
1278
this.dataset = dataset;
1279
this.view_id = view_id;
1280
this.set_default_options(options);
1282
start: function () {
1283
return this.load_view();
1285
load_view: function(context) {
1287
var view_loaded_def;
1288
if (this.embedded_view) {
1289
view_loaded_def = $.Deferred();
1290
$.async_when().done(function() {
1291
view_loaded_def.resolve(self.embedded_view);
1294
if (! this.view_type)
1295
console.warn("view_type is not defined", this);
1296
view_loaded_def = instance.web.fields_view_get({
1297
"model": this.dataset._model,
1298
"view_id": this.view_id,
1299
"view_type": this.view_type,
1300
"toolbar": !!this.options.$sidebar,
1301
"context": this.dataset.get_context(),
1304
return view_loaded_def.then(function(r) {
1305
self.fields_view = r;
1306
// add css classes that reflect the (absence of) access rights
1307
self.$el.addClass('oe_view')
1308
.toggleClass('oe_cannot_create', !self.is_action_enabled('create'))
1309
.toggleClass('oe_cannot_edit', !self.is_action_enabled('edit'))
1310
.toggleClass('oe_cannot_delete', !self.is_action_enabled('delete'));
1311
return $.when(self.view_loading(r)).then(function() {
1312
self.trigger('view_loaded', r);
1316
view_loading: function(r) {
1318
set_default_options: function(options) {
1319
this.options = options || {};
1320
_.defaults(this.options, {
1321
// All possible views options should be defaulted here
1325
action_views_ids: {}
1329
* Fetches and executes the action identified by ``action_data``.
1331
* @param {Object} action_data the action descriptor data
1332
* @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
1333
* @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
1334
* @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
1335
* @param {Object} [action_data.context=null] additional action context, to add to the current context
1336
* @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
1337
* @param {Object} [record_id] the identifier of the object on which the action is to be applied
1338
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
1340
do_execute_action: function (action_data, dataset, record_id, on_closed) {
1342
var result_handler = function () {
1343
if (on_closed) { on_closed.apply(null, arguments); }
1344
if (self.getParent() && self.getParent().on_action_executed) {
1345
return self.getParent().on_action_executed.apply(null, arguments);
1348
var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
1349
var handler = function (action) {
1350
if (action && action.constructor == Object) {
1351
var ncontext = new instance.web.CompoundContext(context);
1354
active_id: record_id,
1355
active_ids: [record_id],
1356
active_model: dataset.model
1359
ncontext.add(action.context || {});
1360
action.context = ncontext;
1361
return self.do_action(action, {
1362
on_close: result_handler,
1365
self.do_action({"type":"ir.actions.act_window_close"});
1366
return result_handler();
1370
if (action_data.special === 'cancel') {
1371
return handler({"type":"ir.actions.act_window_close"});
1372
} else if (action_data.type=="object") {
1373
var args = [[record_id]], additional_args = [];
1374
if (action_data.args) {
1376
// Warning: quotes and double quotes problem due to json and xml clash
1377
// Maybe we should force escaping in xml or do a better parse of the args array
1378
additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
1379
args = args.concat(additional_args);
1381
console.error("Could not JSON.parse arguments", action_data.args);
1385
return dataset.call_button(action_data.name, args).then(handler).then(function () {
1386
if (instance.webclient) {
1387
instance.webclient.menu.do_reload_needaction();
1390
} else if (action_data.type=="action") {
1391
return this.rpc('/web/action/load', {
1392
action_id: action_data.name,
1393
context: instance.web.pyeval.eval('context', context),
1397
return dataset.exec_workflow(record_id, action_data.name).then(handler);
1401
* Directly set a view to use instead of calling fields_view_get. This method must
1402
* be called before start(). When an embedded view is set, underlying implementations
1403
* of instance.web.View must use the provided view instead of any other one.
1405
* @param embedded_view A view.
1407
set_embedded_view: function(embedded_view) {
1408
this.embedded_view = embedded_view;
1410
do_show: function () {
1413
do_hide: function () {
1416
is_active: function () {
1417
var manager = this.getParent();
1418
return !manager || !manager.active_view
1419
|| manager.views[manager.active_view].controller === this;
1421
* Wraps fn to only call it if the current view is the active one. If the
1422
* current view is not active, doesn't call fn.
1424
* fn can not return anything, as a non-call to fn can't return anything
1427
* @param {Function} fn function to wrap in the active guard
1429
guard_active: function (fn) {
1431
return function () {
1432
if (self.is_active()) {
1433
fn.apply(self, arguments);
1437
do_push_state: function(state) {
1438
if (this.getParent() && this.getParent().do_push_state) {
1439
this.getParent().do_push_state(state);
1442
do_load_state: function(state, warm) {
1445
* Switches to a specific view type
1447
do_switch_view: function() {
1448
this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments)));
1451
* Cancels the switch to the current view, switches to the previous one
1453
* @param {Object} [options]
1454
* @param {Boolean} [options.created=false] resource was created
1455
* @param {String} [options.default=null] view to switch to if no previous view
1458
do_search: function(view) {
1460
on_sidebar_export: function() {
1461
new instance.web.DataExport(this, this.dataset).open();
1463
sidebar_eval_context: function () {
1467
* Asks the view to reload itself, if the reloading is asynchronous should
1468
* return a {$.Deferred} indicating when the reloading is done.
1470
reload: function () {
1474
* Return whether the user can perform the action ('create', 'edit', 'delete') in this view.
1475
* An action is disabled by setting the corresponding attribute in the view's main element,
1476
* like: <form string="" create="false" edit="false" delete="false">
1478
is_action_enabled: function(action) {
1479
var attrs = this.fields_view.arch.attrs;
1480
return (action in attrs) ? JSON.parse(attrs[action]) : true;
1485
* Performs a fields_view_get and apply postprocessing.
1486
* return a {$.Deferred} resolved with the fvg
1488
* @param {Object} args
1489
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
1490
* @param {Object} [args.context] context if args.model is a string
1491
* @param {Number} [args.view_id] id of the view to be loaded, default view if null
1492
* @param {String} [args.view_type] type of view to be loaded if view_id is null
1493
* @param {Boolean} [args.toolbar=false] get the toolbar definition
1495
instance.web.fields_view_get = function(args) {
1496
function postprocess(fvg) {
1497
var doc = $.parseXML(fvg.arch).documentElement;
1498
fvg.arch = instance.web.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
1499
if ('id' in fvg.fields) {
1500
// Special case for id's
1501
var id_field = fvg.fields['id'];
1502
id_field.original_type = id_field.type;
1503
id_field.type = 'id';
1505
_.each(fvg.fields, function(field) {
1506
_.each(field.views || {}, function(view) {
1512
args = _.defaults(args, {
1515
var model = args.model;
1516
if (typeof model === 'string') {
1517
model = new instance.web.Model(args.model, args.context);
1519
return args.model.call('fields_view_get', [args.view_id, args.view_type, args.context, args.toolbar]).then(function(fvg) {
1520
return postprocess(fvg);
1524
instance.web.xml_to_json = function(node, strip_whitespace) {
1525
switch (node.nodeType) {
1527
return instance.web.xml_to_json(node.documentElement, strip_whitespace);
1530
return (strip_whitespace && node.data.trim() === '') ? undefined : node.data;
1532
var attrs = $(node).getAttributes();
1533
_.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
1536
attrs[key] = JSON.parse(attrs[key]);
1541
tag: node.tagName.toLowerCase(),
1543
children: _.compact(_.map(node.childNodes, function(node) {
1544
return instance.web.xml_to_json(node, strip_whitespace);
1549
instance.web.json_node_to_xml = function(node, human_readable, indent) {
1550
// For debugging purpose, this function will convert a json node back to xml
1551
indent = indent || 0;
1552
var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
1553
r = sindent + '<' + node.tag,
1554
cr = human_readable ? '\n' : '';
1556
if (typeof(node) === 'string') {
1557
return sindent + node;
1558
} else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
1560
_.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
1561
JSON.stringify(node)));
1563
for (var attr in node.attrs) {
1564
var vattr = node.attrs[attr];
1565
if (typeof(vattr) !== 'string') {
1567
vattr = JSON.stringify(vattr);
1569
vattr = vattr.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
1570
if (human_readable) {
1571
vattr = vattr.replace(/"/g, "'");
1573
r += ' ' + attr + '="' + vattr + '"';
1575
if (node.children && node.children.length) {
1578
for (var i = 0, ii = node.children.length; i < ii; i++) {
1579
childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1));
1581
r += childs.join(cr);
1582
r += cr + sindent + '</' + node.tag + '>';
1588
instance.web.xml_to_str = function(node) {
1590
if (window.XMLSerializer) {
1591
str = (new XMLSerializer()).serializeToString(node);
1592
} else if (window.ActiveXObject) {
1595
throw new Error(_t("Could not serialize XML"));
1597
// Browsers won't deal with self closing tags except void elements:
1598
// http://www.w3.org/TR/html-markup/syntax.html
1599
var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
1601
// The following regex is a bit naive but it's ok for the xmlserializer output
1602
str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
1603
if (void_elements.indexOf(tag) < 0) {
1604
return "<" + tag + attrs + "></" + tag + ">";
1613
* Registry for all the main views
1615
instance.web.views = new instance.web.Registry();
1619
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: