[MERGE] xmo editable lists

bzr revid: al@openerp.com-20110609012045-off7e8p7qem11vza
This commit is contained in:
Antony Lesuisse 2011-06-09 03:20:45 +02:00
commit 45eb411f04
11 changed files with 581 additions and 185 deletions

View File

@ -611,20 +611,6 @@ class ListView(View):
fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
return {'fields_view': fields_view}
def fields_view_get(self, request, model, view_id, view_type="tree",
transform=True, toolbar=False, submenu=False):
""" Sets @editable on the view's arch if it isn't already set and
``set_editable`` is present in the request context
"""
view = super(ListView, self).fields_view_get(
request, model, view_id, view_type, transform, toolbar, submenu)
view_attributes = view['arch']['attrs']
if request.context.get('set_editable')\
and 'editable' not in view_attributes:
view_attributes['editable'] = 'bottom'
return view
def process_colors(self, view, row, context):
colors = view['arch']['attrs'].get('colors')

View File

@ -28,6 +28,7 @@
<script type="text/javascript" src="/base/static/src/js/views.js"></script>
<script type="text/javascript" src="/base/static/src/js/form.js"></script>
<script type="text/javascript" src="/base/static/src/js/list.js"></script>
<script type="text/javascript" src="/base/static/src/js/list-editable.js"></script>
<script type="text/javascript" src="/base/static/src/js/tree.js"></script>
<script type="text/javascript" src="/base/static/src/js/search.js"></script>
<script type="text/javascript" src="/base/static/src/js/m2o.js"></script>

View File

@ -27,6 +27,10 @@ body.openerp {
overflow-y: scroll;
}
.openerp .oe-number {
text-align: right !important;
}
/* STATES */
.openerp .on_logged {
display: none;
@ -602,6 +606,7 @@ background: linear-gradient(top, #ffffff 0%,#d8d8d8 11%,#afafaf 86%,#333333 91%,
padding: 0;
border: none;
background: none;
width: 100%;
}
.openerp .oe-listview .oe-field-cell button:active {
opacity: 0.5;

View File

@ -132,6 +132,7 @@ openerp.base = function(instance) {
openerp.base.tree(instance);
openerp.base.m2o(instance);
openerp.base.form(instance);
openerp.base.list.editable(instance);
};
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:

View File

@ -149,6 +149,16 @@ openerp.base.Registry = Class.extend( /** @lends openerp.base.Registry# */ {
add: function (key, object_path) {
this.map[key] = object_path;
return this;
},
/**
* Creates and returns a copy of the current mapping, with the provided
* mapping argument added in (replacing existing keys if needed)
*
* @param {Object} [mapping={}] a mapping of keys to object-paths
*/
clone: function (mapping) {
return new openerp.base.Registry(
_.extend({}, this.map, mapping || {}));
}
});

View File

@ -259,7 +259,7 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
*/
read_ids: function (ids, fields, callback) {
var self = this;
this.rpc('/base/dataset/get', {
return this.rpc('/base/dataset/get', {
model: this.model,
ids: ids,
fields: fields
@ -279,10 +279,10 @@ openerp.base.DataSet = openerp.base.Controller.extend( /** @lends openerp.base.
*/
read_index: function (fields, callback) {
if (_.isEmpty(this.ids)) {
callback([]);
return $.Deferred().reject().promise();
} else {
fields = fields || false;
this.read_ids([this.ids[this.index]], fields, function(records) {
return this.read_ids([this.ids[this.index]], fields, function(records) {
callback(records[0]);
});
}

View File

@ -8,12 +8,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV
* view should be displayed (if there is one active).
*/
searchable: false,
template: "FormView",
/**
* @constructs
* @param {openerp.base.Session} session the current openerp session
* @param {String} element_id this view's root element id
* @param {openerp.base.DataSet} dataset the dataset this view will work with
* @param {String} view_id the identifier of the OpenERP view object
*
* @property {openerp.base.Registry} registry=openerp.base.form.widgets widgets registry for this form view instance
*/
init: function(view_manager, session, element_id, dataset, view_id) {
this._super(session, element_id);
@ -29,7 +32,8 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV
this.ready = false;
this.show_invalid = true;
this.touched = false;
this.flags = this.view_manager.flags || {};
this.flags = this.view_manager.action.flags || {};
this.registry = openerp.base.form.widgets;
},
start: function() {
//this.log('Starting FormView '+this.model+this.view_id)
@ -44,9 +48,9 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV
var self = this;
this.fields_view = data.fields_view;
var frame = new openerp.base.form.WidgetFrame(this, this.fields_view.arch);
var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
this.$element.html(QWeb.render("FormView", { 'frame': frame, 'view': this }));
this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this }));
_.each(this.widgets, function(w) {
w.start();
});
@ -220,7 +224,15 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV
self.on_record_loaded(result.result);
});
},
do_save: function(success) {
/**
* Triggers saving the form's record. Chooses between creating a new
* record or saving an existing one depending on whether the record
* already has an id property.
*
* @param {Function} success callback on save success
* @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end)
*/
do_save: function(success, prepend_on_create) {
var self = this;
if (!this.ready) {
return false;
@ -243,7 +255,7 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV
this.log("About to save", values);
if (!this.datarecord.id) {
this.dataset.create(values, function(r) {
self.on_created(r, success);
self.on_created(r, success, prepend_on_create);
});
} else {
this.dataset.write(this.datarecord.id, values, function(r) {
@ -281,19 +293,37 @@ openerp.base.FormView = openerp.base.View.extend( /** @lends openerp.base.FormV
}
}
},
on_created: function(r, success) {
/**
* Updates the form' dataset to contain the new record:
*
* * Adds the newly created record to the current dataset (at the end by
* default)
* * Selects that record (sets the dataset's index to point to the new
* record's id).
* * Updates the pager and sidebar displays
*
* @param {Object} r
* @param {Function} success callback to execute after having updated the dataset
* @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end
*/
on_created: function(r, success, prepend_on_create) {
if (!r.result) {
this.notification.warn("Record not created", "Problem while creating record.");
} else {
this.datarecord.id = arguments[0].result;
this.dataset.ids.push(this.datarecord.id);
this.dataset.index = this.dataset.ids.length - 1;
this.datarecord.id = r.result;
if (!prepend_on_create) {
this.dataset.ids.push(this.datarecord.id);
this.dataset.index = this.dataset.ids.length - 1;
} else {
this.dataset.ids.unshift(this.datarecord.id);
this.dataset.index = 0;
}
this.dataset.count++;
this.do_update_pager();
this.do_update_sidebar();
this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id);
if (success) {
success(r);
success(_.extend(r, {created: true}));
}
}
},
@ -422,6 +452,7 @@ openerp.base.form.compute_domain = function(expr, fields) {
};
openerp.base.form.Widget = openerp.base.Controller.extend({
template: 'Widget',
init: function(view, node) {
this.view = view;
this.node = node;
@ -435,7 +466,6 @@ openerp.base.form.Widget = openerp.base.Controller.extend({
this.view.widgets[this.element_id] = this;
this.children = node.children;
this.colspan = parseInt(node.attrs.colspan || 1);
this.template = "Widget";
this.string = this.string || node.attrs.string;
this.help = this.help || node.attrs.help;
@ -460,9 +490,9 @@ openerp.base.form.Widget = openerp.base.Controller.extend({
});
openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({
template: 'WidgetFrame',
init: function(view, node) {
this._super(view, node);
this.template = "WidgetFrame";
this.columns = node.attrs.col || 4;
this.x = 0;
this.y = 0;
@ -504,9 +534,9 @@ openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({
handle_node: function(node) {
var type = this.view.fields_view.fields[node.attrs.name] || {};
var widget_type = node.attrs.widget || type.type || node.tag;
var widget = new (openerp.base.form.widgets.get_object(widget_type)) (this.view, node);
var widget = new (this.view.registry.get_object(widget_type)) (this.view, node);
if (node.tag == 'field' && node.attrs.nolabel != '1') {
var label = new (openerp.base.form.widgets.get_object('label')) (this.view, node);
var label = new (this.view.registry.get_object('label')) (this.view, node);
label["for"] = widget;
this.add_widget(label);
}
@ -1369,6 +1399,7 @@ openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({
* Registry of form widgets, called by :js:`openerp.base.FormView`
*/
openerp.base.form.widgets = new openerp.base.Registry({
'frame' : 'openerp.base.form.WidgetFrame',
'group' : 'openerp.base.form.WidgetFrame',
'notebook' : 'openerp.base.form.WidgetNotebook',
'separator' : 'openerp.base.form.WidgetSeparator',

View File

@ -0,0 +1,260 @@
/**
* @namespace handles editability case for lists, because it depends on form and forms already depends on lists it had to be split out
*/
openerp.base.list.editable = function (openerp) {
var KEY_RETURN = 13,
KEY_ESCAPE = 27;
// editability status of list rows
openerp.base.ListView.prototype.defaults.editable = null;
var old_init = openerp.base.ListView.prototype.init,
old_actual_search = openerp.base.ListView.prototype.do_actual_search,
old_add_record = openerp.base.ListView.prototype.do_add_record,
old_on_loaded = openerp.base.ListView.prototype.on_loaded;
// TODO: not sure second @lends on existing item is correct, to check
_.extend(openerp.base.ListView.prototype, /** @lends openerp.base.ListView# */{
init: function () {
var self = this;
old_init.apply(this, arguments);
$(this.groups).bind({
'edit': function (e, id, dataset) {
self.do_edit(dataset.index, id, dataset);
},
'saved': function () {
if (self.groups.get_selection().length) {
return;
}
self.compute_aggregates();
}
})
},
/**
* Handles the activation of a record in editable mode (making a record
* editable), called *after* the record has become editable.
*
* The default behavior is to setup the listview's dataset to match
* whatever dataset was provided by the editing List
*
* @param {Number} index index of the record in the dataset
* @param {Object} id identifier of the record being edited
* @param {openerp.base.DataSet} dataset dataset in which the record is available
*/
do_edit: function (index, id, dataset) {
_.extend(this.dataset, dataset);
},
/**
* Sets editability status for the list, based on defaults, view
* architecture and the provided flag, if any.
*
* @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
*/
set_editable: function (force) {
// If ``force``, set editability to bottom
// otherwise rely on view default
// view' @editable is handled separately as we have not yet
// fetched and processed the view at this point.
this.options.editable = (
(force && "bottom")
|| this.defaults.editable);
},
/**
* Replace do_actual_search to handle editability process
*/
do_actual_search: function (results) {
this.set_editable(results.context['set_editable']);
old_actual_search.call(this, results);
},
/**
* Replace do_add_record to handle editability (and adding new record
* as an editable row at the top or bottom of the list)
*/
do_add_record: function () {
if (this.options.editable) {
this.groups.new_record();
} else {
old_add_record.call(this);
}
},
on_loaded: function (data, grouped) {
// tree/@editable takes priority on everything else if present.
this.options.editable = data.fields_view.arch.attrs.editable || this.options.editable;
return old_on_loaded.call(this, data, grouped);
}
});
_.extend(openerp.base.ListView.Groups.prototype, /** @lends openerp.base.ListView.Groups# */{
passtrough_events: openerp.base.ListView.Groups.prototype.passtrough_events + " edit saved",
new_record: function () {
// TODO: handle multiple children
this.children[null].new_record();
}
});
var old_list_row_clicked = openerp.base.ListView.List.prototype.row_clicked;
_.extend(openerp.base.ListView.List.prototype, /** @lends openerp.base.ListView.List */{
row_clicked: function (event) {
if (!this.options.editable) {
return old_list_row_clicked.call(this, event);
}
this.edit_record();
},
/**
* Checks if a record is being edited, and if so cancels it
*/
cancel_pending_edition: function () {
if (!this.edition) {
return;
}
if (this.edition_index !== null) {
this.reload_record(this.edition_index);
}
this.edition_form.stop();
this.edition_form.$element.remove();
delete this.edition_form;
delete this.edition_index;
delete this.edition;
},
render_row_as_form: function (row) {
this.cancel_pending_edition();
var self = this;
var $new_row = $('<tr>', {
id: _.uniqueId('oe-editable-row-'),
'class': $(row).attr('class'),
click: function (e) {e.stopPropagation();}
})
.keyup(function (e) {
switch (e.which) {
case KEY_RETURN:
self.save_row(true);
break;
case KEY_ESCAPE:
self.cancel_edition();
break;
default:
return;
}
})
.delegate('button.oe-edit-row-save', 'click', function () {
self.save_row();
})
.delegate('button.oe-edit-row-cancel', 'click', function () {
self.cancel_edition();
});
if (row) {
$new_row.replaceAll(row);
} else if (this.options.editable === 'top') {
this.$current.prepend($new_row);
} else if (this.options.editable) {
this.$current.append($new_row);
}
this.edition = true;
this.edition_index = this.dataset.index;
this.edition_form = _.extend(new openerp.base.FormView(
null, this.group.view.session, $new_row.attr('id'),
this.dataset, false), {
template: 'ListView.row.form',
registry: openerp.base.list.form.widgets
});
$.when(this.edition_form.on_loaded({fields_view: this.get_fields_view()})).then(function () {
// put in $.when just in case FormView.on_loaded becomes asynchronous
$new_row.find('td')
.addClass('oe-field-cell')
.removeAttr('width')
.end()
.find('td:first').removeClass('oe-field-cell').end()
.find('td:last').removeClass('oe-field-cell').end();
// pad in case of groupby
_(self.columns).each(function (column) {
if (column.meta) {
$new_row.prepend('<td>');
}
});
self.edition_form.do_show();
});
},
/**
* Saves the current row, and triggers the edition of its following
* sibling if asked.
*
* @param {Boolean} [edit_next=false] should the next row become editable
*/
save_row: function (edit_next) {
var self = this;
this.edition_form.do_save(function (result) {
self.reload_record(self.dataset.index, true).then(function () {
self.edition_form.stop();
delete self.edition_form;
delete self.edition_index;
delete self.edition;
$(self).trigger('saved', [self.dataset]);
if (!edit_next) {
return;
}
if (result.created) {
self.new_record();
return;
}
self.dataset.next();
self.edit_record();
});
}, this.options.editable === 'top');
},
/**
* Cancels the edition of the row for the current dataset index
*/
cancel_edition: function () {
this.cancel_pending_edition();
},
/**
* Edits record currently selected via dataset
*/
edit_record: function () {
this.render_row_as_form(
this.$current.children(
_.sprintf('[data-index=%d]',
this.dataset.index)));
$(this).trigger(
'edit',
[this.rows[this.dataset.index].data.id.value, this.dataset]);
},
new_record: function () {
this.dataset.index = null;
this.render_row_as_form();
}
});
openerp.base.list = {form: {}};
openerp.base.list.form.WidgetFrame = openerp.base.form.WidgetFrame.extend({
template: 'ListView.row.frame'
});
var form_widgets = openerp.base.form.widgets;
openerp.base.list.form.widgets = form_widgets.clone({
'frame': 'openerp.base.list.form.WidgetFrame'
});
// All form widgets inherit a problematic behavior from
// openerp.base.form.WidgetFrame: the cell itself is removed when invisible
// whether it's @invisible or @attrs[invisible]. In list view, only the
// former should completely remove the cell. We need to override update_dom
// on all widgets since we can't just hit on widget itself (I think)
var list_form_widgets = openerp.base.list.form.widgets;
_(list_form_widgets.map).each(function (widget_path, key) {
if (key === 'frame') { return; }
var new_path = 'openerp.base.list.form.' + key;
openerp.base.list.form[key] = (form_widgets.get_object(key)).extend({
update_dom: function () {
this.$element.children().css('visibility', '');
if (this.invisible && this.node.attrs.invisible !== '1') {
this.$element.children().css('visibility', 'hidden');
} else {
this._super();
}
}
});
list_form_widgets.add(key, new_path);
});
};

View File

@ -83,8 +83,8 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
'action': function (e, action_name, id, callback) {
self.do_action(action_name, id, callback);
},
'row_link': function (e, index, id, dataset) {
self.do_activate_record(index, id, dataset);
'row_link': function (e, id, dataset) {
self.do_activate_record(dataset.index, id, dataset);
}
});
},
@ -134,9 +134,11 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
this.$element.html(QWeb.render("ListView", this));
// Head hook
this.$element.find('#oe-list-add').click(this.do_add_record);
this.$element.find('#oe-list-add')
.click(this.do_add_record)
.attr('disabled', grouped && this.options.editable);
this.$element.find('#oe-list-delete')
.hide()
.attr('disabled', true)
.click(this.do_delete_selected);
this.$element.find('thead').delegate('th[data-id]', 'click', function (e) {
e.stopPropagation();
@ -199,18 +201,24 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
return column.invisible !== '1';
});
this.aggregate_columns = _(this.columns).chain()
.filter(function (column) {
return column['sum'] || column['avg'];})
this.aggregate_columns = _(this.visible_columns)
.map(function (column) {
var func = column['sum'] ? 'sum' : 'avg';
if (column.type !== 'integer' && column.type !== 'float') {
return {};
}
var aggregation_func = column['group_operator'] || 'sum';
if (!column[aggregation_func]) {
return {};
}
return {
field: column.id,
type: column.type,
'function': func,
label: column[func]
'function': aggregation_func,
label: column[aggregation_func]
};
}).value();
});
},
/**
* Used to handle a click on a table row, if no other handler caught the
@ -267,6 +275,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
return this.rpc('/base/listview/load', {
model: this.model,
view_id: this.view_id,
context: this.dataset.context,
toolbar: !!this.flags.sidebar
}, callback);
}
@ -289,25 +298,32 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
* @returns {$.Deferred} fold request evaluation promise
*/
do_search: function (domains, contexts, groupbys) {
var self = this;
return this.rpc('/base/session/eval_domain_and_context', {
domains: domains,
contexts: contexts,
group_by_seq: groupbys
}, function (results) {
self.dataset.context = results.context;
self.dataset.domain = results.domain;
self.groups.datagroup = new openerp.base.DataGroup(
self.session, self.model,
results.domain, results.context,
results.group_by);
}, $.proxy(this, 'do_actual_search'));
},
/**
* Handler for the result of eval_domain_and_context, actually perform the
* searching
*
* @param {Object} results results of evaluating domain and process for a search
*/
do_actual_search: function (results) {
this.dataset.context = results.context;
this.dataset.domain = results.domain;
this.groups.datagroup = new openerp.base.DataGroup(
this.session, this.model,
results.domain, results.context,
results.group_by);
if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
results.group_by = null;
}
self.reload_view(!!results.group_by).then(
$.proxy(self, 'reload_content'));
});
if (_.isEmpty(results.group_by) && !results.context['group_by_no_leaf']) {
results.group_by = null;
}
this.reload_view(!!results.group_by).then(
$.proxy(this, 'reload_content'));
},
/**
* Handles the signal to delete a line from the DOM
@ -349,13 +365,15 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
*/
do_select: function (ids, records) {
this.$element.find('#oe-list-delete')
.toggle(!!ids.length);
.attr('disabled', !ids.length);
if (!records.length) {
this.compute_aggregates();
return;
}
this.compute_aggregates(records);
this.compute_aggregates(_(records).map(function (record) {
return {count: 1, values: record};
}));
},
/**
* Handles action button signals on a record
@ -382,7 +400,7 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
*
* @param {Number} index index of the record in the dataset
* @param {Object} id identifier of the activated record
* @param {openobject.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
* @param {openerp.base.DataSet} dataset dataset in which the record is available (may not be the listview's dataset in case of nested groups)
*/
do_activate_record: function (index, id, dataset) {
var self = this;
@ -420,75 +438,56 @@ openerp.base.ListView = openerp.base.View.extend( /** @lends openerp.base.ListVi
* @param {Array} [records]
*/
compute_aggregates: function (records) {
if (_.isEmpty(this.aggregate_columns)) {
return;
}
var columns = _(this.aggregate_columns).filter(function (column) {
return column['function']; });
if (_.isEmpty(columns)) { return; }
if (_.isEmpty(records)) {
records = this.groups.get_records();
}
var aggregator = this.build_aggregator(this.aggregate_columns);
this.display_aggregates(
_(records).reduce(aggregator, aggregator).value());
},
/**
* Creates a stateful callable aggregator object, which can be reduced over
* a collection of records in order to build the aggregations described
* by the parameter
*
* @param {Array} aggregation_descriptors
*/
build_aggregator: function (aggregation_descriptors) {
var values = {};
var descriptors = {};
_(aggregation_descriptors).each(function (descriptor) {
values[descriptor.field] = [];
descriptors[descriptor.field] = descriptor;
});
var aggregator = function (_i, record) {
_(values).each(function (collection, key) {
collection.push(record[key]);
});
return aggregator;
};
aggregator.value = function () {
var result = {};
_(values).each(function (collection, key) {
var value;
switch(descriptors[key]['function']) {
case 'avg':
value = (_(collection).chain()
.filter(function (item) {
return !_.isUndefined(item); })
.reduce(function (total, item) {
return total + item; }, 0).value()
/ collection.length);
break;
var count = 0, sums = {};
_(columns).each(function (column) { sums[column.field] = 0; });
_(records).each(function (record) {
count += record.count || 1;
_(columns).each(function (column) {
var field = column.field;
switch (column['function']) {
case 'sum':
value = (_(collection).chain()
.filter(function (item) {
return !_.isUndefined(item); })
.reduce(function (total, item) {
return total + item; }, 0).value());
sums[field] += record.values[field];
break;
case 'avg':
sums[field] += record.count * record.values[field];
break;
}
result[key] = value;
});
});
return result;
};
return aggregator;
var aggregates = {};
_(columns).each(function (column) {
var field = column.field;
switch (column['function']) {
case 'sum':
aggregates[field] = sums[field];
break;
case 'avg':
aggregates[field] = sums[field] / count;
break;
}
});
this.display_aggregates(aggregates);
},
display_aggregates: function (aggregation) {
var $footer = this.$element.find('.oe-list-footer').empty();
var $footer_cells = this.$element.find('.oe-list-footer');
_(this.aggregate_columns).each(function (column) {
$(_.sprintf(
"<span>%s: %.2f</span>",
column.label, aggregation[column.field]))
.appendTo($footer);
if (!column['function']) {
return;
}
var pattern = (column.type == 'integer') ? '%d' : '%.2f';
$footer_cells.filter(_.sprintf('[data-field=%s]', column.field))
.text(_.sprintf(pattern, aggregation[column.field]));
});
}
// TODO: implement reorder (drag and drop rows)
@ -521,8 +520,9 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List
* @constructs
* @param {Object} opts display options, identical to those of :js:class:`openerp.base.ListView`
*/
init: function (opts) {
init: function (group, opts) {
var self = this;
this.group = group;
this.options = opts.options;
this.columns = opts.columns;
@ -546,19 +546,26 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List
e.stopPropagation();
var $target = $(e.currentTarget),
field = $target.closest('td').data('field'),
record_id = self.row_id($target.closest('tr'));
$row = $target.closest('tr'),
record_id = self.row_id($row),
index = self.row_position($row);
$(self).trigger('action', [field, record_id]);
$(self).trigger('action', [field, record_id, function () {
self.reload_record(index, true);
}]);
})
.delegate('tr', 'click', function (e) {
e.stopPropagation();
$(self).trigger(
'row_link',
[self.row_position(e.currentTarget),
self.row_id(e.currentTarget),
self.dataset]);
self.dataset.index = self.row_position(e.currentTarget);
self.row_clicked(e);
});
},
row_clicked: function () {
$(this).trigger(
'row_link',
[this.rows[this.dataset.index].data.id.value,
this.dataset]);
},
render: function () {
if (this.$current) {
this.$current.remove();
@ -566,6 +573,18 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List
this.$current = this.$_element.clone(true);
this.$current.empty().append($(QWeb.render('ListView.rows', this)));
},
get_fields_view: function () {
// deep copy of view
var view = $.extend(true, {}, this.group.view.fields_view);
_(view.arch.children).each(function (widget) {
widget.attrs.nolabel = true;
if (widget.tag === 'button') {
delete widget.attrs.string;
}
});
view.arch.attrs.col = 2 * view.arch.children.length;
return view;
},
/**
* Gets the ids of all currently selected records, if any
* @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves.
@ -579,7 +598,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List
this.$current.find('th.oe-record-selector input:checked')
.closest('tr').each(function () {
var record = {};
_(rows[$(this).prevAll().length].data).each(function (obj, key) {
_(rows[$(this).data('index')].data).each(function (obj, key) {
record[key] = obj.value;
});
result.ids.push(record.id);
@ -594,7 +613,7 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List
* @returns {Number} the position of the row in this.rows
*/
row_position: function (row) {
return $(row).prevAll().length;
return $(row).data('index');
},
/**
* Returns the identifier of the object displayed in the provided table
@ -621,13 +640,86 @@ openerp.base.ListView.List = Class.extend( /** @lends openerp.base.ListView.List
_(row.data).each(function (obj, key) {
record[key] = obj.value;
});
return record;
return {count: 1, values: record};
});
},
/**
* Transforms a record from what is returned by a dataset read (a simple
* mapping of ``$fieldname: $value``) to the format expected by list rows
* and form views:
*
* data: {
* $fieldname: {
* value: $value
* }
* }
*
* This format allows for the insertion of a bunch of metadata (names,
* colors, etc...)
*
* @param {Object} record original record, in dataset format
* @returns {Object} record displayable in a form or list view
*/
transform_record: function (record) {
// TODO: colors handling
var form_data = {},
form_record = {data: form_data};
_(record).each(function (value, key) {
form_data[key] = {value: value};
});
return form_record;
},
/**
* Reloads the record at index ``row_index`` in the list's rows.
*
* By default, simply re-renders the record. If the ``fetch`` parameter is
* provided and ``true``, will first fetch the record anew.
*
* @param {Number} record_index index of the record to reload
* @param {Boolean} fetch fetches the record from remote before reloading it
*/
reload_record: function (record_index, fetch) {
var self = this;
var read_p = null;
if (fetch) {
// save index to restore it later, if already set
var old_index = this.dataset.index;
this.dataset.index = record_index;
read_p = this.dataset.read_index(
_.filter(_.pluck(this.columns, 'name'), _.identity),
function (record) {
var form_record = self.transform_record(record);
self.rows.splice(record_index, 1, form_record);
self.dataset.index = old_index;
}
)
}
return $.when(read_p).then(function () {
self.$current.children().eq(record_index)
.replaceWith(self.render_record(record_index)); })
},
/**
* Renders a list record to HTML
*
* @param {Number} record_index index of the record to render in ``this.rows``
* @returns {String} QWeb rendering of the selected record
*/
render_record: function (record_index) {
return QWeb.render('ListView.row', {
columns: this.columns,
options: this.options,
row: this.rows[record_index],
row_parity: (record_index % 2 === 0) ? 'even' : 'odd',
row_index: record_index
});
}
// drag and drop
// editable?
});
openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Groups# */{
passtrough_events: 'action deleted row_link',
/**
* Grouped display for the ListView. Handles basic DOM events and interacts
* with the :js:class:`~openerp.base.DataGroup` bound to it.
@ -679,24 +771,11 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr
}
return red_letter_tboday;
},
open_group: function (e, group) {
var row = e.currentTarget;
if (this.children[group.value]) {
this.children[group.value].apoptosis();
delete this.children[group.value];
}
var prospekt = this.children[group.value] = new openerp.base.ListView.Groups(this.view, {
options: this.options,
columns: this.columns
});
this.bind_child_events(prospekt);
prospekt.datagroup = group;
prospekt.render().insertAfter(
this.point_insertion(row));
$(row).find('span.ui-icon')
.removeClass('ui-icon-triangle-1-e')
.addClass('ui-icon-triangle-1-s');
open: function (point_insertion) {
this.render().insertAfter(point_insertion);
},
close: function () {
this.apoptosis();
},
/**
* Prefixes ``$node`` with floated spaces in order to indent it relative
@ -716,18 +795,32 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr
var self = this;
var placeholder = this.make_fragment();
_(datagroups).each(function (group) {
if (self.children[group.value]) {
self.children[group.value].apoptosis();
delete self.children[group.value];
}
var child = self.children[group.value] = new openerp.base.ListView.Groups(self.view, {
options: self.options,
columns: self.columns
});
self.bind_child_events(child);
child.datagroup = group;
var $row = $('<tr>');
if (group.openable) {
$row.click(function (e) {
if (!$row.data('open')) {
$row.data('open', true);
self.open_group(e, group);
$row.data('open', true)
.find('span.ui-icon')
.removeClass('ui-icon-triangle-1-e')
.addClass('ui-icon-triangle-1-s');
child.open(self.point_insertion(e.currentTarget));
} else {
$row.removeData('open')
.find('span.ui-icon')
.removeClass('ui-icon-triangle-1-s')
.addClass('ui-icon-triangle-1-e');
_(self.children).each(function (child) {child.apoptosis();});
child.close();
}
});
}
@ -779,21 +872,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr
// can have selections spanning multiple links
var selection = self.get_selection();
$this.trigger(e, [selection.ids, selection.records]);
}).bind('action', function (e, name, id, callback) {
if (!callback) {
callback = function () {
var $prev = child.$current.prev();
if (!$prev.is('tbody')) {
// ungrouped
$(self.elements[0]).replaceWith(self.render());
} else {
// ghetto reload child (and its siblings)
$prev.children().last().click();
}
};
}
$this.trigger(e, [name, id, callback]);
}).bind('deleted row_link', function (e) {
}).bind(this.passtrough_events, function (e) {
// additional positional parameters are provided to trigger as an
// Array, following the event type or event object, but are
// provided to the .bind event handler as *args.
@ -806,7 +885,7 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr
},
render_dataset: function (dataset) {
var rows = [],
list = new openerp.base.ListView.List({
list = new openerp.base.ListView.List(this, {
options: this.options,
columns: this.columns,
dataset: dataset,
@ -819,17 +898,8 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr
_.filter(_.pluck(this.columns, 'name'), _.identity),
0, false,
function (records) {
var form_records = _(records).map(function (record) {
// TODO: colors handling
var form_data = {},
form_record = {data: form_data};
_(record).each(function (value, key) {
form_data[key] = {value: value};
});
return form_record;
});
var form_records = _(records).map(
$.proxy(list, 'transform_record'));
rows.splice(0, rows.length);
rows.push.apply(rows, form_records);
@ -881,6 +951,12 @@ openerp.base.ListView.Groups = Class.extend( /** @lends openerp.base.ListView.Gr
return this;
},
get_records: function () {
if (_(this.children).isEmpty()) {
return {
count: this.datagroup.length,
values: this.datagroup.aggregates
}
}
return _(this.children).chain()
.map(function (child) {
return child.get_records();

View File

@ -9,7 +9,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
init: function(session, element_id) {
this._super(session, element_id);
this.viewmanager = null;
this.dialog_stack = []
this.dialog_stack = [];
// Temporary linking view_manager to session.
// Will use controller_parent to find it when implementation will be done.
session.action_manager = this;
@ -33,8 +33,7 @@ openerp.base.ActionManager = openerp.base.Controller.extend({
case 'ir.actions.act_window':
if (action.target == 'new') {
var element_id = _.uniqueId("act_window_dialog");
var dialog = $('<div id="' + element_id + '"></div>');
dialog.dialog({
$('<div>', {id: element_id}).dialog({
title: action.name,
modal: true,
width: '50%',
@ -347,7 +346,7 @@ openerp.base.Sidebar = openerp.base.BaseWidget.extend({
var action = self.sections[index[0]].elements[index[1]];
action.flags = {
new_window : true
}
};
self.session.action_manager.do_action(action);
e.stopPropagation();
e.preventDefault();

View File

@ -300,11 +300,13 @@
<th t-if="options.deletable"/>
</tr>
</thead>
<tfoot class="ui-widget-header" t-if="aggregate_columns.length">
<tfoot class="ui-widget-header">
<tr>
<td t-att-colspan="columns_count" class='oe-list-footer'>
<td t-if="options.selectable"/>
<td t-foreach="aggregate_columns" t-as="column" class="oe-list-footer oe-number"
t-att-data-field="column.field" t-att-title="column.label">
</td>
<td t-if="options.deletable"/>
</tr>
</tfoot>
</table>
@ -316,7 +318,8 @@
</t-if>
</t>
</t>
<tr t-name="ListView.row" t-att-style="style" t-att-class="row_parity">
<tr t-name="ListView.row" t-att-style="style" t-att-class="row_parity"
t-att-data-index="row_index">
<t t-foreach="columns" t-as="column">
<td t-if="column.meta">
@ -326,8 +329,10 @@
<input type="checkbox"/>
</th>
<t t-foreach="columns" t-as="column">
<t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/>
<td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help"
class="oe-field-cell" t-att-data-field="column.id">
t-att-class="'oe-field-cell' + (align ? ' oe-number' : '')"
t-att-data-field="column.id">
<t t-set="attrs" t-value="column.attrs_for(row.data)"/>
<t t-if="!attrs.invisible">
<t t-set="is_button" t-value="column.tag === 'button'"/>
@ -348,6 +353,9 @@
<button type="button" name="delete"></button>
</td>
</tr>
<t t-name="ListView.row.form">
<t t-raw="frame.render()"/>
</t>
<t t-name="FormView">
<h2 class="oe_view_title"><t t-esc="view.fields_view.arch.attrs.string"/></h2>
<div class="oe_form_header" t-att-id="view.element_id + '_header'">
@ -882,4 +890,23 @@
<button type="button" class="oe_many2xselectpopup-form-save">Save</button>
<button type="button" class="oe_many2xselectpopup-form-close">Close</button>
</t>
<t t-name="ListView.row.frame" t-extend="WidgetFrame">
<t t-jquery="tr">
$(document.createElement('t'))
.append(this.contents())
.attr({
't-foreach': this.attr('t-foreach'),
't-as': this.attr('t-as')
})
.replaceAll(this)
.after($(document.createElement('td')).append(
$(document.createElement('button')).attr({
'class': 'oe-edit-row-save', 'type': 'button'}).text('Save')))
.before($(document.createElement('td')).append(
$(document.createElement('button')).attr({
'class': 'oe-edit-row-cancel', 'type': 'button'}).text('Cancel')))
.unwrap();
</t>
</t>
</templates>