diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index e1855ca5513..e8749388c1e 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -493,21 +493,20 @@ var abstractReconciliationLine = Widget.extend({ // field_manager var dataset = new data.DataSet(this, "account.account", self.context); dataset.ids = []; - dataset.arch = { + var fields_view = {}; + fields_view.arch = { attrs: { string: "Stéphanie de Monaco", version: "7.0", class: "oe_form_container o_form_container" }, children: [], tag: "form" }; - self.field_manager = new FormView ( - this, dataset, false, { + this, dataset, fields_view, { initial_mode: 'edit', disable_autofocus: false, $buttons: $(), $pager: $() }); - - self.field_manager.load_form(dataset); + self.field_manager.appendTo(document.createDocumentFragment()); // starts the FormView // fields default properties var Default_field = function() { diff --git a/addons/document/static/src/js/document.js b/addons/document/static/src/js/document.js index 9448a93adb9..05885839469 100644 --- a/addons/document/static/src/js/document.js +++ b/addons/document/static/src/js/document.js @@ -9,7 +9,8 @@ var _t = core._t; Sidebar.include({ init : function(){ this._super.apply(this, arguments); - if (this.getParent().view_type == "form"){ + var view = this.getParent(); + if (view.fields_view && view.fields_view.type === "form") { this.sections.splice(1, 0, { 'name' : 'files', 'label' : _t('Attachment(s)'), }); this.items.files = []; } diff --git a/addons/google_spreadsheet/static/src/js/search.js b/addons/google_spreadsheet/static/src/js/search.js index abed0a383a3..b1a71c0a034 100644 --- a/addons/google_spreadsheet/static/src/js/search.js +++ b/addons/google_spreadsheet/static/src/js/search.js @@ -27,8 +27,8 @@ FormView.include({ }); FavoriteMenu.include({ - prepare_dropdown_menu: function (filters) { - this._super(filters); + start: function () { + this._super(); var am = this.findAncestor(function(a) { return a instanceof ActionManager; }); diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 64bd86568ad..bbc50adffba 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -10,6 +10,7 @@ var config = require('web.config'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var data = require('web.data'); +var data_manager = require('web.data_manager'); var Dialog = require('web.Dialog'); var framework = require('web.framework'); var Model = require('web.Model'); @@ -160,6 +161,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { init: function(parent, action, options) { this._super.apply(this, arguments); this.action_manager = parent; + this.dataset = new data.DataSetSearch(this, 'mail.message'); this.domain = []; this.action = action; this.options = options || {}; @@ -170,7 +172,14 @@ var ChatAction = Widget.extend(ControlPanelMixin, { }, willStart: function () { - return chat_manager.is_ready; + var self = this; + var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0]; + var def = data_manager + .load_fields_view(this.dataset, view_id, 'search', false) + .then(function (fields_view) { + self.fields_view = fields_view; + }); + return $.when(this._super(), chat_manager.is_ready, def); }, start: function() { @@ -182,8 +191,6 @@ var ChatAction = Widget.extend(ControlPanelMixin, { action: this.action, disable_groupby: true, }; - var dataset = new data.DataSetSearch(this, 'mail.message'); - var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; var default_channel_id = this.options.active_id || this.action.context.active_id || this.action.params.default_active_id || @@ -191,7 +198,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { var default_channel = chat_manager.get_channel(default_channel_id) || chat_manager.get_channel('channel_inbox'); - this.searchview = new SearchView(this, dataset, view_id, {}, options); + this.searchview = new SearchView(this, this.dataset, this.fields_view, options); this.searchview.on('search_data', this, this.on_search); this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true}); diff --git a/addons/sales_team/static/src/js/sales_team_dashboard.js b/addons/sales_team/static/src/js/sales_team_dashboard.js index aadb7105bff..4d3a08df7c8 100644 --- a/addons/sales_team/static/src/js/sales_team_dashboard.js +++ b/addons/sales_team/static/src/js/sales_team_dashboard.js @@ -15,7 +15,6 @@ var _lt = core._lt; var SalesTeamDashboardView = KanbanView.extend({ display_name: _lt('Dashboard'), icon: 'fa-dashboard', - view_type: "sales_team_dashboard", searchview_hidden: true, events: { 'click .o_dashboard_action': 'on_dashboard_action_clicked', diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index b88a103a8df..fc66bb394b5 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1103,7 +1103,7 @@ class Binary(http.Controller): class Action(http.Controller): @http.route('/web/action/load', type='json', auth="user") - def load(self, action_id, do_not_eval=False, additional_context=None): + def load(self, action_id, additional_context=None): Actions = request.session.model('ir.actions.actions') value = False try: diff --git a/addons/web/static/src/js/action_manager.js b/addons/web/static/src/js/action_manager.js index fcce83720ae..359b174a28d 100644 --- a/addons/web/static/src/js/action_manager.js +++ b/addons/web/static/src/js/action_manager.js @@ -5,6 +5,7 @@ var ControlPanel = require('web.ControlPanel'); var core = require('web.core'); var crash_manager = require('web.crash_manager'); var data = require('web.data'); +var data_manager = require('web.data_manager'); var Dialog = require('web.Dialog'); var framework = require('web.framework'); var pyeval = require('web.pyeval'); @@ -564,7 +565,7 @@ var ActionManager = Widget.extend({ active_ids : options.additional_context.active_ids, active_model : options.additional_context.active_model }; - return self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) { + return data_manager.load_action(action, additional_context).then(function(result) { return self.do_action(result, options); }); } diff --git a/addons/web/static/src/js/compatibility.js b/addons/web/static/src/js/compatibility.js index 9fda95be633..5532c8c388e 100644 --- a/addons/web/static/src/js/compatibility.js +++ b/addons/web/static/src/js/compatibility.js @@ -186,8 +186,7 @@ function start_modules (modules) { // Monkey-patching of the ListView for backward compatibiliy of the colors and // fonts row's attributes, as they are deprecated in 9.0. ListView.include({ - load_list: function(data) { - this._super(data); + willStart: function() { if (this.fields_view.arch.attrs.colors) { this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain() .compact() @@ -208,6 +207,8 @@ ListView.include({ return [font, py.parse(py.tokenize(expr)), expr]; }).value(); } + + return this._super(data); }, /** * Returns the style for the provided record in the current view (from the diff --git a/addons/web/static/src/js/framework/data_manager.js b/addons/web/static/src/js/framework/data_manager.js new file mode 100644 index 00000000000..8065b48b4e1 --- /dev/null +++ b/addons/web/static/src/js/framework/data_manager.js @@ -0,0 +1,226 @@ +odoo.define('web.DataManager', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.DataModel'); +var session = require('web.session'); +var utils = require('web.utils'); + +return core.Class.extend({ + init: function () { + this.Filters = new Model('ir.filters'); + this._cache = { + actions: {}, + fields_views: {}, + fields: {}, + filters: {}, + views: {}, + }; + }, + + /** + * Loads an action from its id or xmlid. + * + * @param {int|string} [action_id] the action id or xmlid + * @param {Object} [additional_context] used to load the action + * @return {Deferred} resolved with the action whose id or xmlid is action_id + */ + load_action: function (action_id, additional_context) { + var self = this; + var key = this._gen_key(action_id, additional_context || {}); + + if (!this._cache.actions[key]) { + this._cache.actions[key] = session.rpc("/web/action/load", { + action_id: action_id, + additional_context : additional_context, + }).then(function (action) { + self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key]; + return action; + }); + } + + return this._cache.actions[key]; + }, + + /** + * Loads various information concerning views: fields_view for each view, + * and optionally filters and fields. + * + * @param {Object} [dataset] the dataset for which the views are loaded + * @param {Array} [views_descr] array of [view_id, view_type] + * @param {Object} [options] dictionnary of various options: + * - options.load_filters: whether or not to load the filters, + * - options.action_id: the action_id (required to load filters), + * - options.load_fields: whether or not to load the fields, + * - options.toolbar: whether or not a toolbar will be displayed, + * @return {Deferred} resolved with the requested views information + */ + load_views: function (dataset, views_descr, options) { + var self = this; + var model = dataset.model; + var context = dataset.get_context(); + var key = this._gen_key(model, views_descr, options || {}, context); + + if (!this._cache.views[key]) { + // Don't load fields or filters if already in cache + options.load_fields = options.load_fields && !this._cache.fields[model]; + var filters_key; + if (options.load_filters) { + filters_key = this._gen_key(model, options.action_id); + options.load_filters = !this._cache.filters[filters_key]; + } + + this._cache.views[key] = dataset.call('load_views', { + views: views_descr, + options: options, + context: context, + }).then(function (result) { + // Postprocess fields_views and insert them into the fields_views cache + result.fields_views = _.mapObject(result.fields_views, self._postprocess_fvg.bind(self)); // FIXME: lazy postprocess? + _.each(views_descr, function (view_descr) { + var toolbar = options.toolbar && view_descr[1] !== 'search'; + var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context); + self._cache.fields_views[fv_key] = $.when(result.fields_views[view_descr[1]]); + }); + + // Insert filters, if any, into the filters cache + if (result.filters) { + self._cache.filters[filters_key] = $.when(result.filters); + } + + // Insert fields, if any, into the fields cache + if (result.fields) { + self._cache.fields[model] = $.when(result.fields); + } + + return result.fields_views; + }); + } + + return this._cache.views[key]; + }, + + /** + * Loads the fields_view of a given model, view_type and optional view_id. + * + * @param {Object} [dataset] the dataset for which the fields_view is loaded + * @param {int} [view_id] the id of the view (optional) + * @param {string} [view_type] the type of the view + * @param {Boolean} [toolbar] whether or not a toolbar will be displayed + * @return {Deferred} resolved with the requested fields_view + */ + load_fields_view: function (dataset, view_id, view_type, toolbar) { + var key = this._gen_key(dataset.model, view_id, view_type, toolbar, dataset.get_context()); + if (!this._cache.fields_views[key]) { + this._cache.fields_views[key] = dataset.call('fields_view_get', { + view_id: view_id, + view_type: view_type, + toolbar: toolbar, + context: dataset.get_context(), + }).then(this._postprocess_fvg.bind(this)); + } + return this._cache.fields_views[key]; + }, + + /** + * Loads the fields of a given model. + * + * @param {Object} [dataset] the dataset for which the fields are loaded + * @return {Deferred} resolved with the requested fields + */ + load_fields: function (dataset) { + if (!this._cache.fields[dataset.model]) { + this._cache.fields[dataset.model] = dataset.call('fields_get', { + context: dataset.get_context(), + }); + } + return this._cache.fields[dataset.model]; + }, + + /** + * Loads the filters of a given model and optional action id. + * + * @param {Object} [dataset] the dataset for which the filters are loaded + * @param {int} [action_id] the id of the action (optional) + * @return {Deferred} resolved with the requested filters + */ + load_filters: function (dataset, action_id) { + var key = this._gen_key(dataset.model, action_id); + if (!this._cache.filters[key]) { + this._cache.filters[key] = this.Filters.call('get_filters', [dataset.model, action_id], { + context: dataset.get_context(), + }); + } + return this._cache.filters[key]; + }, + + /** + * Calls 'create_or_replace' on 'ir_filters'. + * + * @param {Object} [filter] the filter description + * @return {Deferred} resolved with the id of the created or replaced filter + */ + create_filter: function (filter) { + var self = this; + return this.Filters + .call('create_or_replace', [filter]) + .then(function (filter_id) { + var key = [ + filter.model_id, + filter.action_id || false, + ].join(','); + self._cache.filters[key] = null; // invalidate cache + return filter_id; + }); + }, + + /** + * Calls 'unlink' on 'ir_filters'. + * + * @param {Object} [filter] the description of the filter to remove + * @return {Deferred} + */ + delete_filter: function (filter) { + var self = this; + return this.Filters + .call('unlink', [filter.id]) + .then(function () { + self._cache.filters = {}; // invalidate cache + }); + }, + + /** + * Private function that postprocesses fields_view (mainly parses the arch attribute) + */ + _postprocess_fvg: function (fields_view) { + var self = this; + var doc = $.parseXML(fields_view.arch).documentElement; + fields_view.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban')); + if ('id' in fields_view.fields) { + // Special case for id's + var id_field = fields_view.fields.id; + id_field.original_type = id_field.type; + id_field.type = 'id'; + } + _.each(fields_view.fields, function(field) { + _.each(field.views || {}, function(view) { + self._postprocess_fvg(view); + }); + }); + return fields_view; + }, + + /** + * Private function that generates a cache key from its arguments + */ + _gen_key: function () { + return _.map(Array.prototype.slice.call(arguments), function (arg) { + if (!arg) { + return false; + } + return _.isObject(arg) ? JSON.stringify(arg) : arg; + }).join(','); + }, +}); + +}); diff --git a/addons/web/static/src/js/framework/model.js b/addons/web/static/src/js/framework/model.js index 2621ca490d4..fc1422d799a 100644 --- a/addons/web/static/src/js/framework/model.js +++ b/addons/web/static/src/js/framework/model.js @@ -3,7 +3,6 @@ odoo.define('web.Model', function (require) { var Class = require('web.Class'); var session = require('web.session'); -var utils = require('web.utils'); var Model = Class.extend({ /** @@ -70,41 +69,6 @@ var Model = Class.extend({ signal: signal }); }, - /** - * Performs a fields_view_get and apply postprocessing. - * return a {$.Deferred} resolved with the fvg - * - * @param {Object} args - * @param {String|Object} args.model instance.web.Model instance or string repr of the model - * @param {Object} [args.context] context if args.model is a string - * @param {Number} [args.view_id] id of the view to be loaded, default view if null - * @param {String} [args.view_type] type of view to be loaded if view_id is null - * @param {Boolean} [args.toolbar=false] get the toolbar definition - */ - fields_view_get: function (options) { - - return this.call('fields_view_get', { - view_id: options.view_id, - view_type: options.view_type, - context: options.context, - toolbar: options.toolbar || false - }).then(function postprocess(fvg) { - var doc = $.parseXML(fvg.arch).documentElement; - fvg.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban')); - if ('id' in fvg.fields) { - // Special case for id's - var id_field = fvg.fields.id; - id_field.original_type = id_field.type; - id_field.type = 'id'; - } - _.each(fvg.fields, function(field) { - _.each(field.views || {}, function(view) { - postprocess(view); - }); - }); - return fvg; - }); - } }); return Model; diff --git a/addons/web/static/src/js/framework/view.js b/addons/web/static/src/js/framework/view.js index b234d02390f..fbbede916e5 100644 --- a/addons/web/static/src/js/framework/view.js +++ b/addons/web/static/src/js/framework/view.js @@ -3,6 +3,7 @@ odoo.define('web.View', function (require) { var core = require('web.core'); var data = require('web.data'); +var data_manager = require('web.data_manager'); var pyeval = require('web.pyeval'); var Widget = require('web.Widget'); @@ -14,10 +15,11 @@ var View = Widget.extend({ this.do_action(action_data); } }, + defaults: { + action: {}, + }, // name displayed in view switchers display_name: '', - // define a view type for each view to allow automatic call to fields_view_get. - view_type: undefined, // used by views that need a searchview. searchable: true, // used by views that need a searchview but don't want it to be displayed. @@ -27,57 +29,28 @@ var View = Widget.extend({ multi_record: true, // icon is the font-awesome icon to display in the view switcher icon: 'fa-question', - init: function(parent, dataset, view_id, options) { + // whether or not the view requires the model's fields to render itself + require_fields: false, + init: function(parent, dataset, fields_view, options) { this._super(parent); this.ViewManager = parent; this.dataset = dataset; - this.view_id = view_id; - this.set_default_options(options); + this.model = dataset.model; + this.fields_view = fields_view; + this.options = _.defaults({}, options, this.defaults); }, - start: function () { - return this.load_view(); - }, - load_view: function() { - var self = this; - var view_loaded_def; - if (this.embedded_view) { - view_loaded_def = $.Deferred(); - $.async_when().done(function() { - view_loaded_def.resolve(self.embedded_view); - }); - } else { - if (! this.view_type) - console.warn("view_type is not defined", this); - view_loaded_def = this.dataset._model.fields_view_get({ - "view_id": this.view_id, - "view_type": this.view_type, - "toolbar": !!this.options.sidebar, - "context": this.dataset.get_context(), - }); - } - return this.alive(view_loaded_def).then(function(r) { - self.fields_view = r; - // add css classes that reflect the (absence of) access rights - self.$el.addClass('oe_view') - .toggleClass('oe_cannot_create', !self.is_action_enabled('create')) - .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit')) - .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete')); - return $.when(self.view_loading(r)).then(function() { - self.trigger('view_loaded', r); - }); - }); - }, - view_loading: function(r) { - }, - set_default_options: function(options) { - this.options = options || {}; - _.defaults(this.options, { - // All possible views options should be defaulted here - $sidebar: null, - sidebar_id: null, - action: null, - action_views_ids: {} - }); + /** + * Triggers event 'view_loaded'. + * Views extending start() must call this.super() once they are ready. + * @return {Deferred} + */ + start: function() { + // add css classes that reflect the (absence of) access rights + this.$el.addClass('oe_view') + .toggleClass('oe_cannot_create', !this.is_action_enabled('create')) + .toggleClass('oe_cannot_edit', !this.is_action_enabled('edit')) + .toggleClass('oe_cannot_delete', !this.is_action_enabled('delete')); + return this._super().then(this.trigger.bind(this, 'view_loaded')); }, /** * Fetches and executes the action identified by ``action_data``. @@ -87,7 +60,7 @@ var View = Widget.extend({ * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``) * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'`` * @param {Object} [action_data.context=null] additional action context, to add to the current context - * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server + * @param {DataSet} dataset a dataset object used to communicate with the server * @param {Object} [record_id] the identifier of the object on which the action is to be applied * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action) */ @@ -150,56 +123,25 @@ var View = Widget.extend({ core.bus.trigger('do_reload_needaction'); }); } else if (action_data.type=="action") { - return this.rpc('/web/action/load', { - action_id: action_data.name, - context: _.extend(pyeval.eval('context', context), {'active_model': dataset.model, 'active_ids': dataset.ids, 'active_id': record_id}), - do_not_eval: true - }).then(handler); + return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), { + active_model: dataset.model, + active_ids: dataset.ids, + active_id: record_id + })).then(handler); } else { return dataset.exec_workflow(record_id, action_data.name).then(handler); } }, - /** - * Directly set a view to use instead of calling fields_view_get. This method must - * be called before start(). When an embedded view is set, underlying implementations - * of instance.web.View must use the provided view instead of any other one. - * - * @param embedded_view A view. - */ - set_embedded_view: function(embedded_view) { - this.embedded_view = embedded_view; - }, do_show: function () { this._super(); core.bus.trigger('view_shown', this); }, - is_active: function () { - return this.ViewManager.active_view.controller === this; - }, - /** - * Wraps fn to only call it if the current view is the active one. If the - * current view is not active, doesn't call fn. - * - * fn can not return anything, as a non-call to fn can't return anything - * either - * - * @param {Function} fn function to wrap in the active guard - */ - guard_active: function (fn) { - var self = this; - return function () { - if (self.is_active()) { - fn.apply(self, arguments); - } - }; - }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { this.getParent().do_push_state(state); } }, do_load_state: function (state, warm) { - }, /** * This function should render the buttons of the view, set this.$buttons to diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js new file mode 100644 index 00000000000..69fc68908f6 --- /dev/null +++ b/addons/web/static/src/js/services/data_manager.js @@ -0,0 +1,10 @@ +odoo.define('web.data_manager', function (require) { +"use strict"; + +var DataManager = require('web.DataManager'); + +var data_manager = new DataManager(); + +return data_manager; + +}); diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js index 00826c34eb9..0dfab67b4ea 100644 --- a/addons/web/static/src/js/view_manager.js +++ b/addons/web/static/src/js/view_manager.js @@ -3,7 +3,7 @@ odoo.define('web.ViewManager', function (require) { var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); -var data = require('web.data'); +var data_manager = require('web.data_manager'); var framework = require('web.framework'); var Model = require('web.DataModel'); var pyeval = require('web.pyeval'); @@ -17,14 +17,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, { template: "ViewManager", /** * @param {Object} [dataset] - * @param {Array} [views] List of [view_id, view_type] + * @param {Array} [views] List of [view_id, view_type[, fields_view]] * @param {Object} [flags] various boolean describing UI state */ init: function(parent, dataset, views, flags, options) { var self = this; this._super(parent); - this.action = options && options.action; + this.action = options && options.action || {}; this.action_manager = options && options.action_manager; this.flags = flags || {}; this.dataset = dataset; @@ -33,7 +33,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.view_stack = []; // used for breadcrumbs this.active_view = null; this.registry = core.view_registry; - this.title = this.action && this.action.name; + this.title = this.action.name; this.is_in_DOM = false; // used to know if the view manager is attached in the DOM _.each(views, function (view) { var view_type = view[1] || view.view_type; @@ -44,45 +44,42 @@ var ViewManager = Widget.extend(ControlPanelMixin, { } var view_label = View.prototype.display_name; var view_descr = { - controller: null, - options: view.options || {}, - view_id: view[0] || view.view_id, - type: view_type, - label: view_label, - embedded_view: view.embedded_view, - title: self.title, - button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}), - multi_record: View.prototype.multi_record, - accesskey: View.prototype.accesskey, - icon: View.prototype.icon, - }; + accesskey: View.prototype.accesskey, + button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}), + controller: null, + fields_view: view[2] || view.fields_view, + embedded_view: view.embedded_view, + icon: View.prototype.icon, + label: view_label, + multi_record: View.prototype.multi_record, + options: view.options || {}, + require_fields: View.prototype.require_fields, + title: self.title, + type: view_type, + view_id: view[0] || view.view_id, + }; self.view_order.push(view_descr); self.views[view_type] = view_descr; }); this.first_view = this.views[options && options.view_type]; // view to open first - - // Listen to event 'switch_view' indicating that the VM must now display view wiew_type - this.on('switch_view', this, function(view_type) { - if (view_type === 'form' && this.active_view && this.active_view.type === 'form') { - this._display_view(view_type); - } else { - this.switch_mode(view_type); - } - }); + this.default_view = this.get_default_view(); + }, + willStart: function () { + var views_def; + var first_view_to_display = this.first_view || this.default_view; + if (!first_view_to_display.fields_view || (this.flags.search_view && !this.search_fields_view)) { + views_def = this.load_views(first_view_to_display.require_fields); + } + return $.when(this._super(), views_def); }, /** - * @returns {jQuery.Deferred} initial view loading promise + * @return {Deferred} initial view and search view (if any) loading promise */ start: function() { var self = this; - this._super(); - - var views_ids = {}; _.each(this.views, function (view) { - views_ids[view.type] = view.view_id; view.options = _.extend({ - action : self.action, - action_views_ids : views_ids, + action: self.action, }, self.flags, self.flags[view.type], view.options); view.$container = self.$(".oe-view-manager-view-" + view.type); }); @@ -99,16 +96,49 @@ var ViewManager = Widget.extend(ControlPanelMixin, { // If a non multi-record first_view is given, switch to it but first push the default_view // to the view_stack to complete the breadcrumbs - var default_view = this.get_default_view(); - if (this.first_view && !this.first_view.multi_record && default_view.multi_record) { - default_view.controller = this.create_view(default_view); - this.view_stack.push(default_view); + if (this.first_view && !this.first_view.multi_record && this.default_view.multi_record) { + this.default_view.controller = this.create_view(this.default_view); + this.view_stack.push(this.default_view); } - var view_to_load = this.first_view || default_view; + var view_to_load = this.first_view || this.default_view; var options = this.flags[view_to_load] && this.flags[view_to_load].options; var main_view_loaded = this.switch_mode(view_to_load.type, options); - return $.when(main_view_loaded, this.search_view_loaded); + return $.when(this._super(), main_view_loaded, this.search_view_loaded); + }, + /** + * Loads all missing field_views of views in this.views and the search view. + * + * @param {Boolean} [load_fields] whether or not to load the fields as well + * @return {Deferred} + */ + load_views: function (load_fields) { + var self = this; + var views = []; + _.each(this.views, function (view) { + if (!view.fields_view) { + views.push([view.view_id, view.type]); + } + }); + var options = { + action_id: this.action.id, + load_fields: load_fields, + toolbar: this.flags.sidebar, + }; + if (this.flags.search_view && !this.search_fields_view) { + options.load_filters = true; + var searchview_id = this.action.search_view_id && this.action.search_view_id[0]; + views.push([searchview_id || false, 'search']); + } + return data_manager.load_views(this.dataset, views, options).then(function (fields_views) { + _.each(fields_views, function (fields_view, view_type) { + if (view_type === 'search') { + self.search_fields_view = fields_view; + } else { + self.views[view_type].fields_view = fields_view; + } + }); + }); }, /** * Returns the default view with the following fallbacks: @@ -131,51 +161,60 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.currently_switching = true; // prevent overlapping switches } - if (view.multi_record) { - this.view_stack = []; - } else if (this.view_stack.length > 0 && !(_.last(this.view_stack).multi_record)) { - // Replace the last view by the new one if both are mono_record - this.view_stack.pop(); - } - this.view_stack.push(view); - this.active_view = view; - - if (!view.loaded) { - if (!view.controller) { - view.controller = this.create_view(view, view_options); - } - view.$fragment = $('