From 002660a9b05cf8620e1c5287a8538ac5e806d59e Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 16 Mar 2016 09:53:23 +0100 Subject: [PATCH] [IMP] openerp, web(_*): batch views RPCs Load the fields_view of all views of the action together. If a searchview is required, also load its fields_view and its filters. Use lazy-loading for fields as they are required for the Pivot and Graph views only, and may be used by Filters and GroupBy menus of the Search view if the users clicks on 'Add a custom Filter/Group'. The fields are thus also loaded directly if the first view of the action is Pivot or Graph, and otherwise, they are lazy-loaded when/if the user opens a Pivot or Graph view, or if he wants to add a custom Filter/Group. Some refactoring of views API as well: - Views must now be instantiated with their fields_view - Move code requiring fields_view from start() to init(), or willStart() for heavy processing. - Remove view_type attribute from views as it was used to call fields_view_get. - Remove view_id parameter to views' init as it is no longer used. - Move duplicate code from various views' init to the View widget. - Remove view_loading() function from views as they now follow the classical lifecycle of widgets (init -> willStart -> start -> destroy). - Searchview now extends View like all other views. - Remove useless function guard_active() as it was used for the form_view's action buttons to wrap handlers to ensure that they are executed only if the form_view is the current active view. This is not useful anymore as from v9, views and their control panel are appended in the DOM simultaneously, so one can't click on a button of a view which is not the current active view. --- addons/web/static/src/js/compatibility.js | 5 +- addons/web/static/src/js/framework/model.js | 34 +-- addons/web/static/src/js/framework/view.js | 103 ++------ addons/web/static/src/js/view_manager.js | 241 ++++++++++-------- addons/web/static/src/js/views/form_common.js | 151 ++++++----- .../src/js/views/form_relational_widgets.js | 56 ++-- addons/web/static/src/js/views/form_view.js | 114 +++------ addons/web/static/src/js/views/graph_view.js | 42 ++- addons/web/static/src/js/views/list_view.js | 197 ++++++-------- .../static/src/js/views/list_view_editable.js | 13 +- addons/web/static/src/js/views/pivot_view.js | 141 +++++----- .../web/static/src/js/views/search_menus.js | 201 +++++++++------ addons/web/static/src/js/views/search_view.js | 81 +++--- addons/web/static/src/js/views/tree_view.js | 84 +++--- addons/web/static/test/list-editable.js | 70 +++-- addons/web/static/test/list.js | 20 +- addons/web/static/test/search.js | 134 +++++----- .../static/src/js/web_calendar.js | 111 +++----- addons/web_calendar/static/src/js/widgets.js | 41 +-- addons/web_diagram/static/src/js/diagram.js | 74 +++--- .../web_kanban/static/src/js/kanban_view.js | 68 +++-- openerp/models.py | 31 +++ 22 files changed, 929 insertions(+), 1083 deletions(-) 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/model.js b/addons/web/static/src/js/framework/model.js index 2621ca490d4..8e37f14218e 100644 --- a/addons/web/static/src/js/framework/model.js +++ b/addons/web/static/src/js/framework/model.js @@ -82,29 +82,31 @@ var Model = Class.extend({ * @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); - }); + }).then(this.postprocess_fvg.bind(this)); + }, + + postprocess_fvg: function (fvg) { + var self = this; + 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) { + self.postprocess_fvg(view); }); - return fvg; }); - } + 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 29307b61833..b6153475b0b 100644 --- a/addons/web/static/src/js/framework/view.js +++ b/addons/web/static/src/js/framework/view.js @@ -14,10 +14,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 +28,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 +59,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) */ @@ -158,47 +130,16 @@ var View = Widget.extend({ 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/view_manager.js b/addons/web/static/src/js/view_manager.js index 00826c34eb9..0996f2c080c 100644 --- a/addons/web/static/src/js/view_manager.js +++ b/addons/web/static/src/js/view_manager.js @@ -3,7 +3,6 @@ odoo.define('web.ViewManager', function (require) { var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); -var data = require('web.data'); var framework = require('web.framework'); var Model = require('web.DataModel'); var pyeval = require('web.pyeval'); @@ -17,14 +16,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 +32,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 +43,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 +95,58 @@ 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]); + } + }); + if (this.flags.search_view && !this.search_fields_view) { + var searchview_id = this.action.search_view_id && this.action.search_view_id[0]; + views.push([searchview_id || false, 'search']); + } + var options = { + action_id: this.action.id, + load_fields: load_fields && !this.fields_get_def, + toolbar: this.flags.sidebar, + }; + return this.dataset._model.call('load_views', { + views: views, + options: options, + context: this.dataset.get_context(), + }).then(function (result) { + _.each(result.fields_views, function (fields_view, view_type) { + if (view_type === 'search') { + self.search_fields_view = self.dataset._model.postprocess_fvg(fields_view); + } else { + self.views[view_type].fields_view = self.dataset._model.postprocess_fvg(fields_view); + } + }); + if (result.filters) { + self.search_filters = result.filters; + } + if (result.fields) { + self.fields_get_def = $.when(result.fields); + } + }); }, /** * Returns the default view with the following fallbacks: @@ -120,6 +158,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, { get_default_view: function() { return this.views[this.flags.default_view || this.view_order[0].type]; }, + get_fields: function() { + if (!this.fields_get_def) { + this.fields_get_def = this.dataset._model.call('fields_get', { + context: this.dataset.get_context() + }); + } + return this.fields_get_def; + }, switch_mode: function(view_type, view_options) { var self = this; var view = this.views[view_type]; @@ -131,51 +177,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 = $('
'); - view.loaded = view.controller.appendTo(view.$fragment).done(function () { - // Remove the unnecessary outer div - view.$fragment = view.$fragment.contents(); - self.trigger("controller_inited", view.type, view.controller); - }); + // Ensure that the fields_view has been loaded + var views_def; + if (!view.fields_view) { + views_def = this.load_views(view.require_fields); } - // Call do_search on the searchview to compute domains, contexts and groupbys - if (this.search_view_loaded && - this.flags.auto_search && - view.controller.searchable !== false) { - this.active_search = $.Deferred(); - $.when(this.search_view_loaded, view.loaded).done(function() { - self.searchview.do_search(); - }); - } - - return $.when(view.loaded, this.active_search) - .then(function() { - return self._display_view(view_options, old_view).then(function() { - self.trigger('switch_mode', view_type, view_options); - }); - }).fail(function(e) { - if (!(e && e.code === 200 && e.data.exception_type)) { - self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded")); - } - // Restore internal state - self.active_view = old_view; + return $.when(views_def).then(function () { + if (view.multi_record) { + self.view_stack = []; + } else if (self.view_stack.length > 0 && !(_.last(self.view_stack).multi_record)) { + // Replace the last view by the new one if both are mono_record self.view_stack.pop(); - }).always(function () { - self.currently_switching = false; + } + self.view_stack.push(view); + + self.active_view = view; + + if (!view.loaded) { + if (!view.controller) { + view.controller = self.create_view(view, view_options); + } + view.$fragment = $('
'); + view.loaded = view.controller.appendTo(view.$fragment).done(function () { + // Remove the unnecessary outer div + view.$fragment = view.$fragment.contents(); + self.trigger("controller_inited", view.type, view.controller); + }); + } + + // Call do_search on the searchview to compute domains, contexts and groupbys + if (self.search_view_loaded && + self.flags.auto_search && + view.controller.searchable !== false) { + self.active_search = $.Deferred(); + $.when(self.search_view_loaded, view.loaded).done(function() { + self.searchview.do_search(); + }); + } + + return $.when(view.loaded, self.active_search) + .then(function() { + return self._display_view(view_options, old_view).then(function() { + self.trigger('switch_mode', view_type, view_options); + }); + }).fail(function(e) { + if (!(e && e.code === 200 && e.data.exception_type)) { + self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded")); + } + // Restore internal state + self.active_view = old_view; + self.view_stack.pop(); + }); + }).always(function () { + self.currently_switching = false; }); }, _display_view: function (view_options, old_view) { @@ -218,17 +273,12 @@ var ViewManager = Widget.extend(ControlPanelMixin, { var self = this; var View = this.registry.get(view.type); var options = _.clone(view.options); - - if (view.type === "form" && ((this.action && (this.action.target === 'new' || this.action.target === 'inline')) || + if (view.type === "form" && ((this.action.target === 'new' || this.action.target === 'inline') || (view_options && view_options.mode === 'edit'))) { options.initial_mode = options.initial_mode || 'edit'; } - var controller = new View(this, this.dataset, view.view_id, options); - - if (view.embedded_view) { - controller.set_embedded_view(view.embedded_view); - } + var controller = new View(this, this.dataset, view.fields_view, options); controller.on('switch_mode', this, this.switch_mode.bind(this)); controller.on('history_back', this, function () { @@ -306,12 +356,6 @@ var ViewManager = Widget.extend(ControlPanelMixin, { } return this.active_view.control_elements; }, - /** - * @returns {Number|Boolean} the view id of the given type, false if not found - */ - get_view_id: function(view_type) { - return this.views[view_type] && this.views[view_type].view_id || false; - }, /** * Sets up the current viewmanager's search view. * Sets $searchview and $searchview_buttons in control_elements to send to the ControlPanel @@ -325,28 +369,25 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.searchview.destroy(); } - var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; - var search_defaults = {}; - - var context = this.action ? this.action.context : []; + var context = this.action.context || []; _.each(context, function (value, key) { var match = /^search_default_(.*)$/.exec(key); if (match) { search_defaults[match[1]] = value; } }); - - var options = { hidden: this.flags.search_view === false, disable_custom_filters: this.flags.search_disable_custom_filters, $buttons: $("
"), action: this.action, + search_defaults: search_defaults, + filters: this.search_filters, }; // Instantiate the SearchView, but do not append it nor its buttons to the DOM as this will // be done later, simultaneously to all other ControlPanel elements - this.searchview = new SearchView(this, this.dataset, view_id, search_defaults, options); + this.searchview = new SearchView(this, this.dataset, this.search_fields_view, options); this.searchview.on('search_data', this, this.search.bind(this)); return $.when(this.searchview.appendTo($("
"))).done(function() { diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js index 7364eeec203..1b29a2819b9 100644 --- a/addons/web/static/src/js/views/form_common.js +++ b/addons/web/static/src/js/views/form_common.js @@ -769,7 +769,7 @@ var AbstractField = FormWidget.extend(FieldInterface, { on_translate: function() { var self = this; var trans = new data.DataSet(this, 'ir.translation'); - return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) { + return trans.call_button('translate_fields', [this.view.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) { self.do_action(r); }); }, @@ -793,7 +793,6 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager * options: * -readonly: only applicable when not in creation mode, default to false * - alternative_form_view - * - view_id * - write_function * - read_function * - create_function @@ -814,7 +813,6 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager this.context = options.context || {}; this.options = _.extend(this.options || {}, options || {}); - this.on('closed', this, this.select); this.on_selected = options.on_selected || (function() {}); }, @@ -838,6 +836,8 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager }; this.dataset.parent_view = this.options.parent_view; this.dataset.child_name = this.options.child_name; + + this.on('closed', this, this.select); }, select: function() { @@ -898,33 +898,41 @@ var FormViewDialog = ViewDialog.extend({ open: function() { var self = this; - self._super(); - self.init_dataset(); + var _super = this._super.bind(this); + this.init_dataset(); - if (self.res_id) { - self.dataset.ids = [self.res_id]; - self.dataset.index = 0; + if (this.res_id) { + this.dataset.ids = [this.res_id]; + this.dataset.index = 0; } else { - self.dataset.index = null; + this.dataset.index = null; } - var options = _.clone(self.options.form_view_options) || {}; - if (self.res_id !== null) { - options.initial_mode = self.options.readonly ? "view" : "edit"; + var options = _.clone(this.options.form_view_options) || {}; + if (this.res_id !== null) { + options.initial_mode = this.options.readonly ? "view" : "edit"; } _.extend(options, { - $buttons: self.$buttons, + $buttons: this.$buttons, }); var FormView = core.view_registry.get('form'); - self.view_form = new FormView(self, self.dataset, self.options.view_id || false, options); - if (self.options.alternative_form_view) { - self.view_form.set_embedded_view(self.options.alternative_form_view); + var fields_view_def; + if (this.options.alternative_form_view) { + fields_view_def = $.when(this.options.alternative_form_view); + } else { + fields_view_def = this.dataset._model.fields_view_get({ + view_id: this.options.view_id || false, + view_type: 'form', + context: this.dataset.get_context(), + toolbar: false, + }); } - - self.do_hide(); - self.view_form.appendTo(self.$el); - self.view_form.on("form_view_loaded", self, function() { - self.view_form.do_show().then(function() { - self.do_show(); + fields_view_def.then(function (fields_view) { + self.view_form = new FormView(self, self.dataset, fields_view, options); + var fragment = document.createDocumentFragment(); + self.view_form.appendTo(fragment).then(function () { + self.view_form.do_show().then(function() { + _super().$el.append(fragment); + }); }); }); @@ -965,49 +973,64 @@ var SelectCreateDialog = ViewDialog.extend({ }, open: function() { - this._super(); - this.init_dataset(); - if (this.options.initial_view == "search") { - var context = pyeval.sync_eval_domains_and_contexts({ - domains: [], - contexts: [this.context] - }).context; - var search_defaults = {}; - _.each(context, function (value_, key) { - var match = /^search_default_(.*)$/.exec(key); - if (match) { - search_defaults[match[1]] = value_; - } - }); - this.setup(search_defaults); - } else { // "form" - this.create_edit_record(); + if(this.options.initial_view !== "search") { + return this.create_edit_record(); } + + var self = this; + var _super = this._super.bind(this); + this.init_dataset(); + var context = pyeval.sync_eval_domains_and_contexts({ + domains: [], + contexts: [this.context] + }).context; + var search_defaults = {}; + _.each(context, function (value_, key) { + var match = /^search_default_(.*)$/.exec(key); + if (match) { + search_defaults[match[1]] = value_; + } + }); + this.dataset._model.call('load_views', { + views: [[false, 'list'], [false, 'search']], + context: this.dataset.get_context(), + }).then(function (result) { + var fields_views = _.mapObject(result.fields_views, self.dataset._model.postprocess_fvg); + self.setup(search_defaults, fields_views).then(function (fragment) { + _super().$el.append(fragment); + }); + }); return this; }, - setup: function(search_defaults) { + setup: function(search_defaults, fields_views) { var self = this; if (this.searchview) { this.searchview.destroy(); } - var $header = $('
').addClass('o_modal_header').appendTo(this.$el); + var fragment = document.createDocumentFragment(); + var $header = $('
').addClass('o_modal_header').appendTo(fragment); var $pager = $('
').addClass('o_pager').appendTo($header); - var $buttons = $('
').addClass('o_search_options').appendTo($header); - this.searchview = new SearchView(this, this.dataset, false, search_defaults, {$buttons: $buttons}); - this.searchview.on('search_data', self, function(domains, contexts, groupbys) { - if (self.initial_ids) { - self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]), - contexts.concat(self.context), groupbys); - self.initial_ids = undefined; + var options = { + $buttons: $('
').addClass('o_search_options').appendTo($header), + search_defaults: search_defaults, + }; + + this.searchview = new SearchView(this, this.dataset, fields_views.search, options); + this.searchview.on('search_data', this, function(domains, contexts, groupbys) { + if (this.initial_ids) { + this.do_search(domains.concat([[["id", "in", this.initial_ids]], this.domain]), + contexts.concat(this.context), groupbys); + this.initial_ids = undefined; } else { - self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys); + this.do_search(domains.concat([this.domain]), contexts.concat(this.context), groupbys); } }); - this.searchview.prependTo($header).done(function() { + return this.searchview.prependTo($header).then(function() { self.searchview.toggle_visibility(true); + self.view_list = new SelectCreateListView(self, - self.dataset, false, + self.dataset, fields_views.list, _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection, 'import_enabled': false, @@ -1019,17 +1042,6 @@ var SelectCreateDialog = ViewDialog.extend({ e.cancel = true; }); self.view_list.popup = self; - self.view_list.appendTo(self.$el).then(function() { - self.view_list.do_show(); - self.view_list.render_pager($pager); - }).then(function() { - if (self.options.initial_facet) { - self.searchview.query.reset([self.options.initial_facet], { - preventSearch: true, - }); - } - self.searchview.do_search(); - }); var buttons = [ {text: _t("Cancel"), classes: "btn-default o_form_button_cancel", close: true} @@ -1045,6 +1057,19 @@ var SelectCreateDialog = ViewDialog.extend({ }}); } self.set_buttons(buttons); + + return self.view_list.appendTo(fragment).then(function() { + self.view_list.do_show(); + self.view_list.render_pager($pager); + if (self.options.initial_facet) { + self.searchview.query.reset([self.options.initial_facet], { + preventSearch: true, + }); + } + self.searchview.do_search(); + + return fragment; + }); }); }, do_search: function(domains, contexts, groupbys) { @@ -1060,8 +1085,8 @@ var SelectCreateDialog = ViewDialog.extend({ this.$footer.find(".o_selectcreatepopup_search_select").prop('disabled', this.selected_ids.length <= 0); }, create_edit_record: function() { - new FormViewDialog(this.getParent(), this.options).open(); this.close(); + return new FormViewDialog(this.__parentedParent, this.options).open(); }, }); diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index 79318721efa..3702b67f976 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -12,7 +12,6 @@ var Model = require('web.DataModel'); var session = require('web.session'); var utils = require('web.utils'); var ViewManager = require('web.ViewManager'); -var formats = require('web.formats'); var _t = core._t; var QWeb = core.qweb; @@ -752,11 +751,9 @@ var FieldX2Many = AbstractManyField.extend({ var view = { view_id: false, view_type: view_type === "tree" ? "list" : view_type, - options: {} + fields_view: self.field.views && self.field.views[view_type], + options: {}, }; - if (self.field.views && self.field.views[view_type]) { - view.embedded_view = self.field.views[view_type]; - } if(view.view_type === "list") { _.extend(view.options, { action_buttons: false, // to avoid 'Save' and 'Discard' buttons to appear in X2M fields @@ -825,9 +822,7 @@ var FieldX2Many = AbstractManyField.extend({ }); }); $.async_when().done(function () { - if (!self.isDestroyed()) { - self.viewmanager.appendTo(self.$el); - } + self.alive( self.viewmanager.appendTo(self.$el)); }); return def; }, @@ -900,7 +895,7 @@ var X2ManyDataSet = data.BufferedDataSet.extend({ var X2ManyViewManager = ViewManager.extend({ init: function(parent, dataset, views, flags, x2many_views) { // By default, render buttons and pager in X2M fields, but no sidebar - var flags = _.extend({}, flags, { + flags = _.extend({}, flags, { headless: false, search_view: false, action_buttons: true, @@ -926,7 +921,6 @@ var X2ManyViewManager = ViewManager.extend({ res_model: self.x2m.field.relation, res_id: id, context: self.x2m.build_context(), - title: _t("Open: ") + self.x2m.string, create_function: function(data, options) { return self.x2m.data_create(data, options); @@ -953,7 +947,6 @@ var X2ManyViewManager = ViewManager.extend({ var X2ManyListView = ListView.extend({ is_valid: function () { - var self = this; if (!this.fields_view || !this.editable()){ return true; } @@ -967,7 +960,7 @@ var X2ManyListView = ListView.extend({ field.no_rerender = true; current_values[field.name] = field.get('value'); }); - var cached_records = _.filter(this.dataset.cache, function(item){return !_.isEmpty(item.values)}); + var cached_records = _.filter(this.dataset.cache, function(item){return !_.isEmpty(item.values);}); var valid = _.every(cached_records, function(record){ _.each(fields, function(field){ var value = record.values[field.name]; @@ -1019,14 +1012,16 @@ var X2ManyList = ListView.List.extend({ .click(function (e) { e.preventDefault(); e.stopPropagation(); - // FIXME: there should also be an API for that one - if (self.view.editor.form.__blur_timeout) { - clearTimeout(self.view.editor.form.__blur_timeout); - self.view.editor.form.__blur_timeout = false; + var def; + if (self.view.editable()) { + // FIXME: there should also be an API for that one + if (self.view.editor.form.__blur_timeout) { + clearTimeout(self.view.editor.form.__blur_timeout); + self.view.editor.form.__blur_timeout = false; + } + def = self.view.save_edition(); } - self.view.save_edition().done(function () { - self.view.do_add_record(); - }); + $.when(def).done(self.view.do_add_record.bind(self)); })); var $padding = this.$current.find('tr:not([data-id]):first'); @@ -1041,12 +1036,12 @@ var X2ManyList = ListView.List.extend({ var One2ManyListView = X2ManyListView.extend({ _template: 'One2Many.listview', - init: function (parent, dataset, view_id, options) { - var self = this; - this._super(parent, dataset, view_id, _.extend(options || {}, { + init: function () { + this._super.apply(this, arguments); + this.options = _.extend(this.options, { GroupsType: One2ManyGroups, ListType: X2ManyList - })); + }); this.on('edit:after', this, this.proxy('_after_edit')); this.on('save:before cancel:before', this, this.proxy('_before_unedit')); @@ -1293,15 +1288,12 @@ var FieldOne2Many = FieldX2Many.extend({ }, }); -/** - * @class - * @extends instance.web.ListView - */ -var Many2ManyListView = X2ManyListView.extend(/** @lends instance.web.form.Many2ManyListView# */{ - init: function (parent, dataset, view_id, options) { - this._super(parent, dataset, view_id, _.extend(options || {}, { +var Many2ManyListView = X2ManyListView.extend({ + init: function () { + this._super.apply(this, arguments); + this.options = _.extend(this.options, { ListType: X2ManyList, - })); + }); }, do_add_record: function () { var self = this; @@ -1323,7 +1315,7 @@ var Many2ManyListView = X2ManyListView.extend(/** @lends instance.web.form.Many2 do_activate_record: function(index, id) { var self = this; var pop = new common.FormViewDialog(this, { - res_model: this.dataset.model, + res_model: this.model, res_id: id, context: this.x2m.build_context(), title: _t("Open: ") + this.x2m.string, diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index 96ae03eab2f..8e973cfd779 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -16,45 +16,29 @@ var _lt = core._lt; var QWeb = core.qweb; var Class = core.Class; -/** @namespace */ -// instance.web.form = instance.web.form || {}; - /** * Properties: * - actual_mode: always "view", "edit" or "create". Read-only property. Determines * the mode used by the view. */ var FormView = View.extend(common.FieldManagerMixin, { - /** - * Indicates that this view is not searchable, and thus that no search - * view should be displayed (if there is one active). - */ + accesskey: "F", + defaults: _.extend({}, View.prototype.defaults, { + not_interactible_on_create: false, + initial_mode: "view", + disable_autofocus: false, + footer_to_buttons: false, + }), + display_name: _lt('Form'), + icon: 'fa-edit', + multi_record: false, + // Indicates that this view is not searchable, and thus that no search view should be displayed. searchable: false, template: "FormView", - display_name: _lt('Form'), - view_type: "form", - multi_record: false, - accesskey: "F", - icon: 'fa-edit', - /** - * @constructs instance.web.FormView - * @extends instance.web.View - * - * @param {instance.web.Session} session the current openerp session - * @param {instance.web.DataSet} dataset the dataset this view will work with - * @param {String} view_id the identifier of the OpenERP view object - * - * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance - */ - init: function(parent, dataset, view_id, options) { + + init: function() { var self = this; - this._super(parent); - this.ViewManager = parent; - this.set_default_options(options); - this.dataset = dataset; - this.model = dataset.model; - this.view_id = view_id || false; - this.fields_view = {}; + this._super.apply(this, arguments); this.fields = {}; this.fields_order = []; this.datarecord = {}; @@ -67,12 +51,6 @@ var FormView = View.extend(common.FieldManagerMixin, { this.widgets_registry = core.form_custom_registry; this.has_been_loaded = $.Deferred(); this.translatable_fields = []; - _.defaults(this.options, { - "not_interactible_on_create": false, - "initial_mode": "view", - "disable_autofocus": false, - "footer_to_buttons": false, - }); this.is_initialized = $.Deferred(); this.mutating_mutex = new utils.Mutex(); this.save_list = []; @@ -104,34 +82,13 @@ var FormView = View.extend(common.FieldManagerMixin, { }); }); }, - view_loading: function(r) { - return this.load_form(r); - }, - destroy: function() { - _.each(this.get_widgets(), function(w) { - w.off('focused blurred'); - w.destroy(); - }); - if (this.$el) { - this.$el.off('.formBlur'); - } - this._super(); - }, - load_form: function(data) { + start: function() { var self = this; - if (!data) { - throw new Error(_t("No data provided.")); - } - if (this.arch) { - throw "Form view does not support multiple calls to load_form"; - } - this.fields_order = []; - this.fields_view = data; this.rendering_engine.set_fields_registry(this.fields_registry); this.rendering_engine.set_tags_registry(this.tags_registry); this.rendering_engine.set_widgets_registry(this.widgets_registry); - this.rendering_engine.set_fields_view(data); + this.rendering_engine.set_fields_view(this.fields_view); var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); this.rendering_engine.render_to($dest); @@ -143,7 +100,7 @@ var FormView = View.extend(common.FieldManagerMixin, { // Add bounce effect on button 'Edit' when click on readonly page view. this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) { - if(self.get("actual_mode") == "view" && self.$buttons && !$(e.target).is('[data-toggle]')) { + if(self.get("actual_mode") === "view" && self.$buttons && !$(e.target).is('[data-toggle]')) { var $button = self.$buttons.find(".oe_form_button_edit"); $button.openerpBounce(); e.stopPropagation(); @@ -152,14 +109,23 @@ var FormView = View.extend(common.FieldManagerMixin, { }); //bounce effect on red button when click on statusbar. this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) { - if((self.get("actual_mode") == "view")) { + if((self.get("actual_mode") === "view")) { var $button = self.$el.find(".oe_highlight:not(.o_form_invisible)").css({'float':'left','clear':'none'}); $button.openerpBounce(); e.stopPropagation(); } }); - this.trigger('form_view_loaded', data); - return $.when(); + return this._super(); + }, + destroy: function() { + _.each(this.get_widgets(), function(w) { + w.off('focused blurred'); + w.destroy(); + }); + if (this.$el) { + this.$el.off('.formBlur'); + } + this._super(); }, /** * Render the buttons according to the FormView.buttons template and add listeners on it. @@ -181,14 +147,10 @@ var FormView = View.extend(common.FieldManagerMixin, { // Show or hide the buttons according to the view mode this.toggle_buttons(); - this.$buttons.on('click', '.oe_form_button_create', - this.guard_active(this.on_button_create)); - this.$buttons.on('click', '.oe_form_button_edit', - this.guard_active(this.on_button_edit)); - this.$buttons.on('click', '.oe_form_button_save', - this.guard_active(this.on_button_save)); - this.$buttons.on('click', '.oe_form_button_cancel', - this.guard_active(this.on_button_cancel)); + this.$buttons.on('click', '.oe_form_button_create', this.on_button_create); + this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit); + this.$buttons.on('click', '.oe_form_button_save', this.on_button_save); + this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel); if ($node) { this.$buttons.appendTo($node); @@ -311,10 +273,8 @@ var FormView = View.extend(common.FieldManagerMixin, { } }, /** - * - * @param {Object} [options] - * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". - * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record + * @param {Boolean} [options.mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". + * @param {Boolean} [options.reload=true] whether the form should reload its content on show, or use the currently loaded record * @return {$.Deferred} */ do_show: function (options) { @@ -521,7 +481,7 @@ var FormView = View.extend(common.FieldManagerMixin, { // In case of a o2m virtual id, we should pass an empty ids list ids.push(self.datarecord.id); } - def = self.alive(new Model(self.dataset.model).call( + def = self.alive(self.dataset.call( "onchange", [ids, values, trigger_field_name, onchange_specs, context])); } this.onchanges_mutex.exec(function(){ @@ -1199,7 +1159,7 @@ var FormView = View.extend(common.FieldManagerMixin, { all_users = d.$el.find('#formview_default_all').is(':checked'); new data.DataSet(self, 'ir.values').call( 'set_default', [ - self.dataset.model, + self.model, field_to_set, self.fields[field_to_set].get_value(), all_users, diff --git a/addons/web/static/src/js/views/graph_view.js b/addons/web/static/src/js/views/graph_view.js index 8a604640fc9..1df6cd14063 100644 --- a/addons/web/static/src/js/views/graph_view.js +++ b/addons/web/static/src/js/views/graph_view.js @@ -6,7 +6,6 @@ odoo.define('web.GraphView', function (require) { var core = require('web.core'); var GraphWidget = require('web.GraphWidget'); -var Model = require('web.DataModel'); var View = require('web.View'); var _lt = core._lt; @@ -17,21 +16,30 @@ var GraphView = View.extend({ className: 'oe_graph', display_name: _lt('Graph'), icon: 'fa-bar-chart', - view_type: 'graph', + require_fields: true, - init: function(parent, dataset, view_id, options) { - this._super(parent, dataset, view_id, options); + init: function () { + this._super.apply(this, arguments); - this.model = new Model(dataset.model, {group_by_no_leaf: true}); this.measures = []; this.active_measure = '__count__'; this.initial_groupbys = []; this.widget = undefined; }, - start: function () { - var load_fields = this.model.call('fields_get', [], {context: this.dataset.get_context()}) - .then(this.prepare_fields.bind(this)); - + willStart: function () { + var self = this; + var load_fields = this.ViewManager.get_fields().then(this.prepare_fields.bind(this)); + this.fields_view.arch.children.forEach(function (field) { + var name = field.attrs.name; + if (field.attrs.interval) { + name += ':' + field.attrs.interval; + } + if (field.attrs.type === 'measure') { + self.active_measure = name; + } else { + self.initial_groupbys.push(name); + } + }); return $.when(this._super(), load_fields); }, /** @@ -59,20 +67,6 @@ var GraphView = View.extend({ $(li).toggleClass('selected', $(li).data('field') === self.active_measure); }); }, - view_loading: function (fvg) { - var self = this; - fvg.arch.children.forEach(function (field) { - var name = field.attrs.name; - if (field.attrs.interval) { - name += ':' + field.attrs.interval; - } - if (field.attrs.type === 'measure') { - self.active_measure = name; - } else { - self.initial_groupbys.push(name); - } - }); - }, do_show: function () { this.do_push_state({}); return this._super(); @@ -92,7 +86,7 @@ var GraphView = View.extend({ do_search: function (domain, context, group_by) { if (!this.widget) { this.initial_groupbys = context.graph_groupbys || this.initial_groupbys; - this.widget = new GraphWidget(this, this.dataset.model, { + this.widget = new GraphWidget(this, this.model, { measure: context.graph_measure || this.active_measure, mode: context.graph_mode || this.active_mode, domain: domain, diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index b8d0c569609..d42d34a98d2 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -32,33 +32,33 @@ var row_decoration = [ 'decoration-warning' ]; -var ListView = View.extend( /** @lends instance.web.ListView# */ { +var ListView = View.extend({ _template: 'ListView', accesskey: 'L', - icon: 'fa-list-ul', display_name: _lt('List'), - defaults: { + defaults: _.extend({}, View.prototype.defaults, { // records can be selected one by one - 'selectable': true, + selectable: true, // list rows can be deleted - 'deletable': false, + deletable: false, // whether the column headers should be displayed - 'header': true, + header: true, // display addition button, with that label - 'addable': _lt("Create"), + addable: _lt("Create"), // whether the list view can be sorted, note that once a view has been // sorted it can not be reordered anymore - 'sortable': true, + sortable: true, // whether the view rows can be reordered (via vertical drag & drop) - 'reorderable': true, - 'action_buttons': true, + reorderable: true, + action_buttons: true, //whether the editable property of the view has to be disabled - 'disable_editable_mode': false, - }, - view_type: 'tree', + disable_editable_mode: false, + }), events: { 'click thead th.oe_sortable[data-id]': 'sort_by_column' }, + icon: 'fa-list-ul', + /** * Core class for list-type displays. * @@ -69,11 +69,11 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { * See constructor parameters and method documentations for information on * the default behaviors and possible options for the list view. * - * @constructs instance.web.ListView - * @extends instance.web.View + * @constructs ListView + * @extends View * * @param parent parent object - * @param {instance.web.DataSet} dataset the dataset the view should work with + * @param {DataSet} dataset the dataset the view should work with * @param {String} view_id the listview's identifier, if any * @param {Object} options A set of options used to configure the view * @param {Boolean} [options.selectable=true] determines whether view rows are selectable (e.g. via a checkbox) @@ -83,13 +83,14 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { * @param {Boolean} [options.sortable=true] is it possible to sort the table by clicking on column headers * @param {Boolean} [options.reorderable=true] is it possible to reorder list rows */ - init: function(parent, dataset, view_id, options) { + init: function() { var self = this; - this._super(parent); - this.set_default_options(_.extend({}, this.defaults, options || {})); - this.dataset = dataset; - this.model = dataset.model; - this.view_id = view_id; + this._super.apply(this, arguments); + this.options = _.defaults(this.options, { + GroupsType: ListView.Groups, + ListType: ListView.List, + }); + this.previous_colspan = null; this.colors = null; this.fonts = null; @@ -105,8 +106,8 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { } else { this.groups.datagroup = new DataGroup( this, this.model, - dataset.get_domain(), - dataset.get_context()); + this.dataset.get_domain(), + this.dataset.get_context()); this.groups.datagroup.sort = this.dataset._sort; } @@ -125,21 +126,22 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { if (!this.options.$pager || !this.options.$pager.length) { this.options.$pager = false; } + + this.name = "" + this.fields_view.arch.attrs.string; + // the view's number of records per page (|| section) this._limit = (this.options.limit || this.defaults.limit || (this.getParent().action || {}).limit || + parseInt(this.fields_view.arch.attrs.limit, 10) || 80); - }, - view_loading: function(r) { - return this.load_list(r); - }, - set_default_options: function (options) { - this._super(options); - _.defaults(this.options, { - GroupsType: ListView.Groups, - ListType: ListView.List - }); + + //Sort + var default_order = this.fields_view.arch.attrs.default_order, + unsorted = !this.dataset._sort.length; + if (unsorted && default_order && !this.grouped) { + this.dataset.set_sort(default_order.split(',')); + } }, /** * Set a custom Group construct as the root of the List View. @@ -169,14 +171,41 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { } }); }, - /** - * View startup method, the default behavior is to set the ``oe_list`` - * class on its root element and to perform an RPC load call. - * - * @returns {$.Deferred} loading promise - */ + willStart: function() { + // Retrieve the decoration defined on the model's list view + this.decoration = _.pick(this.fields_view.arch.attrs, function(value, key) { + return row_decoration.indexOf(key) >= 0; + }); + this.decoration = _.mapObject(this.decoration, function(value) { + return py.parse(py.tokenize(value)); + }); + + this.setup_columns(this.fields_view.fields, this.grouped); + + return this._super(); + }, start: function() { + var self = this; + this.$el.html(QWeb.render(this._template, this)); + this.$el.addClass(this.fields_view.arch.attrs['class']); this.$el.addClass('oe_list o_list_view'); + + // Selecting records + this.$('.oe_list_record_selector').click(function() { + var checked = self.$('.oe_list_record_selector').prop('checked'); + self.$('.oe_list_record_selector input').prop('checked', checked || false); + var selection = self.groups.get_selection(); + $(self.groups).trigger('selected', [selection.ids, selection.records]); + }); + + if(this.dataset._sort.length){ + if(this.dataset._sort[0].indexOf('-') === -1){ + this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown"); + }else { + this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup"); + } + } + this.trigger('list_view_loaded', data, this.grouped); return this._super(); }, /** @@ -201,73 +230,6 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { }); return classnames; }, - /** - * Called after loading the list view's description, sets up such things - * as the view table's columns, renders the table itself and hooks up the - * various table-level and row-level DOM events (action buttons, deletion - * buttons, selection of records, [New] button, selection of a given - * record, ...) - * - * Sets up the following: - * - * * Processes arch and fields to generate a complete field descriptor for each field - * * Create the table itself and allocate visible columns - * * Hook in the top-level (header) [New|Add] and [Delete] button - * * Sets up showing/hiding the top-level [Delete] button based on records being selected or not - * * Sets up event handlers for action buttons and per-row deletion button - * * Hooks global callback for clicking on a row - * - * @param {Object} data wrapped fields_view_get result - * @param {Object} data.fields_view fields_view_get result (processed) - * @param {Object} data.fields_view.fields mapping of fields for the current model - * @param {Object} data.fields_view.arch current list view descriptor - * @param {Boolean} grouped Is the list view grouped - */ - load_list: function(data) { - var self = this; - this.fields_view = data; - this.name = "" + this.fields_view.arch.attrs.string; - this._limit = parseInt(this.fields_view.arch.attrs.limit, 10) || this._limit; - - // Retrieve the decoration defined on the model's list view - this.decoration = _.pick(this.fields_view.arch.attrs, function(value, key) { - return row_decoration.indexOf(key) >= 0; - }); - this.decoration = _.mapObject(this.decoration, function(value) { - return py.parse(py.tokenize(value)); - }); - - this.setup_columns(this.fields_view.fields, this.grouped); - - this.$el.html(QWeb.render(this._template, this)); - this.$el.addClass(this.fields_view.arch.attrs['class']); - - // Head hook - // Selecting records - this.$el.find('.oe_list_record_selector').click(function(){ - self.$el.find('.oe_list_record_selector input').prop('checked', - self.$el.find('.oe_list_record_selector').prop('checked') || false); - var selection = self.groups.get_selection(); - $(self.groups).trigger( - 'selected', [selection.ids, selection.records]); - }); - - //Sort - var default_order = this.fields_view.arch.attrs.default_order, - unsorted = !this.dataset._sort.length; - if (unsorted && default_order && !this.grouped) { - this.dataset.set_sort(default_order.split(',')); - } - - if(this.dataset._sort.length){ - if(this.dataset._sort[0].indexOf('-') == -1){ - this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown"); - }else { - this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup"); - } - } - this.trigger('list_view_loaded', data, this.grouped); - }, /** * Render the buttons according to the ListView.buttons template and * add listeners on it. @@ -499,16 +461,6 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { this.sidebar.do_hide(); } }, - /** - * Reloads the list view based on the current settings (dataset & al) - * - * @deprecated - * @param {Boolean} [grouped] Should the list be displayed grouped - * @param {Object} [context] context to send the server while loading the view - */ - reload_view: function (grouped, context) { - return this.load_view(context); - }, /** * re-renders the content of the list view * @@ -617,8 +569,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { this.no_leaf = !!context['group_by_no_leaf']; this.grouped = !!group_by; - return this.alive(this.load_view(context)).then( - this.proxy('reload_content')); + return this.reload_content(); }, /** * Handles the signal to delete lines from the records list @@ -720,7 +671,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { c.set_eval_context(_.extend({ active_id: id, active_ids: [id], - active_model: this.dataset.model + active_model: this.model }, this.records.get(id).toContext())); if (action.context) { c.add(action.context); @@ -960,7 +911,7 @@ var ListView = View.extend( /** @lends instance.web.ListView# */ { }); core.view_registry.add('list', ListView); -ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{ +ListView.List = Class.extend({ /** * List display for the ListView, handles basic DOM events and transforms * them in the relevant higher-level events, to which the list view (or @@ -1187,7 +1138,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{ } } return column.format(record.toForm().data, { - model: this.dataset.model, + model: this.model, id: record.get('id') }); }, @@ -1299,7 +1250,7 @@ ListView.List = Class.extend( /** @lends instance.web.ListView.List# */{ } }); -ListView.Groups = Class.extend( /** @lends instance.web.ListView.Groups# */{ +ListView.Groups = Class.extend({ passthrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js index 2bd6dc1ab25..627e033840e 100644 --- a/addons/web/static/src/js/views/list_view_editable.js +++ b/addons/web/static/src/js/views/list_view_editable.js @@ -38,19 +38,18 @@ var Editor = Widget.extend({ delegate: this.getParent(), }); this.delegate = this.options.delegate; - this.record = null; - this.form = new (this.options.formView)(this, this.delegate.dataset, false, { + }, + start: function () { + var self = this; + var form_fields_view = this._validate_view(this.delegate.edition_view(this)); + this.form = new (this.options.formView)(this, this.delegate.dataset, form_fields_view, { initial_mode: 'edit', is_list_editable: true, disable_autofocus: true, $buttons: $(), $pager: $(), }); - }, - start: function () { - var self = this; - this.form.embedded_view = this._validate_view(this.delegate.edition_view(this)); return $.when(this._super(), this.form.appendTo($('
')).then(function() { self.form.$el.addClass(self.$el.attr('class')); self.replaceElement(self.form.$el); @@ -258,7 +257,7 @@ ListView.include(/** @lends instance.web.ListView# */{ this._super.apply(this, arguments); } }, - load_list: function (data, grouped) { + start: function () { // tree/@editable takes priority on everything else if present. var result = this._super.apply(this, arguments); diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index 7adc806c61f..5559906e905 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -19,10 +19,6 @@ var _t = core._t; var QWeb = core.qweb; var PivotView = View.extend({ - template: 'PivotView', - icon: 'fa-table', - display_name: _lt('Pivot'), - view_type: 'pivot', events: { 'click .oe-opened': 'on_open_header_click', 'click .oe-closed': 'on_closed_header_click', @@ -30,17 +26,20 @@ var PivotView = View.extend({ 'click td': 'on_cell_click', 'click .measure-row': 'on_measure_row_click', }, + display_name: _lt('Pivot'), + icon: 'fa-table', + require_fields: true, + template: 'PivotView', - init: function(parent, dataset, view_id, options) { - this._super(parent, dataset, view_id, options); - this.model = new Model(dataset.model, {group_by_no_leaf: true}); + init: function() { + this._super.apply(this, arguments); + this._model = new Model(this.model, {group_by_no_leaf: true}); - this.fields = {}; this.measures = {}; this.groupable_fields = {}; this.ready = false; // will be ready after the first do_search this.data_loaded = $.Deferred(); - this.title = options.title; + this.title = this.options.title || this.fields_view.arch.attrs.string; this.main_row = { root: undefined, @@ -63,30 +62,72 @@ var PivotView = View.extend({ this.numbering = {}; this.widgets = []; + + this.enable_linking = !this.fields_view.arch.attrs.disable_linking; }, willStart: function () { var self = this; - return session.rpc('/web/pivot/check_xlwt').then(function(result) { + + var load_fields = this.ViewManager.get_fields(); + var load_xlwt = session.rpc('/web/pivot/check_xlwt').then(function(result) { self.xlwt_installed = result; }); + + this.fields_view.arch.children.forEach(function (field) { + var name = field.attrs.name; + if (field.attrs.interval) { + name += ':' + field.attrs.interval; + } + //noinspection FallThroughInSwitchStatementJS + switch (field.attrs.type) { + case 'measure': + self.widgets.push(field.attrs.widget || ""); + self.active_measures.push(name); + break; + case 'col': + self.initial_col_groupby.push(name); + break; + default: + if ('operator' in field.attrs) { + self.active_measures.push(name); + break; + } + case 'row': + self.initial_row_groupby.push(name); + } + }); + if ((!this.active_measures.length) || this.fields_view.arch.attrs.display_quantity) { + this.active_measures.push('__count__'); + } + return $.when(load_fields, load_xlwt, this._super()).then(function (fields) { + self.prepare_fields(fields); + // add active measures to the measure list. This is very rarely necessary, but it + // can be useful if one is working with a functional field non stored, but in a + // model with an overrided read_group method. In this case, the pivot view could + // work, and the measure should be allowed. However, be careful if you define a + // measure in your pivot view: non stored functional fields will probably not work + // (their aggregate will always be 0). + _.each(self.active_measures, function (m) { + if (!(m in self.measures)) { + self.measures[m] = self.fields[m]; + } + }); + }); }, start: function () { this.$table_container = this.$('.o-pivot-table'); - - var load_fields = this.model.call('fields_get', [], {context: this.dataset.get_context()}) - .then(this.prepare_fields.bind(this)); - - return $.when(this._super(), load_fields).then(this.render_field_selection.bind(this)); - }, - render_field_selection: function () { - var self = this; - - var context = {fields: _.chain(this.groupable_fields).pairs().sortBy(function(f){return f[1].string;}).value()}; + this.$el.toggleClass('oe_enable_linking', this.enable_linking); + var context = { + fields: _.chain(this.groupable_fields).pairs().sortBy(function(f) { + return f[1].string; + }).value(), + }; this.$field_selection = this.$('.o-field-selection'); this.$field_selection.html(QWeb.render('PivotView.FieldSelection', context)); - core.bus.on('click', self, function () { - self.$field_selection.find('ul').first().hide(); + core.bus.on('click', this, function () { + this.$field_selection.find('ul').first().hide(); }); + return this._super(); }, /** * Render the buttons according to the PivotView.buttons template and @@ -127,49 +168,6 @@ var PivotView = View.extend({ this.sidebar.appendTo($node); } }, - view_loading: function (fvg) { - var self = this; - this.title = this.title || fvg.arch.attrs.string; - this.enable_linking = !fvg.arch.attrs.disable_linking; - this.$el.toggleClass('oe-enable-linking', this.enable_linking); - fvg.arch.children.forEach(function (field) { - var name = field.attrs.name; - if (field.attrs.interval) { - name += ':' + field.attrs.interval; - } - //noinspection FallThroughInSwitchStatementJS - switch (field.attrs.type) { - case 'measure': - self.widgets.push(field.attrs.widget || ""); - self.active_measures.push(name); - break; - case 'col': - self.initial_col_groupby.push(name); - break; - default: - if ('operator' in field.attrs) { - self.active_measures.push(name); - break; - } - case 'row': - self.initial_row_groupby.push(name); - } - }); - if ((!self.active_measures.length) || fvg.arch.attrs.display_quantity){ - self.active_measures.push('__count__'); - } - // add active measures to the measure list. This is very rarely necessary, but it - // can be useful if one is working with a functional field non stored, but in a - // model with an overrided read_group method. In this case, the pivot view could - // work, and the measure should be allowed. However, be careful if you define a - // measure in your pivot view: non stored functional fields will probably not work - // (their aggregate will always be 0). - _.each(this.active_measures, function (m) { - if (!(m in self.measures)) { - self.measures[m] = self.fields[m]; - } - }); - }, prepare_fields: function (fields) { var self = this, groupable_types = ['many2one', 'char', 'boolean', @@ -277,15 +275,14 @@ var PivotView = View.extend({ col_domain = this.headers[col_id].domain, context = _.omit(_.clone(this.context), 'group_by'); - var views = [ - [this.options.action_views_ids.list || false, 'list'], - [this.options.action_views_ids.form || false, 'form'] - ]; + var views = _.filter(this.options.action.views, function (view) { + return view[1] === 'form' || view[1] === 'list'; + }); return this.do_action({ type: 'ir.actions.act_window', name: this.title, - res_model: this.model.name, + res_model: this.model, views: views, view_type : "list", view_mode : "list", @@ -345,7 +342,7 @@ var PivotView = View.extend({ groupbys.push([field].concat(other_groupbys.slice(0,i))); } return $.when.apply(null, groupbys.map(function (groupby) { - return self.model.query(fields) + return self._model.query(fields) .filter(header.domain.length ? header.domain : self.domain) .context(self.context) .lazy(false) @@ -398,7 +395,7 @@ var PivotView = View.extend({ } } return $.when.apply(null, groupbys.map(function (groupby) { - return self.model.query(fields) + return self._model.query(fields) .filter(self.domain) .context(self.context) .lazy(false) diff --git a/addons/web/static/src/js/views/search_menus.js b/addons/web/static/src/js/views/search_menus.js index 3bd7a3aaba5..c26cdc35ea8 100644 --- a/addons/web/static/src/js/views/search_menus.js +++ b/addons/web/static/src/js/views/search_menus.js @@ -26,18 +26,34 @@ return Widget.extend({ } }, }, - init: function (parent, query, target_model, action_id) { + init: function (parent, query, target_model, action_id, filters) { this._super.apply(this,arguments); this.searchview = parent; this.query = query; this.target_model = target_model; this.model = new Model('ir.filters'); - this.filters = {}; - this.$filters = {}; this.action_id = action_id; + if (filters) { + this.filters = {}; + _.each(filters, this.add_filter.bind(this)); + } + }, + willStart: function () { + var self = this; + var def; + if (!this.filters) { + def = this.model.call('get_filters', [this.target_model, this.action_id], { + context: this.searchview.dataset.context, + }).then(function (filters) { + self.filters = {}; + _.each(filters, self.add_filter.bind(self)); + }); + } + return $.when(def, this._super()); }, start: function () { var self = this; + this.$filters = {}; this.$save_search = this.$('.oe-save-search'); this.$save_name = this.$('.oe-save-name'); this.$inputs = this.$save_name.find('input'); @@ -55,12 +71,10 @@ return Widget.extend({ } }) .on('reset', this.proxy('clear_selection')); - return this.model.call('get_filters', [this.target_model, this.action_id], - {context: this.searchview.dataset.context}) - .done(this.proxy('prepare_dropdown_menu')); - }, - prepare_dropdown_menu: function (filters) { - filters.map(this.append_filter.bind(this)); + + _.each(this.filters, this.append_filter.bind(this)); + + return this._super(); }, toggle_save_menu: function (is_open) { this.$save_search @@ -115,7 +129,7 @@ return Widget.extend({ var filter = { name: filter_name, user_id: shared_filter ? false : session.uid, - model_id: this.searchview.dataset.model, + model_id: this.target_model, context: results.context, domain: results.domain, sort: JSON.stringify(this.searchview.dataset._sort), @@ -126,6 +140,7 @@ return Widget.extend({ filter.id = id; self.toggle_save_menu(false); self.$save_name.find('input').val('').prop('checked', false); + self.add_filter(filter); self.append_filter(filter); self.toggle_filter(filter, true); }); @@ -187,31 +202,39 @@ return Widget.extend({ clear_selection: function () { this.$('li.selected').removeClass('selected'); }, + /** + * Adds a filter description to the filters dict + * @param {Object} [filter] the filter description + */ + add_filter: function (filter) { + this.filters[this.key_for(filter)] = filter; + }, + /** + * Creates a $filter JQuery node, adds it to the $filters dict and appends it to the filter menu + * @param {Object} [filter] the filter description + */ append_filter: function (filter) { - var self = this, - key = this.key_for(filter), - $filter; + var self = this; + var key = this.key_for(filter); this.$divider.show(); - if (key in this.$filters) { - $filter = this.$filters[key]; - } else { - this.filters[key] = filter; - $filter = $('
  • ') - .insertBefore(this.$divider) + if (!(key in this.$filters)) { + var $filter = $('
  • ') + .addClass(filter.user_id ? 'oe_searchview_custom_private' + : 'oe_searchview_custom_public') .toggleClass('oe_searchview_custom_default', filter.is_default) - .append($('').text(filter.name)); - + .append($('').text(filter.name)) + .append($('', { + class: 'fa fa-trash-o remove-filter', + on: { + click: function (event) { + event.stopImmediatePropagation(); + self.remove_filter(filter, $filter, key); + }, + }, + })) + .insertBefore(this.$divider); this.$filters[key] = $filter; - this.$filters[key].addClass(filter.user_id ? 'oe_searchview_custom_private' - : 'oe_searchview_custom_public'); - $('') - .addClass('fa fa-trash-o remove-filter') - .click(function (event) { - event.stopImmediatePropagation(); - self.remove_filter(filter, $filter, key); - }) - .appendTo($filter); } this.$filters[key].unbind('click').click(function () { self.toggle_filter(filter); @@ -282,22 +305,12 @@ return Widget.extend({ } }, }, - init: function (parent, filters, fields_def) { + init: function (parent, filters) { this._super(parent); this.filters = filters || []; this.searchview = parent; this.propositions = []; - this.fields_def = fields_def.then(function (data) { - var fields = { - id: { string: 'ID', type: 'id', searchable: true } - }; - _.each(data, function(field_def, field_name) { - if (field_def.selectable !== false && field_name !== 'id') { - fields[field_name] = field_def; - } - }); - return fields; - }); + this.custom_filters_open = false; }, start: function () { var self = this; @@ -311,23 +324,41 @@ return Widget.extend({ $('
  • ').insertBefore(self.$add_filter); } }); - this.append_proposition().then(function (prop) { - prop.$el.hide(); - }); + }, + get_fields: function () { + if (!this._fields_def) { + this._fields_def = this.searchview.ViewManager.get_fields().then(function (data) { + var fields = { + id: { string: 'ID', type: 'id', searchable: true } + }; + _.each(data, function(field_def, field_name) { + if (field_def.selectable !== false && field_name !== 'id') { + fields[field_name] = field_def; + } + }); + return fields; + }); + } + return this._fields_def; }, toggle_custom_filter_menu: function (is_open) { - this.$add_filter - .toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined) - .toggleClass('o_open_menu', is_open); - this.$add_filter_menu.toggle(is_open); - if (this.$add_filter.hasClass('o_closed_menu') && (!this.propositions.length)) { - this.append_proposition(); + var self = this; + this.custom_filters_open = !_.isUndefined(is_open) ? is_open : !this.custom_filters_open; + var def; + if (this.custom_filters_open && !this.propositions.length) { + def = this.append_proposition(); } - this.$('.oe-filter-condition').toggle(is_open); + $.when(def).then(function () { + self.$add_filter + .toggleClass('o_closed_menu', !self.custom_filters_open) + .toggleClass('o_open_menu', self.custom_filters_open); + self.$add_filter_menu.toggle(self.custom_filters_open); + self.$('.oe-filter-condition').toggle(self.custom_filters_open); + }); }, append_proposition: function () { var self = this; - return this.fields_def.then(function (fields) { + return this.get_fields().then(function (fields) { var prop = new search_filters.ExtendedSearchProposition(self, fields); self.propositions.push(prop); prop.insertBefore(self.$add_filter_menu); @@ -387,15 +418,13 @@ return Widget.extend({ this.toggle_add_menu(); }, }, - init: function (parent, groups, fields_def) { + init: function (parent, groups) { this._super(parent); this.groups = groups || []; this.groupable_fields = []; this.searchview = parent; - this.fields_def = fields_def.then(this.proxy('get_groupable_fields')); }, start: function () { - var self = this; this.$menu = this.$('.group-by-menu'); var divider = this.$menu.find('.divider'); _.invoke(this.groups, 'insertBefore', divider); @@ -403,35 +432,43 @@ return Widget.extend({ divider.show(); } this.$add_group = this.$menu.find('.add-custom-group'); - this.fields_def.then(function () { - self.$menu.append(QWeb.render('GroupByMenuSelector', self)); - self.$add_group_menu = self.$('.oe-add-group'); - self.$group_selector = self.$('.oe-group-selector'); - self.$('.oe-select-group').click(function () { - self.toggle_add_menu(false); - var field = self.$group_selector.find(':selected').data('name'); - self.add_groupby_to_menu(field); - }); - }); }, - get_groupable_fields: function (fields) { - var groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime']; - var filter_group_field = _.filter(fields, function(field, name) { - if (field.store && _.contains(groupable_types, field.type)) { - field.name = name; - return field; - } - }); - this.groupable_fields = _.sortBy(filter_group_field, 'string'); + get_fields: function () { + var self = this; + if (!this._fields_def) { + this._fields_def = this.searchview.ViewManager.get_fields().then(function (fields) { + var groupable_types = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime']; + var filter_group_field = _.filter(fields, function(field, name) { + if (field.store && _.contains(groupable_types, field.type)) { + field.name = name; + return field; + } + }); + self.groupable_fields = _.sortBy(filter_group_field, 'string'); + + self.$menu.append(QWeb.render('GroupByMenuSelector', self)); + self.$add_group_menu = self.$('.oe-add-group'); + self.$group_selector = self.$('.oe-group-selector'); + self.$('.oe-select-group').click(function () { + self.toggle_add_menu(false); + var field = self.$group_selector.find(':selected').data('name'); + self.add_groupby_to_menu(field); + }); + }); + } + return this._fields_def; }, toggle_add_menu: function (is_open) { - this.$add_group - .toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined) - .toggleClass('o_open_menu', is_open); - this.$add_group_menu.toggle(is_open); - if (this.$add_group.hasClass('o_open_menu')) { - this.$group_selector.focus(); - } + var self = this; + this.get_fields().then(function () { + self.$add_group + .toggleClass('o_closed_menu', !(_.isUndefined(is_open)) ? !is_open : undefined) + .toggleClass('o_open_menu', is_open); + self.$add_group_menu.toggle(is_open); + if (self.$add_group.hasClass('o_open_menu')) { + self.$group_selector.focus(); + } + }); }, add_groupby_to_menu: function (field_name) { var filter = new search_inputs.Filter({attrs:{ diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js index caf6d5a9c74..4dc6b541a6a 100644 --- a/addons/web/static/src/js/views/search_view.js +++ b/addons/web/static/src/js/views/search_view.js @@ -6,10 +6,9 @@ var core = require('web.core'); var FavoriteMenu = require('web.FavoriteMenu'); var FilterMenu = require('web.FilterMenu'); var GroupByMenu = require('web.GroupByMenu'); -var Model = require('web.DataModel'); var pyeval = require('web.pyeval'); var search_inputs = require('web.search_inputs'); -var utils = require('web.utils'); +var View = require('web.View'); var Widget = require('web.Widget'); var Backbone = window.Backbone; @@ -317,8 +316,14 @@ var FacetValueView = Widget.extend({ } }); -var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{ - template: "SearchView", +var SearchView = View.extend({ + defaults: _.extend({}, View.prototype.defaults, { + hidden: false, + disable_filters: false, + disable_groupby: false, + disable_favorites: false, + disable_custom_filters: false, + }), events: { // focus last input if view itself is clicked 'click': function (e) { @@ -355,44 +360,36 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{ this.$el.autocomplete('widget').css('z-index', 9999); }, }, + template: "SearchView", + /** - * @constructs instance.web.SearchView - * @extends instance.web.Widget + * @constructs SearchView + * @extends View * * @param parent * @param dataset - * @param view_id - * @param defaults + * @param fields_view * @param {Object} [options] * @param {Boolean} [options.hidden=false] hide the search view * @param {Boolean} [options.disable_custom_filters=false] do not load custom filters from ir.filters */ - init: function(parent, dataset, view_id, defaults, options) { - this.options = _.defaults(options || {}, { - hidden: false, - disable_filters: false, - disable_groupby: false, - disable_favorites: false, - disable_custom_filters: false, - }); - this._super(parent); + init: function() { + this._super.apply(this, arguments); + this.title = this.options.action && this.options.action.name; + this.action_id = this.options.action && this.options.action.id; this.query = undefined; - this.dataset = dataset; - this.view_id = view_id; - this.title = options.action && options.action.name; this.search_fields = []; this.filters = []; this.groupbys = []; this.visible_filters = (localStorage.visible_search_menu === 'true'); this.input_subviews = []; // for user input in searchbar - this.defaults = defaults || {}; - this.headless = this.options.hidden && _.isEmpty(this.defaults); + this.search_defaults = this.options.search_defaults || {}; + this.headless = this.options.hidden && _.isEmpty(this.search_defaults); this.$buttons = this.options.$buttons; this.filter_menu = undefined; this.groupby_menu = undefined; this.favorite_menu = undefined; - this.action_id = this.options && this.options.action && this.options.action.id; }, start: function() { if (this.headless) { @@ -405,43 +402,29 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{ .on('add change reset remove', this.proxy('do_search')) .on('change', this.proxy('renderChangedFacets')) .on('add reset remove', this.proxy('renderFacets')); - var load_view = this.dataset._model.fields_view_get({ - view_id: this.view_id, - view_type: 'search', - context: this.dataset.get_context(), - }); this.$('.oe_searchview_unfold_drawer') .toggleClass('fa-caret-down', !this.visible_filters) .toggleClass('fa-caret-up', this.visible_filters); - return this.alive($.when(this._super(), this.alive(load_view).then(this.view_loaded.bind(this)))); - }, - get_title: function() { - return this.title; - }, - view_loaded: function (r) { var menu_defs = []; - this.fields_view_get = r; - this.view_id = this.view_id || r.view_id; this.prepare_search_inputs(); if (this.$buttons) { - var fields_def = new Model(this.dataset.model).call('fields_get', { - context: this.dataset.get_context() - }); - if (!this.options.disable_filters) { - this.filter_menu = new FilterMenu(this, this.filters, fields_def); + this.filter_menu = new FilterMenu(this, this.filters); menu_defs.push(this.filter_menu.appendTo(this.$buttons)); } if (!this.options.disable_groupby) { - this.groupby_menu = new GroupByMenu(this, this.groupbys, fields_def); + this.groupby_menu = new GroupByMenu(this, this.groupbys); menu_defs.push(this.groupby_menu.appendTo(this.$buttons)); } if (!this.options.disable_favorites) { - this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id); + this.favorite_menu = new FavoriteMenu(this, this.query, this.dataset.model, this.action_id, this.options.filters); menu_defs.push(this.favorite_menu.appendTo(this.$buttons)); } } - return $.when.apply($, menu_defs).then(this.proxy('set_default_filters')); + return $.when.apply($, menu_defs).then(this.set_default_filters.bind(this)); + }, + get_title: function() { + return this.title; }, set_default_filters: function () { var self = this, @@ -449,10 +432,10 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{ if (!self.options.disable_custom_filters && default_custom_filter) { return this.favorite_menu.toggle_filter(default_custom_filter, true); } - if (!_.isEmpty(this.defaults)) { + if (!_.isEmpty(this.search_defaults)) { var inputs = this.search_fields.concat(this.filters, this.groupbys), - defaults = _.invoke(inputs, 'facet_for_defaults', this.defaults); - return $.when.apply(null, defaults).then(function () { + search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults); + return $.when.apply(null, search_defaults).then(function () { self.query.reset(_(arguments).compact(), {preventSearch: true}); }); } @@ -667,7 +650,7 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{ // * this.group_by: group_bys prepare_search_inputs: function () { var self = this, - arch = this.fields_view_get.arch; + arch = this.fields_view.arch; var filters = [].concat.apply([], _.map(arch.children, function (item) { return item.tag !== 'group' ? eval_item(item) : item.children.map(eval_item); @@ -702,7 +685,7 @@ var SearchView = Widget.extend(/** @lends instance.web.SearchView# */{ } if (filter.item.tag === 'field') { var attrs = filter.item.attrs; - var field = self.fields_view_get.fields[attrs.name]; + var field = self.fields_view.fields[attrs.name]; // M2O combined with selection widget is pointless and broken in search views, // but has been used in the past for unsupported hacks -> ignore it diff --git a/addons/web/static/src/js/views/tree_view.js b/addons/web/static/src/js/views/tree_view.js index ab9193095ea..6246a1494f5 100644 --- a/addons/web/static/src/js/views/tree_view.js +++ b/addons/web/static/src/js/views/tree_view.js @@ -14,41 +14,21 @@ var View = require('web.View'); var _lt = core._lt; var QWeb = core.qweb; -var TreeView = View.extend(/** @lends instance.web.TreeView# */{ +var TreeView = View.extend({ display_name: _lt('Tree'), icon: 'fa-align-left', - view_type: 'tree', - /** - * Indicates that this view is not searchable, and thus that no search - * view should be displayed (if there is one active). - */ - searchable : false, + // Indicates that this view is not searchable, and thus that no search view should be displayed. + searchable: false, + /** * Genuine tree view (the one displayed as a tree, not the list) - * - * @constructs instance.web.TreeView - * @extends instance.web.View - * - * @param parent - * @param dataset - * @param view_id - * @param options */ - init: function(parent, dataset, view_id, options) { - this._super(parent); - this.dataset = dataset; - this.model = dataset.model; - this.view_id = view_id; - + init: function() { + this._super.apply(this, arguments); this.records = {}; - - this.options = _.extend({}, this.defaults, options || {}); - _.bindAll(this, 'color_for'); - }, - - view_loading: function(r) { - return this.load_tree(r); + this.fields = this.fields_view.fields; + this.children_field = this.fields_view.field_parent; }, /** @@ -67,20 +47,29 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{ } return fields; }, - load_tree: function (fields_view) { - var self = this; - var has_toolbar = !!fields_view.arch.attrs.toolbar; - // field name in OpenERP is kinda stupid: this is the name of the field - // holding the ids to the children of the current node, why call it - // field_parent? - this.children_field = fields_view.field_parent; - this.fields_view = fields_view; + willStart: function () { _(this.fields_view.arch.children).each(function (field) { if (field.attrs.modifiers) { field.attrs.modifiers = JSON.parse(field.attrs.modifiers); } }); - this.fields = fields_view.fields; + + if (this.fields_view.arch.attrs.colors) { + this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain() + .compact() + .map(function(color_pair) { + var pair = color_pair.split(':'), + color = pair[0], + expr = pair[1]; + return [color, py.parse(py.tokenize(expr)), expr]; + }).value(); + } + + return this._super(); + }, + start: function () { + var self = this; + var has_toolbar = !!this.fields_view.arch.attrs.toolbar; this.hook_row_click(); this.$el.html(QWeb.render('TreeView', { 'title': this.fields_view.arch.attrs.string, @@ -120,17 +109,7 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{ // TODO store open nodes in url ?... this.do_push_state({}); - if (!this.fields_view.arch.attrs.colors) { - return; - } - this.colors = _(this.fields_view.arch.attrs.colors.split(';')).chain() - .compact() - .map(function(color_pair) { - var pair = color_pair.split(':'), - color = pair[0], - expr = pair[1]; - return [color, py.parse(py.tokenize(expr)), expr]; - }).value(); + return this._super(); }, /** * Returns the color for the provided record in the current view (from the @@ -225,7 +204,7 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{ activate: function(id) { var self = this; var local_context = { - active_model: self.dataset.model, + active_model: self.model, active_id: id, active_ids: [id] }; @@ -234,7 +213,7 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{ this.dataset.get_context(), local_context)); return this.rpc('/web/treeview/action', { id: id, - model: this.dataset.model, + model: this.model, context: ctx }).then(function (actions) { if (!actions.length) { return; } @@ -260,11 +239,6 @@ var TreeView = View.extend(/** @lends instance.web.TreeView# */{ $child_row.toggle(show); }, this); }, - - do_hide: function () { - this.hidden = true; - this._super(); - } }); core.view_registry.add('tree', TreeView); diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js index d3d45c347a1..1606dabaf85 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -193,17 +193,6 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test } return []; }); - mock.add('demo:fields_view_get', function () { - return { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: '', - }; - }); mock.add('demo:onchange', function () { return {}; }); @@ -224,7 +213,16 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test }); var ds = new data.DataSetStatic(null, 'demo', null, [1]); - var l = new ListView({}, ds, false, {editable: 'top'}); + var fields_view = ds._model.postprocess_fvg({ + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }, + arch: '', + }); + var l = new ListView({}, ds, fields_view, {editable: 'top'}); var $fix = $( "#qunit-fixture"); return l.appendTo($fix) @@ -250,22 +248,21 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView'], function (test }); odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], function (test, mock) { - + function fields_view_get () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }, + arch: '', + }; + } function setup () { mock.add('demo:read', function () { return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }]; }); - mock.add('demo:fields_view_get', function () { - return { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: '', - }; - }); } test('edition events',function (assert, data, ListView) { @@ -276,7 +273,8 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], functio counter: 0, onEvent: function (e) { this.counter++; } }; - var l = new ListView({}, ds, false, {editable: 'top'}); + var fields_view = ds._model.postprocess_fvg(fields_view_get()); + var l = new ListView({}, ds, fields_view, {editable: 'top'}); l.on('edit:before edit:after', o, o.onEvent); var $fix = $( "#qunit-fixture"); @@ -297,7 +295,8 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView'], functio setup(); var edit_after = false; var ds = new data.DataSetStatic(null, 'demo', null, [1]); - var l = new ListView({}, ds, false, {editable: 'top'}); + var fields_view = ds._model.postprocess_fvg(fields_view_get()); + var l = new ListView({}, ds, fields_view, {editable: 'top'}); l.on('edit:before', {}, function (e) { e.cancel = true; }); @@ -329,16 +328,6 @@ odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView'], functi mock.add('demo:onchange', function () { return {}; }); - - mock.add('demo:fields_view_get', function () { - return { - type: 'tree', - fields: { - a: {type: 'char', string: "A"} - }, - arch: '', - }; - }); mock.add('demo:read', function (args, kwargs) { if (_.isEmpty(args[0])) { return []; @@ -358,7 +347,14 @@ odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView'], functi mock.add('demo:on_write', function () { return [42]; }); var ds = new data.DataSetStatic(null, 'demo', null, []); - var l = new ListView({}, ds, false, {editable: 'top'}); + var fields_view = ds._model.postprocess_fvg({ + type: 'tree', + fields: { + a: {type: 'char', string: "A"} + }, + arch: '', + }); + var l = new ListView({}, ds, fields_view, {editable: 'top'}); var $fix = $( "#qunit-fixture"); return l.appendTo($fix) diff --git a/addons/web/static/test/list.js b/addons/web/static/test/list.js index 26e17aff77d..1a19e567364 100644 --- a/addons/web/static/test/list.js +++ b/addons/web/static/test/list.js @@ -3,15 +3,6 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data'], function (test test('record-deletion', function (assert, ListView, data) { assert.expect(2); - mock.add('demo:fields_view_get', function () { - return { - type: 'tree', - fields: { - a: {type: 'char', string: "A"} - }, - arch: '