+
+
+
diff --git a/addons/base/static/lib/qweb/qweb-test.js.html b/addons/base/static/lib/qweb/qweb-test.js.html
index b39527f0c13..fda837497c4 100644
--- a/addons/base/static/lib/qweb/qweb-test.js.html
+++ b/addons/base/static/lib/qweb/qweb-test.js.html
@@ -5,6 +5,7 @@
+
diff --git a/addons/base/static/lib/qweb/qweb2.js b/addons/base/static/lib/qweb/qweb2.js
index edf8f8b2b21..36d0dce09c4 100644
--- a/addons/base/static/lib/qweb/qweb2.js
+++ b/addons/base/static/lib/qweb/qweb2.js
@@ -77,6 +77,41 @@ var QWeb2 = {
}
return -1;
},
+ xml_node_to_string : function(node, childs_only) {
+ if (childs_only) {
+ var childs = node.childNodes, r = [];
+ for (var i = 0, ilen = childs.length; i < ilen; i++) {
+ r.push(this.xml_node_to_string(childs[i]));
+ }
+ return r.join('');
+ } else {
+ if (node.xml !== undefined) {
+ return node.xml;
+ } else {
+ return (new XMLSerializer()).serializeToString(node);
+ }
+ }
+ },
+ xpath_selectNodes : function(node, expr) {
+ if (node.selectNodes !== undefined) {
+ return node.selectNodes(expr);
+ } else {
+ var xpath = new XPathEvaluator();
+ try {
+ var result = xpath.evaluate(expr, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
+ } catch(error) {
+ this.exception("Error with XPath expression '" + expr + "' : " + error);
+ }
+ var r = [];
+ if (result != null) {
+ var element;
+ while (element = result.iterateNext()) {
+ r.push(element);
+ }
+ }
+ return r
+ }
+ },
call: function(context, template, old_dict, _import, callback) {
var new_dict = this.extend({}, old_dict);
new_dict['__caller__'] = old_dict['__template__'];
@@ -157,8 +192,10 @@ QWeb2.Engine = (function() {
this.templates_resources = []; // TODO: implement this.reload()
this.templates = {};
this.compiled_templates = {};
+ this.extend_templates = {};
this.dict = {};
this.tools = QWeb2.tools;
+ this.jQuery = window.jQuery;
for (var i = 0; i < arguments.length; i++) {
this.add_template(arguments[i]);
}
@@ -178,8 +215,25 @@ QWeb2.Engine = (function() {
return this.tools.exception(node.innerText);
}
var name = node.getAttribute(this.prefix + '-name');
+ var extend = node.getAttribute(this.prefix + '-extend');
+ if (name && extend) {
+ // Clone template and extend it
+ if (!this.templates[extend]) {
+ return this.tools.exception("Can't clone undefined template " + extend);
+ }
+ this.templates[name] = this.templates[extend].cloneNode(true);
+ extend = name;
+ name = undefined;
+ }
if (name) {
this.templates[name] = node;
+ } else if (extend) {
+ delete(this.compiled_templates[extend]);
+ if (this.extend_templates[extend]) {
+ this.extend_templates[extend].push(node);
+ } else {
+ this.extend_templates[extend] = [node];
+ }
}
}
}
@@ -256,15 +310,26 @@ QWeb2.Engine = (function() {
"});";
},
render : function(template, dict) {
- //console.time("QWeb render template " + template);
+ if (this.debug && window['console'] !== undefined) {
+ console.time("QWeb render template " + template);
+ }
var r = this._render(template, dict);
- //console.timeEnd("QWeb render template " + template);
+ if (this.debug && window['console'] !== undefined) {
+ console.timeEnd("QWeb render template " + template);
+ }
return r;
},
_render : function(template, dict) {
if (this.compiled_templates[template]) {
return this.compiled_templates[template].apply(this, [dict || {}]);
} else if (this.templates[template]) {
+ var ext;
+ if (ext = this.extend_templates[template]) {
+ var extend_node;
+ while (extend_node = ext.shift()) {
+ this.extend(template, extend_node);
+ }
+ }
var code = this.compile(this.templates[template]), tcompiled;
try {
// tcompiled = new Function(code); TODO: Check if could use this in order to avoid eval
@@ -278,6 +343,53 @@ QWeb2.Engine = (function() {
} else {
return this.tools.exception("Template '" + template + "' not found");
}
+ },
+ extend : function(template, extend_node) {
+ if (!this.jQuery) {
+ return this.tools.exception("Can't extend template " + template + " without jQuery");
+ }
+ for (var i = 0, ilen = extend_node.childNodes.length; i < ilen; i++) {
+ var child = extend_node.childNodes[i];
+ if (child.nodeType === 1) {
+ var xpath = child.getAttribute(this.prefix + '-xpath'),
+ jquery = child.getAttribute(this.prefix + '-jquery'),
+ operation = child.getAttribute(this.prefix + '-operation'),
+ target,
+ error_msg = "Error while extending template '" + template;
+ if (jquery) {
+ target = this.jQuery(jquery, this.templates[template]);
+ } else if (xpath) {
+ // NOTE: due to the XPath implementation, extending a template will only work once
+ // when using XPath because XPathResult won't match objects with other constructor than 'Element'
+ // As jQuery will create HTMLElements, xpath will ignore them then causing problems when a
+ // template has already been extended. XPath selectors will probably be removed in QWeb.
+ target = this.jQuery(this.tools.xpath_selectNodes(this.templates[template], xpath));
+ } else {
+ this.tools.exception(error_msg + "No expression given");
+ }
+ error_msg += "' (expression='" + (jquery || xpath) + "') : ";
+ var inner = this.tools.xml_node_to_string(child, true);
+ if (operation) {
+ var allowed_operations = "append,prepend,before,after,replace,inner".split(',');
+ if (this.tools.arrayIndexOf(allowed_operations, operation) == -1) {
+ this.tools.exception(error_msg + "Invalid operation : '" + operation + "'");
+ }
+ operation = {'replace' : 'replaceWith', 'inner' : 'html'}[operation] || operation;
+ target[operation]($(inner));
+ } else {
+ try {
+ var f = new Function(['$'], inner);
+ } catch(error) {
+ return this.tools.exception("Parse " + error_msg + error);
+ }
+ try {
+ f.apply(target, [this.jQuery]);
+ } catch(error) {
+ return this.tools.exception("Runtime " + error_msg + error);
+ }
+ }
+ }
+ }
}
});
return Engine;
diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css
index 0ca1c995b33..6656dd42849 100644
--- a/addons/base/static/src/css/base.css
+++ b/addons/base/static/src/css/base.css
@@ -568,7 +568,6 @@ body.openerp {
/* Form */
.openerp table.oe_frame td {
color: #4c4c4c;
- font-weight: bold;
}
.openerp .required.error {
border: 1px solid #900;
@@ -611,6 +610,9 @@ body.openerp {
min-width: 90px;
color: #1f1f1f;
}
+.openerp textarea {
+ resize:vertical;
+}
.openerp input[type="text"], .openerp input[type="password"], .openerp select, .openerp .button {
height: 22px;
}
@@ -646,6 +648,12 @@ body.openerp {
background-color: #F66;
border: 1px solid #D00;
}
+.openerp div.oe-progressbar span {
+ position: absolute;
+ margin-left: 10px;
+ margin-top: 5px;
+ font-weight: bold;
+}
/* jQuery UI override */
.openerp .ui-widget {
diff --git a/addons/base/static/src/js/form.js b/addons/base/static/src/js/form.js
index 3a6ef174c8d..27beadbdecf 100644
--- a/addons/base/static/src/js/form.js
+++ b/addons/base/static/src/js/form.js
@@ -773,6 +773,14 @@ openerp.base.form.FieldProgressBar = openerp.base.form.Field.extend({
value: this.value,
disabled: this.readonly
});
+ },
+ set_value: function(value) {
+ this._super.apply(this, arguments);
+ var show_value = Number(value);
+ if (show_value === NaN) {
+ show_value = 0;
+ }
+ this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
}
});
diff --git a/addons/base/static/src/js/list.js b/addons/base/static/src/js/list.js
index e9061fa7121..62d29da0cba 100644
--- a/addons/base/static/src/js/list.js
+++ b/addons/base/static/src/js/list.js
@@ -44,6 +44,7 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @borrows openerp.base.ActionExecutor#execute_action as #execute_action
*/
init: function(view_manager, session, element_id, dataset, view_id, options) {
+ var self = this;
this._super(session, element_id);
this.view_manager = view_manager;
this.dataset = dataset;
@@ -54,6 +55,35 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.rows = [];
this.options = _.extend({}, this.defaults, options || {});
+
+ this.list = new openerp.base.ListView.List({
+ options: this.options,
+ columns: this.columns,
+ rows: this.rows
+ });
+ $(this.list).bind({
+ 'selected': function (e, selection) {
+ self.$element.find('#oe-list-delete')
+ .toggle(!!selection.length);
+ },
+ 'deleted': function (e, ids) {
+ self.do_delete(ids);
+ },
+ 'action': function (e, action_name, id) {
+ var action = _.detect(self.columns, function (field) {
+ return field.name === action_name;
+ });
+ if (!action) { return; }
+ // TODO: not supposed to reload everything, I think
+ self.execute_action(
+ action, self.dataset, self.session.action_manager,
+ id, self.do_reload);
+ },
+ 'row_link': function (e, index) {
+ self.switch_to_record(index);
+ }
+ });
+
},
/**
* View startup method, the default behavior is to set the ``oe-listview``
@@ -99,7 +129,9 @@ openerp.base.ListView = openerp.base.Controller.extend(
var fields = this.fields_view.fields;
var domain_computer = openerp.base.form.compute_domain;
- this.columns = _(this.fields_view.arch.children).chain()
+
+ this.columns.splice(0, this.columns.length);
+ this.columns.push.apply(this.columns, _(this.fields_view.arch.children).chain()
.map(function (field) {
var name = field.attrs.name;
var column = _.extend({id: name, tag: field.tag},
@@ -118,13 +150,14 @@ openerp.base.ListView = openerp.base.Controller.extend(
column.attrs_for = function () { return {}; };
}
return column;
- }).value();
+ }).value());
this.visible_columns = _.filter(this.columns, function (column) {
return column.invisible !== '1';
});
if (!this.fields_view.sorted) { this.fields_view.sorted = {}; }
+
this.$element.html(QWeb.render("ListView", this));
// Head hook
@@ -142,42 +175,7 @@ openerp.base.ListView = openerp.base.Controller.extend(
});
var $table = this.$element.find('table');
- // Cell events
- $table.delegate(
- 'th.oe-record-selector', 'click', function (e) {
- // TODO: ~linear performances, would a simple counter work?
- if ($table.find('th.oe-record-selector input:checked').length) {
- $table.find('#oe-list-delete').show();
- } else {
- $table.find('#oe-list-delete').hide();
- }
- // A click in the selection cell should not activate the
- // linking feature
- e.stopImmediatePropagation();
- });
- $table.delegate(
- 'td.oe-field-cell button', 'click', function (e) {
- e.stopImmediatePropagation();
-
- var $cell = $(e.currentTarget).closest('td');
-
- var col_index = $cell.prevAll('td').length;
- var field = self.visible_columns[col_index];
-
- var $row = $cell.parent('tr');
- var row = self.rows[$row.prevAll().length];
-
- // TODO: we should probably only reload content, also maybe diff records or something, instead of replacing every single row
- self.execute_action(
- field, self.dataset, self.session.action_manager,
- row.data.id.value, self.do_reload);
- });
- $table.delegate(
- 'td.oe-record-delete button', 'click', this.do_delete);
-
- // Global rows handlers
- $table.delegate(
- 'tr', 'click', this.on_select_row);
+ this.list.move_to($table);
// sidebar stuff
if (this.view_manager && this.view_manager.sidebar) {
@@ -192,15 +190,15 @@ openerp.base.ListView = openerp.base.Controller.extend(
* @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(result) {
if (result.view) {
this.on_loaded({fields_view: result.view});
}
var records = result.records;
- var $table = this.$element.find('table');
- this.rows = records;
+
+ this.rows.splice(0, this.rows.length);
+ this.rows.push.apply(this.rows, records);
// Keep current selected record, if it's still in our new search
var current_record_id = this.dataset.ids[this.dataset.index];
@@ -214,46 +212,11 @@ openerp.base.ListView = openerp.base.Controller.extend(
this.dataset.count = this.dataset.ids.length;
var results = this.rows.length;
- $table.find('.oe-pager-last').text(results);
- $table.find('.oe-pager-total').text(results);
+ this.$element.find('table')
+ .find('.oe-pager-last').text(results).end()
+ .find('.oe-pager-total').text(results);
-
- // remove all data lines
- var $old_body = $table.find('tbody');
-
- // add new content
- var columns = this.columns,
- rows = this.rows,
- options = this.options;
-
- // Paginate by groups of 50 for rendering
- var PAGE_SIZE = 50,
- bodies_count = Math.ceil(this.rows.length / PAGE_SIZE),
- body = 0,
- $body = $('
').appendTo($table);
-
- var rendered = $.Deferred();
- var render_body = function () {
- setTimeout(function () {
- $body.append(
- QWeb.render("ListView.rows", {
- columns: columns,
- rows: rows.slice(body*PAGE_SIZE, (body+1)*PAGE_SIZE),
- options: options
- }));
- ++body;
- if (body < bodies_count) {
- render_body();
- } else {
- rendered.resolve();
- }
- }, 0);
- };
- render_body();
-
- return rendered.promise().then(function () {
- $old_body.remove();
- });
+ this.list.refresh();
},
/**
* Used to handle a click on a table row, if no other handler caught the
@@ -279,30 +242,6 @@ openerp.base.ListView = openerp.base.Controller.extend(
}
}, 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')) {
- return;
- }
- // count number of preceding siblings to line clicked
- var row = this.rows[$target.prevAll().length];
-
- var index = _.indexOf(this.dataset.ids, row.data.id.value);
- if (index == undefined || index === -1) {
- return;
- }
- this.switch_to_record(index);
- },
do_show: function () {
this.$element.show();
if (this.hidden) {
@@ -361,13 +300,35 @@ openerp.base.ListView = openerp.base.Controller.extend(
/**
* Handles the signal to delete a line from the DOM
*
- * @param e jQuery event object
+ * @param {Array} ids the id of the object to delete
*/
- do_delete: function (e) {
- // don't link to forms
- e.stopImmediatePropagation();
- this.dataset.unlink(
- [this.rows[$(e.currentTarget).closest('tr').prevAll().length].data.id.value]);
+ do_delete: function (ids) {
+ if (!ids.length) {
+ return;
+ }
+ var self = this;
+ return $.when(this.dataset.unlink(ids)).then(function () {
+ _(self.rows).chain()
+ .map(function (row, index) {
+ return {
+ index: index,
+ id: row.data.id.value
+ };})
+ .filter(function (record) {
+ return _.contains(ids, record.id);
+ })
+ .sort(function (a, b) {
+ // sort in reverse index order, so we delete from the end
+ // and don't blow up the following indexes (leading to
+ // removing the wrong records from the visible list)
+ return b.index - a.index;
+ })
+ .each(function (record) {
+ self.rows.splice(record.index, 1);
+ });
+ // TODO only refresh modified rows
+ self.list.refresh();
+ });
},
/**
* Handles signal for the addition of a new record (can be a creation,
@@ -383,10 +344,89 @@ openerp.base.ListView = openerp.base.Controller.extend(
* Handles deletion of all selected lines
*/
do_delete_selected: function () {
- var selection = this.get_selection();
- if (selection.length) {
- this.dataset.unlink(selection);
- }
+ this.do_delete(
+ this.list.get_selection());
+ }
+ // TODO: implement reorder (drag and drop rows)
+});
+_.extend(openerp.base.ListView.prototype, openerp.base.ActionExecutor);
+
+openerp.base.ListView.List = Class.extend(
+ /** @lends openerp.base.ListView.List# */{
+ /**
+ * List display for the ListView, handles basic DOM events and transforms
+ * them in the relevant higher-level events, to which the list view (or
+ * other consumers) can subscribe.
+ *
+ * Events on this object are registered via jQuery.
+ *
+ * Available events:
+ *
+ * `selected`
+ * Triggered when a row is selected (using check boxes), provides an
+ * array of ids of all the selected records.
+ * `deleted`
+ * Triggered when deletion buttons are hit, provide an array of ids of
+ * all the records being marked for suppression.
+ * `action`
+ * Triggered when an action button is clicked, provides two parameters:
+ *
+ * * The name of the action to execute (as a string)
+ * * The id of the record to execute the action on
+ * `row_link`
+ * Triggered when a row of the table is clicked, provides the index (in
+ * the rows array) and id of the selected record to the handle function.
+ *
+ * @constructs
+ * @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView`
+ */
+ init: function (opts) {
+ var self = this;
+ // columns, rows, options
+
+ this.options = opts.options;
+ this.columns = opts.columns;
+ this.rows = opts.rows;
+
+ this.$_element = $('')
+ .appendTo(document.body)
+ .delegate('th.oe-record-selector', 'click', function (e) {
+ e.stopPropagation();
+ $(self).trigger('selected', [self.get_selection()]);
+ })
+ .delegate('td.oe-record-delete button', 'click', function (e) {
+ e.stopPropagation();
+ var $row = $(e.target).closest('tr');
+ $(self).trigger('deleted', [[self.row_id($row)]]);
+ })
+ .delegate('td.oe-field-cell button', 'click', function (e) {
+ e.stopPropagation();
+ var $target = $(e.currentTarget),
+ field = $target.closest('td').data('field'),
+ record_id = self.row_id($target.closest('tr'));
+
+ $(self).trigger('action', [field, record_id]);
+ })
+ .delegate('tr', 'click', function (e) {
+ e.stopPropagation();
+ $(self).trigger(
+ 'row_link',
+ [self.row_position(e.currentTarget),
+ self.row_id(e.currentTarget)]);
+ });
+ },
+ move_to: function (element) {
+ this.$current = this.$_element.clone(true).appendTo(element);
+ this.render();
+ return this;
+ },
+ render: function () {
+ this.$current.empty().append($(QWeb.render('ListView.rows', this)));
+ return this;
+ },
+ refresh: function () {
+ this.render();
+ return this;
},
/**
* Gets the ids of all currently selected records, if any
@@ -397,15 +437,33 @@ openerp.base.ListView = openerp.base.Controller.extend(
return [];
}
var rows = this.rows;
- return this.$element.find('th.oe-record-selector input:checked')
+ return this.$current.find('th.oe-record-selector input:checked')
.closest('tr').map(function () {
return rows[$(this).prevAll().length].data.id.value;
}).get();
+ },
+ /**
+ * Returns the index of the row in the list of rows.
+ *
+ * @param {Object} row the selected row
+ * @returns {Number} the position of the row in this.rows
+ */
+ row_position: function (row) {
+ return $(row).prevAll().length;
+ },
+ /**
+ * Returns the identifier of the object displayed in the provided table
+ * row
+ *
+ * @param {Object} row the selected table row
+ * @returns {Number|String} the identifier of the row's object
+ */
+ row_id: function (row) {
+ return this.rows[this.row_position(row)].data.id.value;
}
- // TODO: implement sort (click on column headers), if sorted, the list can not be reordered anymore
- // TODO: implement reorder (drag and drop rows)
+ // drag and drop
+ // editable?
});
-_.extend(openerp.base.ListView.prototype, openerp.base.ActionExecutor);
openerp.base.TreeView = openerp.base.Controller.extend({
});
diff --git a/addons/base/static/src/xml/base.xml b/addons/base/static/src/xml/base.xml
index 9e8000c0f95..8675e59818d 100644
--- a/addons/base/static/src/xml/base.xml
+++ b/addons/base/static/src/xml/base.xml
@@ -208,7 +208,7 @@