diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index bdedb85e63d..8faca685300 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -409,6 +409,11 @@ class DataSet(openerpweb.Controller): r = getattr(m, method)(ids, *args) return {'result': r} + @openerpweb.jsonrequest + def exec_workflow(self, req, model, id, signal): + r = req.session.exec_workflow(model, id, signal) + return {'result': r} + @openerpweb.jsonrequest def default_get(self, req, model, fields, context={}): m = req.session.model(model) @@ -573,19 +578,22 @@ class ListView(View): :param int limit: search limit, for pagination :returns: hell if I have any idea yet """ - view = self.fields_view_get(request, model, id) + view = self.fields_view_get(request, model, id, toolbar=True) rows = DataSet().do_search_read(request, model, offset=offset, limit=limit, domain=domain) eval_context = request.session.evaluation_context( request.context) - return [ - {'data': dict((key, {'value': value}) - for key, value in row.iteritems()), - 'color': self.process_colors(view, row, eval_context)} - for row in rows - ] + return { + 'view': view, + 'records': [ + {'data': dict((key, {'value': value}) + for key, value in row.iteritems()), + 'color': self.process_colors(view, row, eval_context)} + for row in rows + ] + } def process_colors(self, view, row, context): colors = view['arch']['attrs'].get('colors') @@ -618,5 +626,12 @@ class Action(openerpweb.Controller): _cp_path = "/base/action" @openerpweb.jsonrequest - def load(self, req, action_id): - return {} + def load(self, req, action_id, context={}): + Actions = req.session.model('ir.actions.actions') + value = False + action_type = Actions.read([action_id], ['type'], context) + if action_type: + action = req.session.model(action_type[0]['type']).read([action_id], False, context) + if action: + value = action[0] + return {'result': value} diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index cf6d0beabd4..1bd004b53f6 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -336,8 +336,9 @@ body.openerp { } /* Main Application */ -.openerp .application { +.openerp .oe-application { padding: 0px; + height: 100%; } .openerp h2.oe_view_title { @@ -486,6 +487,9 @@ body.openerp { vertical-align: middle; } +.openerp .oe-listview .oe-field-cell { + cursor: pointer; +} .openerp .oe-listview .oe-field-cell button { padding: 0; border: none; @@ -636,7 +640,7 @@ body.openerp { .openerp td.required input, .openerp td.required select { background-color: #D2D2FF; } -.openerp td.invalid input, .openerp td.invalid select { +.openerp td.invalid input, .openerp td.invalid select, .openerp td.invalid textarea { background-color: #F66; border: 1px solid #D00; } @@ -673,13 +677,14 @@ body.openerp { } .openerp .view-manager-main-content { + width: 100%; padding: 6px; } .openerp .view-manager-main-sidebar { - width:0; - padding:0; - margin:0; + width: 180px; + padding: 0; + margin: 0; } .openerp .sidebar-main-div { @@ -746,6 +751,7 @@ body.openerp { } .openerp .toggle-sidebar { + cursor: pointer; border-color: #D2CFCF; border-style: solid; border-width: 1px; diff --git a/addons/base/static/src/js/data.js b/addons/base/static/src/js/data.js index 5ed21be533c..a29f320b79c 100644 --- a/addons/base/static/src/js/data.js +++ b/addons/base/static/src/js/data.js @@ -118,6 +118,13 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base. ids: ids, args: args }, callback); + }, + exec_workflow: function (id, signal, callback) { + return this.rpc('/base/dataset/exec_workflow', { + model: this.model, + id: id, + signal: signal + }, callback); } }); diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js index 9efa3cd01b6..84d7fe0babd 100644 --- a/addons/base/static/src/js/form.js +++ b/addons/base/static/src/js/form.js @@ -457,16 +457,14 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({ } this.add_widget(widget); }, - add_widget: function(w) { - if (!w.invisible) { - var current_row = this.table[this.table.length - 1]; - if (current_row.length && (this.x + w.colspan) > this.columns) { - current_row = this.add_row(); - } - current_row.push(w); - this.x += w.colspan; + add_widget: function(widget) { + var current_row = this.table[this.table.length - 1]; + if (current_row.length && (this.x + widget.colspan) > this.columns) { + current_row = this.add_row(); } - return w; + current_row.push(widget); + this.x += widget.colspan; + return widget; } }); @@ -534,7 +532,7 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ on_confirmed: function() { var attrs = this.node.attrs; if (attrs.special) { - this.on_button_object({ + this.on_button_action({ result : { type: 'ir.actions.act_window_close' } }); } else { @@ -542,19 +540,20 @@ openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({ var context = _.extend({}, this.view.dataset.context, attrs.context || {}); switch(type) { case 'object': - return this.view.dataset.call(attrs.name, [this.view.datarecord.id], [context], this.on_button_object); - break; + return this.view.dataset.call(attrs.name, [this.view.datarecord.id], [context], this.on_button_action); + case 'action': + return this.rpc('/base/action/load', { action_id: parseInt(attrs.name) }, this.on_button_action); default: - this.log(_.sprintf("Unsupported button type : %s", type)); + return this.view.dataset.exec_workflow(this.view.datarecord.id, attrs.name, this.on_button_action); } } }, - on_button_object: function(r) { - if (r.result === false) { - this.log("Button object returns false"); - } else if (r.result.constructor == Object) { + on_button_action: function(r) { + console.log("Got reesonse button", r) + if (r.result && r.result.constructor == Object) { this.session.action_manager.do_action(r.result); } else { + this.log("Button returned", r.result); this.view.reload(); } } @@ -911,7 +910,6 @@ openerp.base.form.FieldReference = openerp.base.form.Field.extend({ * Registry of form widgets, called by :js:`openerp.base.FormView` */ openerp.base.form.widgets = new openerp.base.Registry({ - 'hpaned': 'openerp.base.form.Hpaned', 'group' : 'openerp.base.form.WidgetFrame', 'notebook' : 'openerp.base.form.WidgetNotebook', 'separator' : 'openerp.base.form.WidgetSeparator', diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js index 5a08b96ac14..8035616e59a 100644 --- a/addons/base/static/src/js/list.js +++ b/addons/base/static/src/js/list.js @@ -10,9 +10,23 @@ openerp.base.ListView = openerp.base.Controller.extend( // whether the column headers should be displayed 'header': true, // display addition button, with that label - 'addable': "New" + 'addable': "New", + // whether the list view can be sorted, note that once a view has been + // sorted it can not be reordered anymore + 'sortable': true, + // whether the view rows can be reordered (via vertical drag & drop) + 'reorderable': true }, /** + * Core class for list-type displays. + * + * As a view, needs a number of view-related parameters to be correctly + * instantiated, provides options and overridable methods for behavioral + * customization. + * + * See constructor parameters and method documentations for information on + * the default behaviors and possible options for the list view. + * * @constructs * @param view_manager * @param session An OpenERP session object @@ -24,6 +38,8 @@ openerp.base.ListView = openerp.base.Controller.extend( * @param {Boolean} [options.header=true] should the list's header be displayed * @param {Boolean} [options.deletable=true] are the list rows deletable * @param {null|String} [options.addable="New"] should the new-record button be displayed, and what should its label be. Use ``null`` to hide the button. + * @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(view_manager, session, element_id, dataset, view_id, options) { this._super(session, element_id); @@ -37,11 +53,42 @@ openerp.base.ListView = openerp.base.Controller.extend( this.options = _.extend({}, this.defaults, options || {}); }, + /** + * View startup method, the default behavior is to set the ``oe-listview`` + * class on its root element and to perform an RPC load call. + * + * @returns {$.Deferred} loading promise + */ start: function() { this.$element.addClass('oe-listview'); - return this.rpc("/base/listview/load", {"model": this.model, "view_id":this.view_id, - toolbar:!!this.view_manager.sidebar}, this.on_loaded); + return this.rpc("/base/listview/load", { + model: this.model, + view_id: this.view_id, + toolbar: !!this.view_manager.sidebar + }, this.on_loaded); }, + /** + * 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 + * * Sets up its sidebar, if any + * + * @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 + */ on_loaded: function(data) { var self = this; this.fields_view = data.fields_view; @@ -108,8 +155,9 @@ openerp.base.ListView = openerp.base.Controller.extend( var context = _.extend( {}, self.dataset.context, field.context || {}); - self.dataset.call(action, [row.data.id.value], [context], - self.do_reload); + self.notification.notify( + '[Dummy] Execute action', self.dataset.model + '#' + action + "([" + row.data.id.value + '])'); + // TODO: implement actions correctly once the corresponding manager has been extracted (and filled) e.stopImmediatePropagation(); }); $table.delegate( @@ -127,10 +175,18 @@ openerp.base.ListView = openerp.base.Controller.extend( /** * Fills the table with the provided records after emptying it * - * @param {Array} records the records to fill the list view with + * TODO: should also re-load the table itself, as e.g. columns may have changed + * + * @param {Object} result filling result + * @param {Array} [result.view] the new view (wrapped fields_view_get result) + * @param {Array} result.records records the records to fill the list view with * @returns {Promise} promise to the end of view rendering (list views are asynchronously filled for improved responsiveness) */ - do_fill_table: function(records) { + do_fill_table: function(result) { + if (result.view) { + this.on_loaded({fields_view: result.view}); + } + var records = result.records; var $table = this.$element.find('table'); this.rows = records; @@ -188,14 +244,19 @@ openerp.base.ListView = openerp.base.Controller.extend( }); }, /** - * Asks the view manager to switch to a different view, using the provided - * record index (within the current dataset). + * Used to handle a click on a table row, if no other handler caught the + * event. + * + * The default implementation asks the list view's view manager to switch + * to a different view (by calling + * :js:func:`~openerp.base.ViewManager.on_mode_switch`), using the + * provided record index (within the current list view's dataset). * * If the index is null, ``switch_to_record`` asks for the creation of a * new record. * * @param {Number|null} index the record index (in the current dataset) to switch to - * @param {String} [view="form"] the view to switch to + * @param {String} [view="form"] the view type to switch to */ switch_to_record:function (index, view) { view = view || 'form'; @@ -204,6 +265,16 @@ openerp.base.ListView = openerp.base.Controller.extend( this.view_manager.on_mode_switch(view); }, this)); }, + /** + * Base handler for clicking on a row, discovers the index of the record + * corresponding to the clicked row in the list view's dataset. + * + * Should not be overridden, use + * :js:func:`~openerp.base.ListView.switch_to_record` to customize the + * behavior of the list view when clicking on a row instead. + * + * @param {Object} event jQuery DOM event object + */ on_select_row: function (event) { var $target = $(event.currentTarget); if (!$target.parent().is('tbody')) { @@ -216,8 +287,8 @@ openerp.base.ListView = openerp.base.Controller.extend( if (index == undefined || index === -1) { return; } - this.switch_to_record(index); - }, + this.switch_to_record(index); + }, do_show: function () { this.$element.show(); if (this.hidden) { @@ -246,6 +317,15 @@ openerp.base.ListView = openerp.base.Controller.extend( 'domain': this.dataset.domain }, this.do_fill_table); }, + /** + * Event handler for a search, asks for the computation/folding of domains + * and contexts (and group-by), then reloads the view's content. + * + * @param {Array} domains a sequence of literal and non-literal domains + * @param {Array} contexts a sequence of literal and non-literal contexts + * @param {Array} groupbys a sequence of literal and non-literal group-by contexts + * @returns {$.Deferred} fold request evaluation promise + */ do_search: function (domains, contexts, groupbys) { var self = this; return this.rpc('/base/session/eval_domain_and_context', { @@ -266,7 +346,7 @@ openerp.base.ListView = openerp.base.Controller.extend( /** * Handles the signal to delete a line from the DOM * - * @param e + * @param e jQuery event object */ do_delete: function (e) { // don't link to forms @@ -295,7 +375,7 @@ openerp.base.ListView = openerp.base.Controller.extend( }, /** * Gets the ids of all currently selected records, if any - * @returns a list of ids, empty if no record is selected (or the list view is not selectable + * @returns {Array} empty if no record is selected (or the list view is not selectable) */ get_selection: function () { if (!this.options.selectable) { @@ -307,6 +387,8 @@ openerp.base.ListView = openerp.base.Controller.extend( return rows[$(this).prevAll().length].data.id.value; }).get(); } + // TODO: implement sort (click on column headers), if sorted, the list can not be reordered anymore + // TODO: implement reorder (drag and drop rows) }); openerp.base.TreeView = openerp.base.Controller.extend({ diff --git a/addons/base/static/src/js/views.js b/addons/base/static/src/js/views.js index f79d0e611eb..8ef14a3edbf 100644 --- a/addons/base/static/src/js/views.js +++ b/addons/base/static/src/js/views.js @@ -19,7 +19,6 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ * Supported actions: act_window */ do_action: function(action) { - this.log(action); var self = this; // instantiate the right controllers by understanding the action switch (action.type) { @@ -36,14 +35,14 @@ openerp.base.ActionManager = openerp.base.Controller.extend({ // When dialog is closed with ESC key or close manually, branch to act_window_close logic self.do_action({ type: 'ir.actions.act_window_close' }); }); - var viewmanager = new openerp.base.ViewManagerAction(this.session ,element_id, action, false); + var viewmanager = new openerp.base.ViewManagerAction(this.session, element_id, action, false); viewmanager.start(); this.dialog_stack.push(viewmanager); - } else if (action.target == "current") { + } else { if (this.viewmanager) { this.viewmanager.stop(); } - this.viewmanager = new openerp.base.ViewManagerAction(this.session,this.element_id, action, true); + this.viewmanager = new openerp.base.ViewManagerAction(this.session, this.element_id, action, true); this.viewmanager.start(); } break; @@ -211,14 +210,17 @@ openerp.base.ViewManagerAction = openerp.base.ViewManager.extend({ // init search view var searchview_id = this.action.search_view_id && this.action.search_view_id[0]; - var searchview_loaded = this.setup_search_view( - searchview_id || false, search_defaults); + if (searchview_id) { + var searchview_loaded = this.setup_search_view( + searchview_id, search_defaults); - // schedule auto_search - if (searchview_loaded != null && this.action['auto_search']) { - $.when(searchview_loaded, inital_view_loaded) - .then(this.searchview.do_search); + // schedule auto_search + if (this.action['auto_search']) { + $.when(searchview_loaded, inital_view_loaded) + .then(this.searchview.do_search); + } } + }, stop: function() { // should be replaced by automatic destruction implemented in BaseWidget diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml index cbb90ca9e2e..b49ef2c7d6a 100644 --- a/addons/base/static/src/xml/base.xml +++ b/addons/base/static/src/xml/base.xml @@ -18,9 +18,6 @@
-|
-
-
-
-
+
|
|||
|
-
-
-
-
- |
- - | -
+
+
+
+
+ |
+ + | +