[MERGE] overlay editable list a la rache
bzr revid: al@openerp.com-20120723174341-byh5ep8pg15kpeg9
This commit is contained in:
commit
6f9fbb5d7a
|
@ -2180,9 +2180,33 @@
|
|||
height: auto;
|
||||
line-height: 16px;
|
||||
}
|
||||
.openerp .oe_form_field_one2many .oe_list_buttons.oe_editing .oe_list_save {
|
||||
visibility: hidden;
|
||||
}
|
||||
.openerp .oe_form .oe_form_field_many2many > .oe_list .oe_list_pager_single_page {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_list_buttons .oe_list_save, .openerp .oe_list_buttons .oe_list_discard {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_add, .openerp .oe_list_buttons.oe_editing .oe_list_button_import {
|
||||
display: none;
|
||||
}
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_save {
|
||||
display: inline-block;
|
||||
}
|
||||
.openerp .oe_list_buttons.oe_editing .oe_list_discard {
|
||||
display: inline;
|
||||
}
|
||||
.openerp .oe_list {
|
||||
position: relative;
|
||||
}
|
||||
.openerp .oe_list .oe_form .oe_form_field {
|
||||
width: auto;
|
||||
position: absolute;
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
}
|
||||
.openerp .oe_list_content {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -2240,6 +2264,7 @@
|
|||
}
|
||||
.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell {
|
||||
padding: 3px 6px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.openerp .oe_list_content > tbody > tr > td.oe_list_field_cell progress {
|
||||
width: 100%;
|
||||
|
@ -2290,9 +2315,6 @@
|
|||
.openerp .oe_list_content .numeric input {
|
||||
text-align: right;
|
||||
}
|
||||
.openerp .oe_list_content .oe_list_edit_row_save:before {
|
||||
content: "S";
|
||||
}
|
||||
.openerp .oe_trad_field.touched {
|
||||
border: 1px solid green !important;
|
||||
}
|
||||
|
@ -2419,6 +2441,19 @@
|
|||
.kitten-mode-activated > * {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.kitten-mode-activated .oe_footer a {
|
||||
background-image: url(http://www.risacher.com/la-rache/zfiles/la-rache.png);
|
||||
font-size: 1px;
|
||||
letter-spacing: -1px;
|
||||
color: transparent;
|
||||
display: inline-block;
|
||||
height: 15px;
|
||||
width: 80px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.kitten-mode-activated .oe_footer a span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.ui-widget-overlay {
|
||||
background: black;
|
||||
|
|
|
@ -92,9 +92,8 @@ $sheet-max-width: 860px
|
|||
letter-spacing: -1px
|
||||
color: transparent
|
||||
&:before
|
||||
font-family: "mnmliconsRegular"
|
||||
font: 21px "mnmliconsRegular"
|
||||
content: $icon-name
|
||||
font-size: 20px
|
||||
color: $color
|
||||
|
||||
// }}}
|
||||
|
@ -1721,6 +1720,9 @@ $sheet-max-width: 860px
|
|||
li
|
||||
height: auto
|
||||
line-height: 16px
|
||||
.oe_list_buttons.oe_editing .oe_list_save
|
||||
// keep "save row" button hidden in o2m
|
||||
visibility: hidden
|
||||
// }}}
|
||||
// FormView.many2many {{{
|
||||
.oe_form .oe_form_field_many2many > .oe_list
|
||||
|
@ -1728,6 +1730,25 @@ $sheet-max-width: 860px
|
|||
display: none
|
||||
// }}}
|
||||
// ListView {{{
|
||||
.oe_list_buttons
|
||||
.oe_list_save, .oe_list_discard
|
||||
display: none
|
||||
&.oe_editing
|
||||
.oe_list_add, .oe_list_button_import
|
||||
display: none
|
||||
.oe_list_save
|
||||
display: inline-block
|
||||
.oe_list_discard
|
||||
display: inline
|
||||
|
||||
.oe_list
|
||||
position: relative
|
||||
.oe_form .oe_form_field
|
||||
width: auto
|
||||
position: absolute
|
||||
margin: 0 !important // dammit
|
||||
padding: 0
|
||||
|
||||
.oe_list_content
|
||||
width: 100%
|
||||
td:first-child, th:first-child
|
||||
|
@ -1771,8 +1792,7 @@ $sheet-max-width: 860px
|
|||
border-top: 1px solid #ddd
|
||||
> td.oe_list_field_cell
|
||||
padding: 3px 6px
|
||||
progress
|
||||
width: 100%
|
||||
white-space: pre-line
|
||||
> td, > th
|
||||
> button
|
||||
border: none
|
||||
|
@ -1800,8 +1820,6 @@ $sheet-max-width: 860px
|
|||
width: 82px
|
||||
input
|
||||
text-align: right
|
||||
.oe_list_edit_row_save:before
|
||||
content: "S"
|
||||
// }}}
|
||||
// Translation {{{
|
||||
.oe_trad_field.touched
|
||||
|
@ -1910,6 +1928,7 @@ $sheet-max-width: 860px
|
|||
background-attachment: fixed
|
||||
>*
|
||||
opacity: 0.70
|
||||
|
||||
// }}}
|
||||
|
||||
div.ui-widget-overlay
|
||||
|
|
|
@ -1669,6 +1669,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({
|
|||
return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data"));
|
||||
},
|
||||
load_data:function(){
|
||||
if (!instance.webclient) { return $.Deferred().reject(); }
|
||||
var self = this,dashboard_menu = instance.webclient.menu.data.data.children;
|
||||
var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']);
|
||||
var map_data = function(result){
|
||||
|
|
|
@ -239,6 +239,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode.
|
||||
* @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
|
||||
* @return {$.Deferred}
|
||||
*/
|
||||
do_show: function (options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
@ -253,23 +260,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
}
|
||||
this.$element.show().css('visibility', 'hidden');
|
||||
this.$element.add(this.$buttons).removeClass('oe_form_dirty');
|
||||
return this.has_been_loaded.pipe(function() {
|
||||
var result;
|
||||
if (self.dataset.index === null) {
|
||||
// null index means we should start a new record
|
||||
result = self.on_button_new();
|
||||
} else {
|
||||
result = self.dataset.read_index(_.keys(self.fields_view.fields), {
|
||||
context : { 'bin_size' : true }
|
||||
}).pipe(self.on_record_loaded);
|
||||
}
|
||||
result.pipe(function() {
|
||||
if (options.editable) {
|
||||
self.set({mode: "edit"});
|
||||
|
||||
var shown = this.has_been_loaded;
|
||||
if (options.reload !== false) {
|
||||
shown = shown.pipe(function() {
|
||||
if (self.dataset.index === null) {
|
||||
// null index means we should start a new record
|
||||
return self.on_button_new();
|
||||
}
|
||||
self.$element.css('visibility', 'visible');
|
||||
return self.dataset.read_index(_.keys(self.fields_view.fields), {
|
||||
context: { 'bin_size': true }
|
||||
}).pipe(self.on_record_loaded);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return shown.pipe(function() {
|
||||
if (options.editable) {
|
||||
self.set({mode: "edit"});
|
||||
}
|
||||
self.$element.css('visibility', 'visible');
|
||||
});
|
||||
},
|
||||
do_hide: function () {
|
||||
|
@ -330,6 +338,20 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
self.$element.add(self.$buttons).removeClass('oe_form_dirty');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Loads and sets up the default values for the model as the current
|
||||
* record
|
||||
*
|
||||
* @return {$.Deferred}
|
||||
*/
|
||||
load_defaults: function () {
|
||||
var keys = _.keys(this.fields_view.fields);
|
||||
if (keys.length) {
|
||||
return this.dataset.default_get(keys)
|
||||
.pipe(this.on_record_loaded);
|
||||
}
|
||||
return this.on_record_loaded({});
|
||||
},
|
||||
on_form_changed: function() {
|
||||
this.trigger("view_content_has_changed");
|
||||
},
|
||||
|
@ -600,22 +622,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
on_button_new: function() {
|
||||
var self = this;
|
||||
this.set({mode: "edit"});
|
||||
var def = $.Deferred();
|
||||
$.when(this.has_been_loaded).then(function() {
|
||||
return $.when(this.has_been_loaded).pipe(function() {
|
||||
if (self.can_be_discarded()) {
|
||||
var keys = _.keys(self.fields_view.fields);
|
||||
if (keys.length) {
|
||||
self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() {
|
||||
def.resolve();
|
||||
});
|
||||
} else {
|
||||
self.on_record_loaded({}).then(function() {
|
||||
def.resolve();
|
||||
});
|
||||
}
|
||||
return self.load_defaults();
|
||||
}
|
||||
});
|
||||
return def.promise();
|
||||
},
|
||||
on_button_edit: function() {
|
||||
return this.set({mode: "edit"});
|
||||
|
@ -674,6 +685,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
values = {},
|
||||
first_invalid_field = null;
|
||||
for (var f in self.fields) {
|
||||
if (!self.fields.hasOwnProperty(f)) { continue; }
|
||||
f = self.fields[f];
|
||||
if (!f.is_valid()) {
|
||||
form_invalid = true;
|
||||
|
@ -689,8 +701,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
}
|
||||
if (form_invalid) {
|
||||
self.set({'display_invalid_fields': true});
|
||||
for (var f in self.fields) {
|
||||
self.fields[f]._check_css_flags();
|
||||
for (var g in self.fields) {
|
||||
if (!self.fields.hasOwnProperty(g)) { continue; }
|
||||
self.fields[g]._check_css_flags();
|
||||
}
|
||||
first_invalid_field.focus();
|
||||
self.on_invalid();
|
||||
|
@ -722,14 +735,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
});});
|
||||
},
|
||||
on_invalid: function() {
|
||||
var msg = "<ul>";
|
||||
_.each(this.fields, function(f) {
|
||||
if (!f.is_valid()) {
|
||||
msg += "<li>" + f.string + "</li>";
|
||||
}
|
||||
});
|
||||
msg += "</ul>";
|
||||
this.do_warn("The following fields are invalid :", msg);
|
||||
var warnings = _(this.fields).chain()
|
||||
.filter(function (f) { return !f.is_valid(); })
|
||||
.map(function (f) {
|
||||
return _.str.sprintf('<li>%s</li>',
|
||||
_.escape(f.string));
|
||||
}).value();
|
||||
warnings.unshift('<ul>');
|
||||
warnings.push('</ul>');
|
||||
this.do_warn("The following fields are invalid :", warnings.join(''));
|
||||
},
|
||||
on_saved: function(r, success) {
|
||||
if (!r.result) {
|
||||
|
@ -807,10 +821,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
|
|||
var ids = this.get_selected_ids();
|
||||
values["id"] = ids.length > 0 ? ids[0] : false;
|
||||
_.each(this.fields, function(value_, key) {
|
||||
if (_.include(blacklist, key))
|
||||
if (_.include(blacklist, key)) {
|
||||
return;
|
||||
var val = value_.get_value();
|
||||
values[key] = val;
|
||||
}
|
||||
values[key] = value_.get_value();
|
||||
});
|
||||
return values;
|
||||
},
|
||||
|
@ -991,7 +1005,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
|
|||
}
|
||||
});
|
||||
}
|
||||
selector = 'form[version!="7.0"] page,form[version!="7.0"]';
|
||||
},
|
||||
render_to: function($target) {
|
||||
var self = this;
|
||||
|
@ -1125,7 +1138,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
|
|||
if (found)
|
||||
return;
|
||||
|
||||
$label = $('<label/>').attr({
|
||||
var $label = $('<label/>').attr({
|
||||
'for' : name,
|
||||
"modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
|
||||
"string": $field.attr('string'),
|
||||
|
@ -1339,7 +1352,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
|
|||
return $new_label;
|
||||
},
|
||||
handle_common_properties: function($new_element, $node) {
|
||||
var str_modifiers = $node.attr("modifiers") || "{}"
|
||||
var str_modifiers = $node.attr("modifiers") || "{}";
|
||||
var modifiers = JSON.parse(str_modifiers);
|
||||
var ic = null;
|
||||
if (modifiers.invisible !== undefined)
|
||||
|
@ -1458,24 +1471,27 @@ instance.web.form.compute_domain = function(expr, fields) {
|
|||
*/
|
||||
instance.web.form.InvisibilityChangerMixin = {
|
||||
init: function(field_manager, invisible_domain) {
|
||||
this._ic_field_manager = field_manager
|
||||
var self = this;
|
||||
this._ic_field_manager = field_manager;
|
||||
this._ic_invisible_modifier = invisible_domain;
|
||||
this._ic_field_manager.on("view_content_has_changed", this, function() {
|
||||
var result = this._ic_invisible_modifier === undefined ? false :
|
||||
instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields);
|
||||
this.set({"invisible": result});
|
||||
var result = self._ic_invisible_modifier === undefined ? false :
|
||||
instance.web.form.compute_domain(
|
||||
self._ic_invisible_modifier,
|
||||
self._ic_field_manager.fields);
|
||||
self.set({"invisible": result});
|
||||
});
|
||||
this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
|
||||
var check = function() {
|
||||
if (this.get("invisible") || this.get('force_invisible')) {
|
||||
this.set({"effective_invisible": true});
|
||||
if (self.get("invisible") || self.get('force_invisible')) {
|
||||
self.set({"effective_invisible": true});
|
||||
} else {
|
||||
this.set({"effective_invisible": false});
|
||||
self.set({"effective_invisible": false});
|
||||
}
|
||||
};
|
||||
this.on('change:invisible', this, check);
|
||||
this.on('change:force_invisible', this, check);
|
||||
_.bind(check, this)();
|
||||
check.call(this);
|
||||
},
|
||||
start: function() {
|
||||
this.on("change:effective_invisible", this, this._check_visibility);
|
||||
|
@ -1539,6 +1555,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi
|
|||
var compute_domain = instance.web.form.compute_domain;
|
||||
var to_set = {};
|
||||
for (var a in this.modifiers) {
|
||||
if (!this.modifiers.hasOwnProperty(a)) { continue; }
|
||||
if (!_.include(["invisible"], a)) {
|
||||
var val = compute_domain(this.modifiers[a], this.view.fields);
|
||||
to_set[a] = val;
|
||||
|
@ -1738,18 +1755,18 @@ instance.web.form.FieldInterface = {
|
|||
/**
|
||||
* Get the current value of the widget.
|
||||
*
|
||||
* Must always return a syntaxically correct value to be passed to the "write" method of the osv class in
|
||||
* Must always return a syntactically correct value to be passed to the "write" method of the osv class in
|
||||
* the OpenERP server, although it is not assumed to respect the constraints applied to the field.
|
||||
* For example if the field is marqued as "required", a call to get_value() can return false.
|
||||
* For example if the field is marked as "required", a call to get_value() can return false.
|
||||
*
|
||||
* get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
|
||||
* return a defaut value according to the type of field.
|
||||
* return a default value according to the type of field.
|
||||
*
|
||||
* This method is always assumed to perform synchronously, it can not return a promise.
|
||||
*
|
||||
* If there was no user interaction to modify the value of the field, it is always assumed that
|
||||
* get_value() return the same semantic value than the one passed in the last call to set_value(),
|
||||
* altough the syntax can be different. This can be the case for type of fields that have a different
|
||||
* although the syntax can be different. This can be the case for type of fields that have a different
|
||||
* syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
|
||||
*/
|
||||
get_value: function() {},
|
||||
|
@ -1764,7 +1781,7 @@ instance.web.form.FieldInterface = {
|
|||
*/
|
||||
is_valid: function() {},
|
||||
/**
|
||||
* Returns true if the field holds a value which is syntaxically correct, ignoring
|
||||
* Returns true if the field holds a value which is syntactically correct, ignoring
|
||||
* the potential semantic restrictions applied to the field.
|
||||
*/
|
||||
is_syntax_valid: function() {},
|
||||
|
@ -1794,6 +1811,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
|
|||
* @param node
|
||||
*/
|
||||
init: function(field_manager, node) {
|
||||
var self = this
|
||||
this._super(field_manager, node);
|
||||
this.field_manager = field_manager;
|
||||
this.name = this.node.attrs.name;
|
||||
|
@ -1808,12 +1826,11 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
|
|||
this.set({"readonly": this.modifiers['readonly'] === true});
|
||||
this.set({"force_readonly": false});
|
||||
var test_effective_readonly = function() {
|
||||
this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")});
|
||||
self.set({"effective_readonly": self.get("readonly") || !!self.get("force_readonly")});
|
||||
};
|
||||
this.on("change:readonly", this, test_effective_readonly);
|
||||
this.on("change:force_readonly", this, test_effective_readonly);
|
||||
_.bind(test_effective_readonly, this)();
|
||||
|
||||
test_effective_readonly.call(this);
|
||||
this.on("change:value", this, function() {
|
||||
if (! this._inhibit_on_change)
|
||||
this.trigger('changed_value');
|
||||
|
@ -1890,7 +1907,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
|
|||
*/
|
||||
delay_focus: function($elem) {
|
||||
setTimeout(function() {
|
||||
$elem.focus();
|
||||
$elem[0].focus();
|
||||
}, 50);
|
||||
},
|
||||
/**
|
||||
|
@ -2233,6 +2250,11 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we
|
|||
} else {
|
||||
this.$textarea.attr('disabled', 'disabled');
|
||||
}
|
||||
this.$element.keyup(function (e) {
|
||||
if (e.which === $.ui.keyCode.ENTER) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
this.setupFocus(this.$textarea);
|
||||
},
|
||||
set_value: function(value_) {
|
||||
|
@ -2336,6 +2358,7 @@ instance.web.form.FieldTextHtml = instance.web.form.FieldText.extend({
|
|||
instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
|
||||
template: 'FieldBoolean',
|
||||
start: function() {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.$checkbox = $("input", this.$element);
|
||||
this.setupFocus(this.$checkbox);
|
||||
|
@ -2343,10 +2366,10 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
|
|||
this.set({'value': this.$checkbox.is(':checked')});
|
||||
}, this));
|
||||
var check_readonly = function() {
|
||||
this.$checkbox.prop('disabled', this.get("effective_readonly"));
|
||||
self.$checkbox.prop('disabled', self.get("effective_readonly"));
|
||||
};
|
||||
this.on("change:effective_readonly", this, check_readonly);
|
||||
_.bind(check_readonly, this)();
|
||||
check_readonly.call(this);
|
||||
},
|
||||
set_value: function(value_) {
|
||||
this._super.apply(this, arguments);
|
||||
|
@ -2959,6 +2982,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
|
|||
selectable: self.multi_selection,
|
||||
sortable: false,
|
||||
import_enabled: false,
|
||||
deletable: true
|
||||
});
|
||||
if (self.get("effective_readonly")) {
|
||||
_.extend(view.options, {
|
||||
|
@ -3188,6 +3212,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
|
|||
form: 'instance.web.form.One2ManyFormView',
|
||||
kanban: 'instance.web.form.One2ManyKanbanView',
|
||||
});
|
||||
this.__ignore_blur = false;
|
||||
},
|
||||
switch_view: function(mode, unused) {
|
||||
if (mode !== 'form') {
|
||||
|
@ -3237,19 +3262,34 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
|
|||
this._super(parent, dataset, view_id, _.extend(options || {}, {
|
||||
ListType: instance.web.form.One2ManyList
|
||||
}));
|
||||
this.on('edit:before', this, this.proxy('_before_edit'));
|
||||
this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
|
||||
|
||||
this.records
|
||||
.bind('add', this.proxy("changed_records"))
|
||||
.bind('edit', this.proxy("changed_records"))
|
||||
.bind('remove', this.proxy("changed_records"));
|
||||
},
|
||||
start: function () {
|
||||
var ret = this._super();
|
||||
this.$element
|
||||
.off('mousedown.handleButtons')
|
||||
.on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
|
||||
return ret;
|
||||
},
|
||||
changed_records: function () {
|
||||
this.o2m.trigger_on_change();
|
||||
},
|
||||
is_valid: function () {
|
||||
var form;
|
||||
// A list not being edited is always valid
|
||||
if (!(form = this.first_edition_form())) {
|
||||
return true;
|
||||
}
|
||||
var form = this.editor.form;
|
||||
|
||||
// If the form has not been modified, the view can only be valid
|
||||
// NB: is_dirty will also be set on defaults/onchanges/whatever?
|
||||
// oe_form_dirty seems to only be set on actual user actions
|
||||
if (!form.$element.is('.oe_form_dirty')) {
|
||||
return true;
|
||||
}
|
||||
this.o2m._dirty_flag = true;
|
||||
|
||||
// Otherwise validate internal form
|
||||
return _(form.fields).chain()
|
||||
|
@ -3260,19 +3300,6 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
|
|||
.all(_.identity)
|
||||
.value();
|
||||
},
|
||||
first_edition_form: function () {
|
||||
var get_form = function (group_or_list) {
|
||||
if (group_or_list.edition) {
|
||||
return group_or_list.edition_form;
|
||||
}
|
||||
return _(group_or_list.children).chain()
|
||||
.map(get_form)
|
||||
.compact()
|
||||
.first()
|
||||
.value();
|
||||
};
|
||||
return get_form(this.groups);
|
||||
},
|
||||
do_add_record: function () {
|
||||
if (this.options.editable) {
|
||||
this._super.apply(this, arguments);
|
||||
|
@ -3327,55 +3354,58 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
|
|||
});
|
||||
},
|
||||
do_button_action: function (name, id, callback) {
|
||||
var _super = _.bind(this._super, this);
|
||||
|
||||
this.o2m.view.do_save().then(function () {
|
||||
_super(name, id, callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
instance.web.form.One2ManyList = instance.web.ListView.List.extend({
|
||||
KEY_RETURN: 13,
|
||||
// blurring caused by hitting the [Return] key, should skip the
|
||||
// autosave-on-blur and let the handler for [Return] do its thing
|
||||
__return_blur: false,
|
||||
render_row_as_form: function () {
|
||||
if (!_.isNumber(id)) {
|
||||
instance.webclient.notification.warn(
|
||||
_t("Action Button"),
|
||||
_t("The o2m record must be saved before an action can be used"));
|
||||
return;
|
||||
}
|
||||
var parent_form = this.o2m.view;
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
// Replace the "Save Row" button with "Cancel Edition"
|
||||
self.edition_form.$element
|
||||
.undelegate('button.oe_list_edit_row_save', 'click')
|
||||
.delegate('button.oe_list_edit_row_save', 'click', function () {
|
||||
self.cancel_pending_edition();
|
||||
});
|
||||
|
||||
// Overload execute_action on the edition form to perform a simple
|
||||
// reload_record after the action is done, rather than fully
|
||||
// reload the parent view (or something)
|
||||
var _execute_action = self.edition_form.do_execute_action;
|
||||
self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) {
|
||||
return _execute_action.call(this, action, dataset, record_id, function () {
|
||||
self.view.reload_record(
|
||||
self.view.records.get(record_id));
|
||||
});
|
||||
};
|
||||
|
||||
self.edition_form.on('blurred', null, function () {
|
||||
if (self.__return_blur) {
|
||||
delete self.__return_blur;
|
||||
return;
|
||||
}
|
||||
if (!self.edition_form.widget_is_stopped) {
|
||||
self.view.ensure_saved();
|
||||
}
|
||||
});
|
||||
this.ensure_saved().pipe(function () {
|
||||
return parent_form.do_save();
|
||||
}).then(function () {
|
||||
self.handle_button(name, id, callback);
|
||||
});
|
||||
},
|
||||
on_row_keyup: function (e) {
|
||||
if (e.which === this.KEY_RETURN) {
|
||||
this.__return_blur = true;
|
||||
|
||||
_before_edit: function () {
|
||||
this.__ignore_blur = false;
|
||||
this.editor.form.on('blurred', this, this._on_form_blur);
|
||||
},
|
||||
_before_unedit: function () {
|
||||
this.editor.form.off('blurred', this, this._on_form_blur);
|
||||
},
|
||||
_button_down: function () {
|
||||
// If a button is clicked (usually some sort of action button), it's
|
||||
// the button's responsibility to ensure the editable list is in the
|
||||
// correct state -> ignore form blurring
|
||||
this.__ignore_blur = true;
|
||||
},
|
||||
/**
|
||||
* Handles blurring of the nested form (saves the currently edited row),
|
||||
* unless the flag to ignore the event is set to ``true``
|
||||
*
|
||||
* Makes the internal form go away
|
||||
*/
|
||||
_on_form_blur: function () {
|
||||
if (this.__ignore_blur) {
|
||||
this.__ignore_blur = false;
|
||||
return;
|
||||
}
|
||||
this._super(e);
|
||||
// FIXME: why isn't there an API for this?
|
||||
if (this.editor.form.$element.hasClass('oe_form_dirty')) {
|
||||
this.save_edition();
|
||||
return;
|
||||
}
|
||||
this.cancel_edition();
|
||||
},
|
||||
keyup_ENTER: function () {
|
||||
// blurring caused by hitting the [Return] key, should skip the
|
||||
// autosave-on-blur and let the handler for [Return] do its thing (save
|
||||
// the current row *anyway*, then create a new one/edit the next one)
|
||||
this.__ignore_blur = true;
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3544,7 +3574,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({
|
|||
},
|
||||
start: function() {
|
||||
this._super.apply(this, arguments);
|
||||
this.$element.addClass('oe_form_field_many2many');
|
||||
this.$element.addClass('oe_form_field oe_form_field_many2many');
|
||||
|
||||
var self = this;
|
||||
|
||||
|
|
|
@ -142,7 +142,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
|||
});
|
||||
},
|
||||
/**
|
||||
* View startup method, the default behavior is to set the ``oe_listw``
|
||||
* 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
|
||||
|
@ -288,7 +288,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
|||
}
|
||||
this.$buttons.find('.oe_list_add')
|
||||
.click(this.proxy('do_add_record'))
|
||||
.prop('disabled', grouped && this.options.editable);
|
||||
.prop('disabled', grouped);
|
||||
this.$buttons.on('click', '.oe_list_button_import', function() {
|
||||
self.on_sidebar_import();
|
||||
return false;
|
||||
|
@ -411,20 +411,27 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
|||
if (column.modifiers) {
|
||||
var modifiers = JSON.parse(column.modifiers);
|
||||
column.modifiers_for = function (fields) {
|
||||
if (!modifiers.invisible) {
|
||||
return {};
|
||||
var out = {};
|
||||
|
||||
for (var attr in modifiers) {
|
||||
if (!modifiers.hasOwnProperty(attr)) { continue; }
|
||||
var modifier = modifiers[attr];
|
||||
out[attr] = _.isBoolean(modifier)
|
||||
? modifier
|
||||
: domain_computer(modifier, fields);
|
||||
}
|
||||
return {
|
||||
'invisible': domain_computer(modifiers.invisible, fields)
|
||||
};
|
||||
|
||||
return out;
|
||||
};
|
||||
if (modifiers['tree_invisible']) {
|
||||
column.invisible = '1';
|
||||
} else {
|
||||
delete column.invisible;
|
||||
}
|
||||
column.modifiers = modifiers;
|
||||
} else {
|
||||
column.modifiers_for = noop;
|
||||
column.modifiers = {};
|
||||
}
|
||||
return column;
|
||||
};
|
||||
|
@ -436,10 +443,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
|||
if (grouped) {
|
||||
this.columns.unshift({
|
||||
id: '_group', tag: '', string: _t("Group"), meta: true,
|
||||
modifiers_for: function () { return {}; }
|
||||
modifiers_for: function () { return {}; },
|
||||
modifiers: {}
|
||||
}, {
|
||||
id: '_count', tag: '', string: '#', meta: true,
|
||||
modifiers_for: function () { return {}; }
|
||||
modifiers_for: function () { return {}; },
|
||||
modifiers: {}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -667,6 +676,19 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
|||
* @param {Function} callback should be called after the action is executed, if non-null
|
||||
*/
|
||||
do_button_action: function (name, id, callback) {
|
||||
this.handle_button(name, id, callback);
|
||||
},
|
||||
/**
|
||||
* Base handling of buttons, can be called when overriding do_button_action
|
||||
* in order to bypass parent overrides.
|
||||
*
|
||||
* This method should not be overridden.
|
||||
*
|
||||
* @param {String} name action name
|
||||
* @param {Object} id id of the record the action should be called on
|
||||
* @param {Function} callback should be called after the action is executed, if non-null
|
||||
*/
|
||||
handle_button: function (name, id, callback) {
|
||||
var action = _.detect(this.columns, function (field) {
|
||||
return field.name === name;
|
||||
});
|
||||
|
@ -911,27 +933,42 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
|||
|
||||
this.record_callbacks = {
|
||||
'remove': function (event, record) {
|
||||
var $row = self.$current.find(
|
||||
'[data-id=' + record.get('id') + ']');
|
||||
var $row = self.$current.children(
|
||||
'[data-id=' + record.get('id') + ']');
|
||||
var index = $row.data('index');
|
||||
$row.remove();
|
||||
self.refresh_zebra(index);
|
||||
},
|
||||
'reset': function () { return self.on_records_reset(); },
|
||||
'change': function (event, record) {
|
||||
var $row = self.$current.find('[data-id=' + record.get('id') + ']');
|
||||
'change': function (event, record, attribute, value, old_value) {
|
||||
var $row;
|
||||
if (attribute === 'id') {
|
||||
if (old_value) {
|
||||
throw new Error("Setting 'id' attribute on existing record "
|
||||
+ JSON.stringify(record.attributes));
|
||||
}
|
||||
if (!_.contains(self.dataset.ids, value)) {
|
||||
// add record to dataset if not already in (added by
|
||||
// the form view?)
|
||||
self.dataset.ids.splice(
|
||||
self.records.indexOf(record), 0, value);
|
||||
}
|
||||
// Set id on new record
|
||||
$row = self.$current.children('[data-id=false]');
|
||||
} else {
|
||||
$row = self.$current.children(
|
||||
'[data-id=' + record.get('id') + ']');
|
||||
}
|
||||
$row.replaceWith(self.render_record(record));
|
||||
},
|
||||
'add': function (ev, records, record, index) {
|
||||
var $new_row = $('<tr>').attr({
|
||||
'data-id': record.get('id')
|
||||
});
|
||||
var $new_row = $(self.render_record(record));
|
||||
|
||||
if (index === 0) {
|
||||
$new_row.prependTo(self.$current);
|
||||
} else {
|
||||
var previous_record = records.at(index-1),
|
||||
$previous_sibling = self.$current.find(
|
||||
$previous_sibling = self.$current.children(
|
||||
'[data-id=' + previous_record.get('id') + ']');
|
||||
$new_row.insertAfter($previous_sibling);
|
||||
}
|
||||
|
@ -975,11 +1012,11 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
|||
e.stopPropagation();
|
||||
})
|
||||
.delegate('tr', 'click', function (e) {
|
||||
e.stopPropagation();
|
||||
var row_id = self.row_id(e.currentTarget);
|
||||
if (row_id !== undefined) {
|
||||
if (row_id) {
|
||||
e.stopPropagation();
|
||||
if (!self.dataset.select_id(row_id)) {
|
||||
throw "Could not find id in dataset"
|
||||
throw new Error("Could not find id in dataset");
|
||||
}
|
||||
self.row_clicked(e);
|
||||
}
|
||||
|
@ -1139,6 +1176,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
|||
* @returns {String} QWeb rendering of the selected record
|
||||
*/
|
||||
render_record: function (record) {
|
||||
var self = this;
|
||||
var index = this.records.indexOf(record);
|
||||
return QWeb.render('ListView.row', {
|
||||
columns: this.columns,
|
||||
|
@ -1147,7 +1185,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
|||
row_parity: (index % 2 === 0) ? 'even' : 'odd',
|
||||
view: this.view,
|
||||
render_cell: function () {
|
||||
return this.render_cell.apply(this, arguments); }
|
||||
return self.render_cell.apply(self, arguments); }
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
@ -1831,7 +1869,7 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{
|
|||
* @returns this
|
||||
*/
|
||||
remove: function (record) {
|
||||
var index = _(this.records).indexOf(record);
|
||||
var index = this.indexOf(record);
|
||||
if (index === -1) {
|
||||
_(this._proxies).each(function (proxy) {
|
||||
proxy.remove(record);
|
||||
|
@ -1847,13 +1885,42 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{
|
|||
return this;
|
||||
},
|
||||
|
||||
_onRecordEvent: function (event, record, options) {
|
||||
_onRecordEvent: function (event) {
|
||||
switch(event) {
|
||||
// don't propagate reset events
|
||||
if (event === 'reset') { return; }
|
||||
case 'reset': return;
|
||||
case 'change:id':
|
||||
var record = arguments[1];
|
||||
var new_value = arguments[2];
|
||||
var old_value = arguments[3];
|
||||
// [change:id, record, new_value, old_value]
|
||||
if (this._byId[old_value] === record) {
|
||||
delete this._byId[old_value];
|
||||
this._byId[new_value] = record;
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.trigger.apply(this, arguments);
|
||||
},
|
||||
|
||||
// underscore-type methods
|
||||
find: function (callback) {
|
||||
var record;
|
||||
for(var section in this._proxies) {
|
||||
if (!this._proxies.hasOwnProperty(section)) {
|
||||
continue
|
||||
}
|
||||
if ((record = this._proxies[section].find(callback))) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
for(var i=0; i<this.length; ++i) {
|
||||
record = this.records[i];
|
||||
if (callback(record)) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
},
|
||||
each: function (callback) {
|
||||
for(var section in this._proxies) {
|
||||
if (this._proxies.hasOwnProperty(section)) {
|
||||
|
@ -1878,6 +1945,46 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{
|
|||
},
|
||||
indexOf: function (record) {
|
||||
return _(this.records).indexOf(record);
|
||||
},
|
||||
succ: function (record, options) {
|
||||
options = options || {wraparound: false};
|
||||
var result;
|
||||
for(var section in this._proxies) {
|
||||
if (!this._proxies.hasOwnProperty(section)) {
|
||||
continue;
|
||||
}
|
||||
if ((result = this._proxies[section].succ(record, options))) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
var index = this.indexOf(record);
|
||||
if (index === -1) { return null; }
|
||||
var next_index = index + 1;
|
||||
if (options.wraparound && (next_index === this.length)) {
|
||||
return this.at(0);
|
||||
}
|
||||
return this.at(next_index);
|
||||
},
|
||||
pred: function (record, options) {
|
||||
options = options || {wraparound: false};
|
||||
|
||||
var result;
|
||||
for (var section in this._proxies) {
|
||||
if (!this._proxies.hasOwnProperty(section)) {
|
||||
continue;
|
||||
}
|
||||
if ((result = this._proxies[section].pred(record, options))) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
var index = this.indexOf(record);
|
||||
if (index === -1) { return null; }
|
||||
var next_index = index - 1;
|
||||
if (options.wraparound && (next_index === -1)) {
|
||||
return this.at(this.length - 1);
|
||||
}
|
||||
return this.at(next_index);
|
||||
}
|
||||
});
|
||||
Collection.include(Events);
|
||||
|
|
|
@ -3,10 +3,6 @@
|
|||
* @namespace
|
||||
*/
|
||||
openerp.web.list_editable = function (instance) {
|
||||
var KEY_RETURN = 13,
|
||||
KEY_ESCAPE = 27;
|
||||
var QWeb = instance.web.qweb;
|
||||
|
||||
// editability status of list rows
|
||||
instance.web.ListView.prototype.defaults.editable = null;
|
||||
|
||||
|
@ -15,6 +11,13 @@ openerp.web.list_editable = function (instance) {
|
|||
init: function () {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this.editor = this.make_editor();
|
||||
// Stores records of {field, cell}, allows for re-rendering fields
|
||||
// depending on cell state during and after resize events
|
||||
this.fields_for_resize = [];
|
||||
instance.web.bus.on('resize', this, this.resize_fields);
|
||||
|
||||
$(this.groups).bind({
|
||||
'edit': function (e, id, dataset) {
|
||||
self.do_edit(dataset.index, id, dataset);
|
||||
|
@ -26,7 +29,24 @@ openerp.web.list_editable = function (instance) {
|
|||
self.configure_pager(self.dataset);
|
||||
self.compute_aggregates();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this.records.bind('remove', function () {
|
||||
if (self.editor.is_editing()) {
|
||||
self.cancel_edition();
|
||||
}
|
||||
});
|
||||
|
||||
this.on('edit:after', this, function () {
|
||||
self.$element.add(self.$buttons).addClass('oe_editing');
|
||||
});
|
||||
this.on('save:after cancel:after', this, function () {
|
||||
self.$element.add(self.$buttons).removeClass('oe_editing');
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
instance.web.bus.off('resize', this, this.resize_fields);
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* Handles the activation of a record in editable mode (making a record
|
||||
|
@ -49,6 +69,7 @@ openerp.web.list_editable = function (instance) {
|
|||
* @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom".
|
||||
*/
|
||||
set_editable: function (force) {
|
||||
// TODO: fix handling of editability status to be simpler & clearer & more coherent
|
||||
// If ``force``, set editability to bottom
|
||||
// otherwise rely on view default
|
||||
// view' @editable is handled separately as we have not yet
|
||||
|
@ -71,15 +92,60 @@ openerp.web.list_editable = function (instance) {
|
|||
if (this.options.editable) {
|
||||
this.$element.find('table:first').show();
|
||||
this.$element.find('.oe_view_nocontent').remove();
|
||||
this.groups.new_record();
|
||||
this.start_edition();
|
||||
} else {
|
||||
this._super();
|
||||
}
|
||||
},
|
||||
on_loaded: function (data, grouped) {
|
||||
var self = this;
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
// tree/@editable takes priority on everything else if present.
|
||||
this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable);
|
||||
return this._super(data, grouped);
|
||||
var result = this._super(data, grouped);
|
||||
if (this.options.editable) {
|
||||
// FIXME: any hook available to ensure this is only done once?
|
||||
this.$buttons
|
||||
.off('click', '.oe_list_save')
|
||||
.on('click', '.oe_list_save', this.proxy('save_edition'))
|
||||
.off('click', '.oe_list_discard')
|
||||
.on('click', '.oe_list_discard', function (e) {
|
||||
e.preventDefault();
|
||||
self.cancel_edition();
|
||||
});
|
||||
this.$element
|
||||
.off('click', 'tbody td:not(.oe_list_field_cell)')
|
||||
.on('click', 'tbody td:not(.oe_list_field_cell)', function () {
|
||||
if (!self.editor.is_editing()) {
|
||||
self.start_edition();
|
||||
}
|
||||
});
|
||||
// Editor is not restartable due to formview not being
|
||||
// restartable
|
||||
this.editor = this.make_editor();
|
||||
var editor_ready = this.editor.prependTo(this.$element)
|
||||
.then(this.proxy('setup_events'));
|
||||
|
||||
return $.when(result, editor_ready);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Builds a new editor object
|
||||
*
|
||||
* @return {instance.web.list.Editor}
|
||||
*/
|
||||
make_editor: function () {
|
||||
return new instance.web.list.Editor(this);
|
||||
},
|
||||
do_button_action: function () {
|
||||
var self = this, args = arguments;
|
||||
this.ensure_saved().then(function () {
|
||||
self.handle_button.apply(self, args);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Ensures the editable list is saved (saves any pending edition if
|
||||
|
@ -90,27 +156,487 @@ openerp.web.list_editable = function (instance) {
|
|||
* @returns {$.Deferred}
|
||||
*/
|
||||
ensure_saved: function () {
|
||||
return this.groups.ensure_saved();
|
||||
if (!this.editor.is_editing()) {
|
||||
return $.when();
|
||||
}
|
||||
return this.save_edition();
|
||||
},
|
||||
/**
|
||||
* Set up the edition of a record of the list view "inline"
|
||||
*
|
||||
* @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record
|
||||
* @param {Object} [options]
|
||||
* @param {String} [options.focus_field] field to focus at start of edition
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
start_edition: function (record, options) {
|
||||
var self = this;
|
||||
var item = false;
|
||||
if (record) {
|
||||
item = record.attributes;
|
||||
} else {
|
||||
var attrs = {id: false};
|
||||
_(this.columns).chain()
|
||||
.filter(function (x) { return x.tag === 'field'})
|
||||
.pluck('name')
|
||||
.each(function (field) { attrs[field] = false; });
|
||||
record = new instance.web.list.Record(attrs);
|
||||
this.records.add(record, {
|
||||
at: this.prepends_on_create() ? 0 : null});
|
||||
}
|
||||
var $recordRow = this.groups.get_row_for(record);
|
||||
var cells = this.get_cells_for($recordRow);
|
||||
|
||||
return this.ensure_saved().pipe(function () {
|
||||
self.fields_for_resize.splice(0, self.fields_for_resize.length);
|
||||
return self.with_event('edit', {
|
||||
record: record.attributes,
|
||||
cancel: false
|
||||
}, function () {
|
||||
return self.editor.edit(item, function (field_name, field) {
|
||||
var cell = cells[field_name];
|
||||
if (!cell || field.get('effective_readonly')) {
|
||||
// Readonly fields can just remain the list's,
|
||||
// form's usually don't have backgrounds &al
|
||||
field.set({invisible: true});
|
||||
return;
|
||||
}
|
||||
|
||||
self.fields_for_resize.push({field: field, cell: cell});
|
||||
}, options).pipe(function () {
|
||||
$recordRow.addClass('oe_edition');
|
||||
self.resize_fields();
|
||||
return record.attributes;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
get_cells_for: function ($row) {
|
||||
var cells = {};
|
||||
$row.children('td').each(function (index, el) {
|
||||
cells[el.getAttribute('data-field')] = el
|
||||
});
|
||||
return cells;
|
||||
},
|
||||
/**
|
||||
* If currently editing a row, resizes all registered form fields based
|
||||
* on the corresponding row cell
|
||||
*/
|
||||
resize_fields: function () {
|
||||
if (!this.editor.is_editing()) { return; }
|
||||
for(var i=0, len=this.fields_for_resize.length; i<len; ++i) {
|
||||
var item = this.fields_for_resize[i];
|
||||
this.resize_field(item.field, item.cell);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Resizes a field's root element based on the corresponding cell of
|
||||
* a listview row
|
||||
*
|
||||
* @param {instance.web.form.AbstractField} field
|
||||
* @param {jQuery} cell
|
||||
*/
|
||||
resize_field: function (field, cell) {
|
||||
var $cell = $(cell);
|
||||
var position = $cell.position();
|
||||
|
||||
field.$element.css({
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: $cell.outerWidth(),
|
||||
minHeight: $cell.outerHeight()
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
save_edition: function () {
|
||||
var self = this;
|
||||
return this.with_event('save', {
|
||||
editor: this.editor,
|
||||
form: this.editor.form,
|
||||
cancel: false
|
||||
}, function () {
|
||||
return this.editor.save().pipe(function (attrs) {
|
||||
var created = false;
|
||||
var record = self.records.get(attrs.id);
|
||||
if (!record) {
|
||||
// new record
|
||||
created = true;
|
||||
record = self.records.find(function (r) {
|
||||
return !r.get('id');
|
||||
}).set('id', attrs.id);
|
||||
}
|
||||
// onwrite callback could be altering & reloading the
|
||||
// record which has *just* been saved, so first perform all
|
||||
// onwrites then do a final reload of the record
|
||||
return self.handle_onwrite(record)
|
||||
.pipe(function () {
|
||||
return self.reload_record(record); })
|
||||
.pipe(function () {
|
||||
return { created: created, record: record }; });
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
cancel_edition: function () {
|
||||
var self = this;
|
||||
return this.with_event('cancel', {
|
||||
editor: this.editor,
|
||||
form: this.editor.form,
|
||||
cancel: false
|
||||
}, function () {
|
||||
return this.editor.cancel().pipe(function (attrs) {
|
||||
if (attrs.id) {
|
||||
var record = self.records.get(attrs.id);
|
||||
if (!record) {
|
||||
// Record removed by third party during edition
|
||||
return
|
||||
}
|
||||
return self.reload_record(record);
|
||||
}
|
||||
var to_delete = self.records.find(function (r) {
|
||||
return !r.get('id');
|
||||
});
|
||||
if (to_delete) {
|
||||
self.records.remove(to_delete);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Executes an action on the view's editor bracketed by a cancellable
|
||||
* event of the name provided.
|
||||
*
|
||||
* The event name provided will be post-fixed with ``:before`` and
|
||||
* ``:after``, the ``event`` parameter will be passed alongside the
|
||||
* ``:before`` variant and if the parameter's ``cancel`` key is set to
|
||||
* ``true`` the action *will not be called* and the method will return
|
||||
* a rejection
|
||||
*
|
||||
* @param {String} event_name name of the event
|
||||
* @param {Object} event event object, provided to ``:before`` sub-event
|
||||
* @param {Function} action callable, called with the view's editor as its context
|
||||
* @param {Array} [args] supplementary arguments provided to the action
|
||||
* @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
with_event: function (event_name, event, action) {
|
||||
var self = this;
|
||||
event = event || {};
|
||||
this.trigger(event_name + ':before', event);
|
||||
if (event.cancel) {
|
||||
return $.Deferred().reject({
|
||||
message: _.str.sprintf("Event %s:before cancelled",
|
||||
event_name)});
|
||||
}
|
||||
return $.when(action.call(this)).then(function () {
|
||||
self.trigger.apply(self, [event_name + ':after']
|
||||
.concat(_.toArray(arguments)));
|
||||
});
|
||||
},
|
||||
edition_view: function (editor) {
|
||||
var view = $.extend(true, {}, this.fields_view);
|
||||
view.arch.tag = 'form';
|
||||
_.extend(view.arch.attrs, {
|
||||
'class': 'oe_form_container',
|
||||
version: '7.0'
|
||||
});
|
||||
_(view.arch.children).each(function (widget) {
|
||||
var modifiers = JSON.parse(widget.attrs.modifiers || '{}');
|
||||
widget.attrs.nolabel = true;
|
||||
if (modifiers['tree_invisible'] || widget.tag === 'button') {
|
||||
modifiers.invisible = true;
|
||||
}
|
||||
widget.attrs.modifiers = JSON.stringify(modifiers);
|
||||
});
|
||||
return view;
|
||||
},
|
||||
handle_onwrite: function (source_record) {
|
||||
var self = this;
|
||||
var on_write_callback = self.fields_view.arch.attrs.on_write;
|
||||
if (!on_write_callback) { return $.when(); }
|
||||
return this.dataset.call(on_write_callback, [source_record.get('id')])
|
||||
.pipe(function (ids) {
|
||||
return $.when.apply(
|
||||
null, _(ids).map(
|
||||
_.bind(self.handle_onwrite_record, self, source_record)));
|
||||
});
|
||||
},
|
||||
handle_onwrite_record: function (source_record, id) {
|
||||
var record = this.records.get(id);
|
||||
if (!record) {
|
||||
// insert after the source record
|
||||
var index = this.records.indexOf(source_record) + 1;
|
||||
record = new instance.web.list.Record({id: id});
|
||||
this.records.add(record, {at: index});
|
||||
this.dataset.ids.splice(index, 0, id);
|
||||
}
|
||||
return this.reload_record(record);
|
||||
},
|
||||
prepends_on_create: function () {
|
||||
return this.options.editable === 'top';
|
||||
},
|
||||
setup_events: function () {
|
||||
var self = this;
|
||||
this.editor.$element.on('keyup keydown', function (e) {
|
||||
if (!self.editor.is_editing()) { return; }
|
||||
var key = _($.ui.keyCode).chain()
|
||||
.map(function (v, k) { return {name: k, code: v}; })
|
||||
.find(function (o) { return o.code === e.which; })
|
||||
.value();
|
||||
if (!key) { return; }
|
||||
var method = e.type + '_' + key.name;
|
||||
if (!(method in self)) { return; }
|
||||
self[method](e);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Saves the current record, and goes to the next one (creation or
|
||||
* edition)
|
||||
*
|
||||
* @private
|
||||
* @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit
|
||||
* @return {*}
|
||||
*/
|
||||
_next: function (next_record) {
|
||||
next_record = next_record || 'succ';
|
||||
var self = this;
|
||||
return this.save_edition().pipe(function (saveInfo) {
|
||||
if (saveInfo.created) {
|
||||
return self.start_edition();
|
||||
}
|
||||
return self.start_edition(
|
||||
self.records[next_record](
|
||||
saveInfo.record, {wraparound: true}));
|
||||
});
|
||||
},
|
||||
keyup_ENTER: function () {
|
||||
return this._next();
|
||||
},
|
||||
keyup_ESCAPE: function () {
|
||||
return this.cancel_edition();
|
||||
},
|
||||
_text_selection_range: function (el) {
|
||||
if (el.selectionStart !== undefined) {
|
||||
return {
|
||||
start: el.selectionStart,
|
||||
end: el.selectionEnd
|
||||
};
|
||||
} else if(document.body.createTextRange) {
|
||||
throw new Error("Implement text range handling for MSIE");
|
||||
var sel = document.body.createTextRange();
|
||||
if (sel.parentElement() === el) {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
_text_cursor: function (el) {
|
||||
var selection = this._text_selection_range(el);
|
||||
if (selection.start !== selection.end) {
|
||||
return null;
|
||||
}
|
||||
return selection.start;
|
||||
},
|
||||
keydown_UP: function (e) {
|
||||
if (!this.editor.is_editing('edit')) { return $.when(); }
|
||||
// FIXME: assumes editable widgets are input-type elements
|
||||
var index = this._text_cursor(e.target);
|
||||
// If selecting or not at the start of the input
|
||||
if (index === null || index !== 0) { return $.when(); }
|
||||
|
||||
e.preventDefault();
|
||||
return this._next('pred');
|
||||
},
|
||||
keydown_DOWN: function (e) {
|
||||
if (!this.editor.is_editing('edit')) { return $.when(); }
|
||||
// FIXME: assumes editable widgets are input-type elements
|
||||
var index = this._text_cursor(e.target);
|
||||
// If selecting or not at the end of the input
|
||||
if (index === null || index !== e.target.value.length) { return $.when(); }
|
||||
|
||||
e.preventDefault();
|
||||
return this._next();
|
||||
},
|
||||
keydown_TAB: function (e) {
|
||||
var form = this.editor.form;
|
||||
var last_field = _(form.fields_order).chain()
|
||||
.map(function (name) { return form.fields[name]; })
|
||||
.filter(function (field) { return field.$element.is(':visible'); })
|
||||
.last()
|
||||
.value();
|
||||
// tabbed from last field in form
|
||||
if (last_field && last_field.$element.has(e.target).length) {
|
||||
e.preventDefault();
|
||||
return this._next();
|
||||
}
|
||||
return $.when();
|
||||
}
|
||||
});
|
||||
|
||||
instance.web.list.Editor = instance.web.Widget.extend({
|
||||
/**
|
||||
* @constructs instance.web.list.Editor
|
||||
* @extends instance.web.Widget
|
||||
*
|
||||
* Adapter between listview and formview for editable-listview purposes
|
||||
*
|
||||
* @param {instance.web.Widget} parent
|
||||
* @param {Object} options
|
||||
* @param {instance.web.FormView} [options.formView=instance.web.FormView]
|
||||
* @param {Object} [options.delegate]
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
this._super(parent);
|
||||
this.options = options || {};
|
||||
_.defaults(this.options, {
|
||||
formView: instance.web.FormView,
|
||||
delegate: this.getParent()
|
||||
});
|
||||
this.delegate = this.options.delegate;
|
||||
|
||||
this.record = null;
|
||||
|
||||
this.form = new (this.options.formView)(
|
||||
this, this.delegate.dataset, false, {
|
||||
initial_mode: 'edit',
|
||||
$buttons: $(),
|
||||
$pager: $()
|
||||
});
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
var _super = this._super();
|
||||
this.form.embedded_view = this._validate_view(
|
||||
this.delegate.edition_view(this));
|
||||
var form_ready = this.form.appendTo(this.$element).then(
|
||||
self.form.proxy('do_hide'));
|
||||
return $.when(_super, form_ready);
|
||||
},
|
||||
_validate_view: function (edition_view) {
|
||||
if (!edition_view) {
|
||||
throw new Error("editor delegate's #edition_view must return "
|
||||
+ "a view descriptor");
|
||||
}
|
||||
var arch = edition_view.arch;
|
||||
if (!(arch && arch.children instanceof Array)) {
|
||||
throw new Error("Editor delegate's #edition_view must have a" +
|
||||
" non-empty arch")
|
||||
}
|
||||
if (!(arch.tag === "form")) {
|
||||
throw new Error("Editor delegate's #edition_view must have a" +
|
||||
" 'form' root node");
|
||||
}
|
||||
if (!(arch.attrs && arch.attrs.version === "7.0")) {
|
||||
throw new Error("Editor delegate's #edition_view must be a" +
|
||||
" version 7 view");
|
||||
}
|
||||
if (!/\boe_form_container\b/.test(arch.attrs['class'])) {
|
||||
throw new Error("Editor delegate's #edition_view must have the" +
|
||||
" class 'oe_form_container' on its root" +
|
||||
" element");
|
||||
}
|
||||
|
||||
return edition_view;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} [state] either ``new`` or ``edit``
|
||||
* @return {Boolean}
|
||||
*/
|
||||
is_editing: function (state) {
|
||||
if (!this.record) {
|
||||
return false;
|
||||
}
|
||||
switch(state) {
|
||||
case null: case undefined:
|
||||
return true;
|
||||
case 'new': return !this.record.id;
|
||||
case 'edit': return !!this.record.id;
|
||||
}
|
||||
throw new Error("is_editing's state filter must be either `new` or" +
|
||||
" `edit` if provided");
|
||||
},
|
||||
_focus_setup: function (focus_field) {
|
||||
var form = this.form;
|
||||
|
||||
var field;
|
||||
// If a field to focus was specified
|
||||
if (focus_field
|
||||
// Is actually in the form
|
||||
&& (field = form.fields[focus_field])
|
||||
// And is visible
|
||||
&& field.$element.is(':visible')) {
|
||||
// focus it
|
||||
field.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
_(form.fields_order).detect(function (name) {
|
||||
// look for first visible field in fields_order, focus it
|
||||
var field = form.fields[name];
|
||||
if (!field.$element.is(':visible')) {
|
||||
return false;
|
||||
}
|
||||
field.focus();
|
||||
// Stop as soon as a field got focused
|
||||
return true;
|
||||
});
|
||||
},
|
||||
edit: function (record, configureField, options) {
|
||||
// TODO: specify sequence of edit calls
|
||||
var self = this;
|
||||
var form = self.form;
|
||||
var loaded = record
|
||||
? form.on_record_loaded(_.extend({}, record))
|
||||
: form.load_defaults();
|
||||
|
||||
return loaded.pipe(function () {
|
||||
return form.do_show({reload: false});
|
||||
}).pipe(function () {
|
||||
self.record = form.datarecord;
|
||||
_(form.fields).each(function (field, name) {
|
||||
configureField(name, field);
|
||||
});
|
||||
self._focus_setup(options && options.focus_field);
|
||||
return form;
|
||||
});
|
||||
},
|
||||
save: function () {
|
||||
var self = this;
|
||||
return this.form
|
||||
.do_save(null, this.delegate.prepends_on_create())
|
||||
.pipe(function (result) {
|
||||
var created = result.created && !self.record.id;
|
||||
if (created) {
|
||||
self.record.id = result.result;
|
||||
}
|
||||
return self.cancel();
|
||||
});
|
||||
},
|
||||
cancel: function () {
|
||||
var record = this.record;
|
||||
this.record = null;
|
||||
if (!this.form.can_be_discarded()) {
|
||||
return $.Deferred().reject({
|
||||
message: "The form's data can not be discarded"}).promise();
|
||||
}
|
||||
this.form.do_hide();
|
||||
return $.when(record);
|
||||
}
|
||||
});
|
||||
|
||||
instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{
|
||||
passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved",
|
||||
new_record: function () {
|
||||
// TODO: handle multiple children
|
||||
this.children[null].new_record();
|
||||
},
|
||||
/**
|
||||
* Ensures descendant editable List instances are all saved if they have
|
||||
* pending editions.
|
||||
*
|
||||
* @returns {$.Deferred}
|
||||
*/
|
||||
ensure_saved: function () {
|
||||
return $.when.apply(null,
|
||||
_.invoke(
|
||||
_.values(this.children),
|
||||
'ensure_saved'));
|
||||
get_row_for: function (record) {
|
||||
return _(this.children).chain()
|
||||
.invoke('get_row_for', record)
|
||||
.compact()
|
||||
.first()
|
||||
.value();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -119,325 +645,27 @@ openerp.web.list_editable = function (instance) {
|
|||
if (!this.options.editable) {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
this.edit_record($(event.currentTarget).data('id'));
|
||||
},
|
||||
/**
|
||||
* Checks if a record is being edited, and if so cancels it
|
||||
*/
|
||||
cancel_pending_edition: function () {
|
||||
var self = this, cancelled;
|
||||
if (!this.edition) {
|
||||
return $.when();
|
||||
}
|
||||
|
||||
if (this.edition_id) {
|
||||
cancelled = this.reload_record(this.records.get(this.edition_id));
|
||||
} else {
|
||||
cancelled = $.when();
|
||||
}
|
||||
cancelled.then(function () {
|
||||
self.view.unpad_columns();
|
||||
self.edition_form.destroy();
|
||||
self.edition_form.$element.remove();
|
||||
delete self.edition_form;
|
||||
self.dataset.index = null;
|
||||
delete self.edition_id;
|
||||
delete self.edition;
|
||||
var record_id = $(event.currentTarget).data('id');
|
||||
this.view.start_edition(
|
||||
record_id ? this.records.get(record_id) : null, {
|
||||
focus_field: $(event.target).data('field')
|
||||
});
|
||||
this.pad_table_to(5);
|
||||
return cancelled;
|
||||
},
|
||||
/**
|
||||
* Adapts this list's view description to be suitable to the inner form
|
||||
* view of a row being edited.
|
||||
* If a row mapping to the record (@data-id matching the record's id or
|
||||
* no @data-id if the record has no id), returns it. Otherwise returns
|
||||
* ``null``.
|
||||
*
|
||||
* @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows.
|
||||
* @param {Record} record the record to get a row for
|
||||
* @return {jQuery|null}
|
||||
*/
|
||||
get_form_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;
|
||||
},
|
||||
on_row_keyup: function (e) {
|
||||
var self = this;
|
||||
switch (e.which) {
|
||||
case KEY_RETURN:
|
||||
$(e.target).blur();
|
||||
e.preventDefault();
|
||||
//e.stopImmediatePropagation();
|
||||
setTimeout(function () {
|
||||
self.save_row().then(function (result) {
|
||||
if (result.created) {
|
||||
self.new_record();
|
||||
return;
|
||||
}
|
||||
|
||||
var next_record_id,
|
||||
next_record = self.records.at(
|
||||
self.records.indexOf(result.edited_record) + 1);
|
||||
if (next_record) {
|
||||
next_record_id = next_record.get('id');
|
||||
self.dataset.index = _(self.dataset.ids)
|
||||
.indexOf(next_record_id);
|
||||
} else {
|
||||
self.dataset.index = 0;
|
||||
next_record_id = self.records.at(0).get('id');
|
||||
}
|
||||
self.edit_record(next_record_id);
|
||||
}, 0);
|
||||
});
|
||||
break;
|
||||
case KEY_ESCAPE:
|
||||
this.cancel_edition();
|
||||
break;
|
||||
get_row_for: function (record) {
|
||||
var id;
|
||||
var $row = this.$current.children('[data-id=' + record.get('id') + ']');
|
||||
if ($row.length) {
|
||||
return $row;
|
||||
}
|
||||
},
|
||||
render_row_as_form: function (row) {
|
||||
var self = this;
|
||||
return this.ensure_saved().pipe(function () {
|
||||
var record_id = $(row).data('id');
|
||||
var $new_row = $('<tr>', {
|
||||
id: _.uniqueId('oe-editable-row-'),
|
||||
'data-id': record_id,
|
||||
'class': (row ? $(row).attr('class') : ''),
|
||||
click: function (e) {e.stopPropagation();}
|
||||
})
|
||||
.addClass('oe_form oe_form_container')
|
||||
.delegate('button.oe_list_edit_row_save', 'click', function () {
|
||||
self.save_row();
|
||||
})
|
||||
.delegate('button', 'keyup', function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
})
|
||||
.keyup(function () {
|
||||
return self.on_row_keyup.apply(self, arguments); })
|
||||
.keydown(function (e) { e.stopPropagation(); })
|
||||
.keypress(function (e) {
|
||||
if (e.which === KEY_RETURN) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (row) {
|
||||
$new_row.replaceAll(row);
|
||||
} else if (self.options.editable) {
|
||||
var $last_child = self.$current.children('tr:last');
|
||||
if (self.records.length) {
|
||||
if (self.options.editable === 'top') {
|
||||
$new_row.insertBefore(
|
||||
self.$current.children('[data-id]:first'));
|
||||
} else {
|
||||
$new_row.insertAfter(
|
||||
self.$current.children('[data-id]:last'));
|
||||
}
|
||||
} else {
|
||||
$new_row.prependTo(self.$current);
|
||||
}
|
||||
if ($last_child.is(':not([data-id])')) {
|
||||
$last_child.remove();
|
||||
}
|
||||
}
|
||||
self.edition = true;
|
||||
self.edition_id = record_id;
|
||||
self.dataset.index = _(self.dataset.ids).indexOf(record_id);
|
||||
if (self.dataset.index === -1) {
|
||||
self.dataset.index = null;
|
||||
}
|
||||
self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), {
|
||||
$element: $new_row,
|
||||
editable_list: self
|
||||
});
|
||||
// put in $.when just in case FormView.on_loaded becomes asynchronous
|
||||
return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () {
|
||||
$new_row.find('> td')
|
||||
.end()
|
||||
.find('td:last').removeClass('oe_list_field_cell').end();
|
||||
// pad in case of groupby
|
||||
_(self.columns).each(function (column) {
|
||||
if (column.meta) {
|
||||
$new_row.prepend('<td>');
|
||||
}
|
||||
});
|
||||
// Add column for the save, if
|
||||
// there is none in the list
|
||||
if (!self.options.deletable) {
|
||||
self.view.pad_columns(
|
||||
1, {except: $new_row});
|
||||
}
|
||||
|
||||
self.edition_form.do_show();
|
||||
});
|
||||
});
|
||||
},
|
||||
handle_onwrite: function (source_record_id) {
|
||||
var self = this;
|
||||
var on_write_callback = self.view.fields_view.arch.attrs.on_write;
|
||||
if (!on_write_callback) { return; }
|
||||
this.dataset.call(on_write_callback, [source_record_id], function (ids) {
|
||||
_(ids).each(function (id) {
|
||||
var record = self.records.get(id);
|
||||
if (!record) {
|
||||
// insert after the source record
|
||||
var index = self.records.indexOf(
|
||||
self.records.get(source_record_id)) + 1;
|
||||
record = new instance.web.list.Record({id: id});
|
||||
self.records.add(record, {at: index});
|
||||
self.dataset.ids.splice(index, 0, id);
|
||||
}
|
||||
self.reload_record(record);
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Saves the current row, and returns a Deferred resolving to an object
|
||||
* with the following properties:
|
||||
*
|
||||
* ``created``
|
||||
* Boolean flag indicating whether the record saved was being created
|
||||
* (``true`` or edited (``false``)
|
||||
* ``edited_record``
|
||||
* The result of saving the record (either the newly created record,
|
||||
* or the post-edition record), after insertion in the Collection if
|
||||
* needs be.
|
||||
*
|
||||
* @returns {$.Deferred<{created: Boolean, edited_record: Record}>}
|
||||
*/
|
||||
save_row: function () {
|
||||
//noinspection JSPotentiallyInvalidConstructorUsage
|
||||
var self = this;
|
||||
return this.edition_form
|
||||
.do_save(null, this.options.editable === 'top')
|
||||
.pipe(function (result) {
|
||||
if (result.created && !self.edition_id) {
|
||||
self.records.add({id: result.result},
|
||||
{at: self.options.editable === 'top' ? 0 : null});
|
||||
self.edition_id = result.result;
|
||||
}
|
||||
var edited_record = self.records.get(self.edition_id);
|
||||
|
||||
return $.when(
|
||||
self.handle_onwrite(self.edition_id),
|
||||
self.cancel_pending_edition().then(function () {
|
||||
$(self).trigger('saved', [self.dataset]);
|
||||
})).pipe(function () {
|
||||
return {
|
||||
created: result.created || false,
|
||||
edited_record: edited_record
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* If the current list is being edited, ensures it's saved
|
||||
*/
|
||||
ensure_saved: function () {
|
||||
if (this.edition) {
|
||||
// kinda-hack-ish: if the user has entered data in a field,
|
||||
// oe_form_dirty will be set on the form so save, otherwise
|
||||
// discard the current (entirely empty) line
|
||||
if (this.edition_form.$element.is('.oe_form_dirty')) {
|
||||
return this.save_row();
|
||||
}
|
||||
return this.cancel_pending_edition();
|
||||
}
|
||||
//noinspection JSPotentiallyInvalidConstructorUsage
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* 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 (record_id) {
|
||||
this.render_row_as_form(
|
||||
this.$current.find('[data-id=' + record_id + ']'));
|
||||
$(this).trigger(
|
||||
'edit',
|
||||
[record_id, this.dataset]);
|
||||
},
|
||||
new_record: function () {
|
||||
this.render_row_as_form();
|
||||
},
|
||||
render_record: function (record) {
|
||||
var index = this.records.indexOf(record),
|
||||
self = this;
|
||||
// FIXME: context dict should probably be extracted cleanly
|
||||
return QWeb.render('ListView.row', {
|
||||
columns: this.columns,
|
||||
options: this.options,
|
||||
record: record,
|
||||
row_parity: (index % 2 === 0) ? 'even' : 'odd',
|
||||
view: this.view,
|
||||
render_cell: function () {
|
||||
return self.render_cell.apply(self, arguments); },
|
||||
edited: !!this.edition_form
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
instance.web.ListEditableFormView = instance.web.FormView.extend({
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
this.rendering_engine = new instance.web.ListEditableRenderingEngine(this);
|
||||
this.options.initial_mode = "edit";
|
||||
},
|
||||
renderElement: function() {}
|
||||
});
|
||||
|
||||
instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
|
||||
init: function(view) {
|
||||
this.view = view;
|
||||
},
|
||||
set_fields_view: function(fields_view) {
|
||||
this.fvg = fields_view;
|
||||
},
|
||||
set_tags_registry: function(tags_registry) {
|
||||
this.tags_registry = tags_registry;
|
||||
},
|
||||
set_fields_registry: function(fields_registry) {
|
||||
this.fields_registry = fields_registry;
|
||||
},
|
||||
render_to: function($element) {
|
||||
var self = this;
|
||||
|
||||
var xml = instance.web.json_node_to_xml(this.fvg.arch);
|
||||
var $xml = $(xml);
|
||||
|
||||
if (this.view.editable_list.options.selectable)
|
||||
$("<td>").appendTo($element);
|
||||
|
||||
$xml.children().each(function(i, el) {
|
||||
var modifiers = JSON.parse($(el).attr("modifiers") || "{}");
|
||||
var $td = $("<td>");
|
||||
if (modifiers.tree_invisible === true)
|
||||
$td.hide();
|
||||
var tag_name = el.tagName.toLowerCase();
|
||||
var w;
|
||||
if (tag_name === "field") {
|
||||
var name = $(el).attr("name");
|
||||
var key = $(el).attr('widget') || self.fvg.fields[name].type;
|
||||
var obj = self.view.fields_registry.get_object(key);
|
||||
w = new (obj)(self.view, instance.web.xml_to_json(el));
|
||||
self.view.register_field(w, $(el).attr("name"));
|
||||
} else {
|
||||
var obj = self.tags_registry.get_object(tag_name);
|
||||
w = new (obj)(self.view, instance.web.xml_to_json(el));
|
||||
}
|
||||
w.appendTo($td);
|
||||
$td.appendTo($element);
|
||||
});
|
||||
$(QWeb.render('ListView.row.save')).appendTo($element);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1348,7 +1348,9 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
|
|||
if (typeof(node) === 'string') {
|
||||
return sindent + node;
|
||||
} else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
|
||||
throw("Node a json node");
|
||||
throw new Error(
|
||||
_.str.sprintf("Node [%s] is not a JSONified XML node",
|
||||
JSON.stringify(node)));
|
||||
}
|
||||
for (var attr in node.attrs) {
|
||||
var vattr = node.attrs[attr];
|
||||
|
|
|
@ -630,9 +630,9 @@
|
|||
<button type="button" class="oe_button oe_list_add oe_highlight">
|
||||
<t t-esc="widget.options.addable"/>
|
||||
</button>
|
||||
<t t-if="widget.options.import_enabled">
|
||||
<span class="oe_alternative" t-if="widget.options.import_enabled">
|
||||
<span class="oe_fade">or</span> <a href="#" class="oe_bold oe_list_button_import">Import</a>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<t t-name="ListView.pager">
|
||||
|
@ -652,6 +652,7 @@
|
|||
<tr t-name="ListView.row" t-att-class="row_parity"
|
||||
t-att-data-id="record.get('id')"
|
||||
t-att-style="view.style_for(record)">
|
||||
<t t-set="asData" t-value="record.toForm().data"/>
|
||||
<t t-foreach="columns" t-as="column">
|
||||
<td t-if="column.meta">
|
||||
|
||||
|
@ -663,23 +664,26 @@
|
|||
<input t-if="!options.radio" type="checkbox" name="radiogroup" t-att-checked="checked"/>
|
||||
</th>
|
||||
<t t-foreach="columns" t-as="column">
|
||||
<t t-set="align" t-value="column.type === 'integer' or column.type == 'float'"/>
|
||||
<t t-set="number" t-value="column.type === 'integer' or column.type == 'float'"/>
|
||||
<t t-set="modifiers" t-value="column.modifiers_for(asData)"/>
|
||||
<td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help"
|
||||
t-att-class="'oe_list_field_cell' + (align ? ' oe_number' : '')
|
||||
+ (column.tag === 'button' ? ' oe_button' : '')"
|
||||
t-att-data-field="column.id">
|
||||
<t t-raw="render_cell(record, column)"/>
|
||||
</td>
|
||||
t-attf-class="oe_list_field_cell oe_list_field_#{column.widget or column.type} #{number ? 'oe_number' : ''} #{column.tag === 'button' ? 'oe-button' : ''} #{modifiers.readonly ? 'oe_readonly' : ''}"
|
||||
t-att-data-field="column.id"
|
||||
><t t-raw="render_cell(record, column)"/></td>
|
||||
</t>
|
||||
<td t-if="options.deletable" class='oe_list_record_delete' width="1">
|
||||
<button type="button" name="delete" class="oe_i">d</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<t t-name="ListView.row.save">
|
||||
<td>
|
||||
<button class='oe_i oe_list_edit_row_save' type='button' name='save'/>
|
||||
</td>
|
||||
<t t-extend="ListView.buttons">
|
||||
<t t-jquery="button.oe_list_add" t-operation="after">
|
||||
<button class="oe_button oe_list_save oe_highlight"
|
||||
type="button">Save</button>
|
||||
</t>
|
||||
<t t-jquery="a.oe_list_button_import" t-operation="after">
|
||||
<a href="#" class="oe_bold oe_list_discard">discard</a>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="FormView">
|
||||
|
@ -1498,7 +1502,7 @@
|
|||
missing columns
|
||||
-->
|
||||
<t t-jquery="> :last" t-operation="after">
|
||||
<td t-if="edited and !options.deletable" class="oe_list_padding"/>
|
||||
<td t-if="edited and !options.deletable" class="oe-listview-padding"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
|
|
|
@ -0,0 +1,352 @@
|
|||
$(document).ready(function () {
|
||||
var $fix = $('#qunit-fixture');
|
||||
|
||||
var instance;
|
||||
var baseSetup = function () {
|
||||
instance = openerp.testing.instanceFor('list_editable');
|
||||
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Object} [attrs]
|
||||
* @param {String} [attrs.type="char"]
|
||||
* @param {Boolean} [attrs.required]
|
||||
* @param {Boolean} [attrs.invisible]
|
||||
* @param {Boolean} [attrs.readonly]
|
||||
* @return {Object}
|
||||
*/
|
||||
function field(name, attrs) {
|
||||
attrs = attrs || {};
|
||||
attrs.name = name;
|
||||
return _.defaults(attrs, {
|
||||
type: 'char'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} fields
|
||||
* @return {Object}
|
||||
*/
|
||||
function makeFormView(fields) {
|
||||
var fobj = {};
|
||||
_(fields).each(function (field) {
|
||||
fobj[field.name] = {
|
||||
type: field.type,
|
||||
string: field.string
|
||||
};
|
||||
});
|
||||
var children = _(fields).map(function (field) {
|
||||
return {
|
||||
tag: 'field',
|
||||
attrs: {
|
||||
name: field.name,
|
||||
modifiers: JSON.stringify({
|
||||
required: field.required,
|
||||
invisible: field.invisible,
|
||||
readonly: field.readonly
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
arch: {
|
||||
tag: 'form',
|
||||
attrs: {
|
||||
version: '7.0',
|
||||
'class': 'oe_form_container'
|
||||
},
|
||||
children: children
|
||||
},
|
||||
fields: fobj
|
||||
};
|
||||
}
|
||||
|
||||
module('editor', {
|
||||
setup: baseSetup
|
||||
});
|
||||
asyncTest('base-state', 2, function () {
|
||||
var e = new instance.web.list.Editor({
|
||||
dataset: {},
|
||||
edition_view: function () {
|
||||
return makeFormView();
|
||||
}
|
||||
});
|
||||
e.appendTo($fix)
|
||||
.always(start)
|
||||
.fail(function (error) { ok(false, error && error.message); })
|
||||
.done(function () {
|
||||
ok(!e.is_editing(), "should not be editing");
|
||||
ok(e.form instanceof instance.web.FormView,
|
||||
"should use default form type");
|
||||
});
|
||||
});
|
||||
asyncTest('toggle-edition-save', 4, function () {
|
||||
instance.connection.responses['/web/dataset/call_kw:create'] = function () {
|
||||
return { result: 42 };
|
||||
};
|
||||
instance.connection.responses['/web/dataset/call_kw:read'] = function () {
|
||||
return { result: [{
|
||||
id: 42,
|
||||
a: false,
|
||||
b: false,
|
||||
c: false
|
||||
}]};
|
||||
};
|
||||
var e = new instance.web.list.Editor({
|
||||
dataset: new instance.web.DataSetSearch(),
|
||||
prepends_on_create: function () { return false; },
|
||||
edition_view: function () {
|
||||
return makeFormView([ field('a'), field('b'), field('c') ]);
|
||||
}
|
||||
});
|
||||
var counter = 0;
|
||||
e.appendTo($fix)
|
||||
.pipe(function () {
|
||||
return e.edit({}, function () {
|
||||
++counter;
|
||||
});
|
||||
})
|
||||
.pipe(function (form) {
|
||||
ok(e.is_editing(), "should be editing");
|
||||
equal(counter, 3, "should have configured all fields");
|
||||
return e.save();
|
||||
})
|
||||
.always(start)
|
||||
.fail(function (error) { ok(false, error && error.message); })
|
||||
.done(function (record) {
|
||||
ok(!e.is_editing(), "should have stopped editing");
|
||||
equal(record.id, 42, "should have newly created id");
|
||||
})
|
||||
});
|
||||
asyncTest('toggle-edition-cancel', 2, function () {
|
||||
instance.connection.responses['/web/dataset/call_kw:create'] = function () {
|
||||
return { result: 42 };
|
||||
};
|
||||
var e = new instance.web.list.Editor({
|
||||
dataset: new instance.web.DataSetSearch(),
|
||||
prepends_on_create: function () { return false; },
|
||||
edition_view: function () {
|
||||
return makeFormView([ field('a'), field('b'), field('c') ]);
|
||||
}
|
||||
});
|
||||
var counter = 0;
|
||||
e.appendTo($fix)
|
||||
.pipe(function () {
|
||||
return e.edit({}, function () {
|
||||
++counter;
|
||||
});
|
||||
})
|
||||
.pipe(function (form) {
|
||||
return e.cancel();
|
||||
})
|
||||
.always(start)
|
||||
.fail(function (error) { ok(false, error && error.message); })
|
||||
.done(function (record) {
|
||||
ok(!e.is_editing(), "should have stopped editing");
|
||||
ok(!record.id, "should have no id");
|
||||
})
|
||||
});
|
||||
asyncTest('toggle-save-required', 2, function () {
|
||||
instance.connection.responses['/web/dataset/call_kw:create'] = function () {
|
||||
return { result: 42 };
|
||||
};
|
||||
var e = new instance.web.list.Editor({
|
||||
do_warn: function () {
|
||||
warnings++;
|
||||
},
|
||||
dataset: new instance.web.DataSetSearch(),
|
||||
prepends_on_create: function () { return false; },
|
||||
edition_view: function () {
|
||||
return makeFormView([
|
||||
field('a', {required: true}), field('b'), field('c') ]);
|
||||
}
|
||||
});
|
||||
var counter = 0;
|
||||
var warnings = 0;
|
||||
e.appendTo($fix)
|
||||
.pipe(function () {
|
||||
return e.edit({}, function () {
|
||||
++counter;
|
||||
});
|
||||
})
|
||||
.pipe(function (form) {
|
||||
return e.save();
|
||||
})
|
||||
.always(start)
|
||||
.done(function () { ok(false, "cancel should not succeed"); })
|
||||
.fail(function () {
|
||||
equal(warnings, 1, "should have been warned");
|
||||
ok(e.is_editing(), "should have kept editing");
|
||||
})
|
||||
});
|
||||
|
||||
module('list-edition', {
|
||||
setup: function () {
|
||||
baseSetup();
|
||||
|
||||
var records = {};
|
||||
_.extend(instance.connection.responses, {
|
||||
'/web/listview/load': function () {
|
||||
return {result: {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
a: {type: 'char', string: "A"},
|
||||
b: {type: 'char', string: "B"},
|
||||
c: {type: 'char', string: "C"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'tree',
|
||||
attrs: {},
|
||||
children: [
|
||||
{tag: 'field', attrs: {name: 'a'}},
|
||||
{tag: 'field', attrs: {name: 'b'}},
|
||||
{tag: 'field', attrs: {name: 'c'}}
|
||||
]
|
||||
}
|
||||
}};
|
||||
},
|
||||
'/web/dataset/call_kw:create': function (params) {
|
||||
records[42] = _.extend({}, params.params.args[0]);
|
||||
return {result: 42};
|
||||
},
|
||||
'/web/dataset/call_kw:read': function (params) {
|
||||
var id = params.params.args[0][0];
|
||||
if (id in records) {
|
||||
return {result: [records[id]]};
|
||||
}
|
||||
return {result: []};
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
asyncTest('newrecord', 6, function () {
|
||||
var got_defaults = false;
|
||||
instance.connection.responses['/web/dataset/call_kw:default_get'] = function (params) {
|
||||
var fields = params.params.args[0];
|
||||
deepEqual(
|
||||
fields, ['a', 'b', 'c'],
|
||||
"should ask defaults for all fields");
|
||||
got_defaults = true;
|
||||
return {result: {
|
||||
a: "qux",
|
||||
b: "quux"
|
||||
}};
|
||||
};
|
||||
|
||||
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
|
||||
var l = new instance.web.ListView({}, ds);
|
||||
l.set_editable(true);
|
||||
|
||||
l.appendTo($fix)
|
||||
.pipe(l.proxy('reload_content'))
|
||||
.pipe(function () {
|
||||
return l.start_edition();
|
||||
})
|
||||
.always(start)
|
||||
.pipe(function () {
|
||||
ok(got_defaults, "should have fetched default values for form");
|
||||
return l.save_edition();
|
||||
})
|
||||
.pipe(function (result) {
|
||||
ok(result.created, "should yield newly created record");
|
||||
equal(result.record.get('a'), "qux",
|
||||
"should have used default values");
|
||||
equal(result.record.get('b'), "quux",
|
||||
"should have used default values");
|
||||
ok(!result.record.get('c'),
|
||||
"should have no value if there was no default");
|
||||
})
|
||||
.fail(function (e) { ok(false, e && e.message || e); });
|
||||
});
|
||||
|
||||
module('list-edition-events', {
|
||||
setup: function () {
|
||||
baseSetup();
|
||||
_.extend(instance.connection.responses, {
|
||||
'/web/listview/load': function () {
|
||||
return {result: {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
a: {type: 'char', string: "A"},
|
||||
b: {type: 'char', string: "B"},
|
||||
c: {type: 'char', string: "C"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'tree',
|
||||
attrs: {},
|
||||
children: [
|
||||
{tag: 'field', attrs: {name: 'a'}},
|
||||
{tag: 'field', attrs: {name: 'b'}},
|
||||
{tag: 'field', attrs: {name: 'c'}}
|
||||
]
|
||||
}
|
||||
}};
|
||||
},
|
||||
'/web/dataset/call_kw:read': function (params) {
|
||||
return {result: [{
|
||||
id: 1,
|
||||
a: 'foo',
|
||||
b: 'bar',
|
||||
c: 'baz'
|
||||
}]};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
asyncTest('edition events', 4, function () {
|
||||
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
|
||||
var o = {
|
||||
counter: 0,
|
||||
onEvent: function (e) { this.counter++; }
|
||||
};
|
||||
var l = new instance.web.ListView({}, ds);
|
||||
l.set_editable(true);
|
||||
l.on('edit:before edit:after', o, o.onEvent);
|
||||
l.appendTo($fix)
|
||||
.pipe(l.proxy('reload_content'))
|
||||
.always(start)
|
||||
.pipe(function () {
|
||||
ok(l.options.editable, "should be editable");
|
||||
equal(o.counter, 0, "should have seen no event yet");
|
||||
return l.start_edition(l.records.get(1));
|
||||
})
|
||||
.pipe(function () {
|
||||
ok(l.editor.is_editing(), "should be editing");
|
||||
equal(o.counter, 2, "should have seen two edition events");
|
||||
})
|
||||
.fail(function (e) { ok(false, e && e.message); });
|
||||
});
|
||||
|
||||
asyncTest('edition events: cancelling', 3, function () {
|
||||
var edit_after = false;
|
||||
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]);
|
||||
var l = new instance.web.ListView({}, ds);
|
||||
l.set_editable(true);
|
||||
l.on('edit:before', {}, function (e) {
|
||||
e.cancel = true;
|
||||
});
|
||||
l.on('edit:after', {}, function () {
|
||||
edit_after = true;
|
||||
});
|
||||
l.appendTo($fix)
|
||||
.pipe(l.proxy('reload_content'))
|
||||
.always(start)
|
||||
.pipe(function () {
|
||||
ok(l.options.editable, "should be editable");
|
||||
return l.start_edition();
|
||||
})
|
||||
// cancelling an event rejects the deferred
|
||||
.pipe($.Deferred().reject(), function () {
|
||||
ok(!l.editor.is_editing(), "should not be editing");
|
||||
ok(!edit_after, "should not have fired the edit:after event");
|
||||
return $.when();
|
||||
})
|
||||
.fail(function (e) { ok(false, e && e.message || e); });
|
||||
});
|
||||
});
|
|
@ -133,7 +133,7 @@ $(document).ready(function () {
|
|||
strictEqual(changed, 1);
|
||||
});
|
||||
|
||||
module('list-collections-degenerate', {
|
||||
module('list-collections', {
|
||||
setup: function () {
|
||||
openerp = window.openerp.init([]);
|
||||
window.openerp.web.corelib(openerp);
|
||||
|
@ -145,7 +145,7 @@ $(document).ready(function () {
|
|||
window.openerp.web.list(openerp);
|
||||
}
|
||||
});
|
||||
test('Fetch from collection', function () {
|
||||
test('degenerate-fetch', function () {
|
||||
var c = new openerp.web.list.Collection();
|
||||
strictEqual(c.length, 0);
|
||||
c.add({id: 1, value: 2});
|
||||
|
@ -163,7 +163,7 @@ $(document).ready(function () {
|
|||
strictEqual(r2.get('id'), 1);
|
||||
strictEqual(r2.get('value'), 2);
|
||||
});
|
||||
test('Add at index', function () {
|
||||
test('degenerate-indexed-add', function () {
|
||||
var c = new openerp.web.list.Collection([
|
||||
{id: 1, value: 5},
|
||||
{id: 2, value: 10},
|
||||
|
@ -175,7 +175,7 @@ $(document).ready(function () {
|
|||
strictEqual(c.at(1).get('value'), 55);
|
||||
strictEqual(c.at(3).get('value'), 20);
|
||||
});
|
||||
test('Remove record', function () {
|
||||
test('degenerate-remove', function () {
|
||||
var c = new openerp.web.list.Collection([
|
||||
{id: 1, value: 5},
|
||||
{id: 2, value: 10},
|
||||
|
@ -188,7 +188,7 @@ $(document).ready(function () {
|
|||
equal(c.get(2), undefined);
|
||||
strictEqual(c.at(1).get('value'), 20);
|
||||
});
|
||||
test('Remove unbind', function () {
|
||||
test('degenerate-remove-bound', function () {
|
||||
var changed = false,
|
||||
c = new openerp.web.list.Collection([ {id: 1, value: 5} ]);
|
||||
c.bind('change', function () { changed = true; });
|
||||
|
@ -198,7 +198,7 @@ $(document).ready(function () {
|
|||
ok(!changed, 'removed records should not trigger events in their ' +
|
||||
'parent collection');
|
||||
});
|
||||
test('Reset', function () {
|
||||
test('degenerate-reset', function () {
|
||||
var event, obj, c = new openerp.web.list.Collection([
|
||||
{id: 1, value: 5},
|
||||
{id: 2, value: 10},
|
||||
|
@ -218,7 +218,7 @@ $(document).ready(function () {
|
|||
strictEqual(c.length, 1);
|
||||
strictEqual(c.get(42).get('value'), 55);
|
||||
});
|
||||
test('Reset unbind', function () {
|
||||
test('degenerate-reset-bound', function () {
|
||||
var changed = false,
|
||||
c = new openerp.web.list.Collection([ {id: 1, value: 5} ]);
|
||||
c.bind('change', function () { changed = true; });
|
||||
|
@ -229,7 +229,7 @@ $(document).ready(function () {
|
|||
'parent collection');
|
||||
});
|
||||
|
||||
test('Events propagation', function () {
|
||||
test('degenerate-propagations', function () {
|
||||
var values = [];
|
||||
var c = new openerp.web.list.Collection([
|
||||
{id: 1, value: 5},
|
||||
|
@ -260,6 +260,82 @@ $(document).ready(function () {
|
|||
c.at(1).set('wealth', 5);
|
||||
strictEqual(total, 47);
|
||||
});
|
||||
test('degenerate-successor', function () {
|
||||
var root = new openerp.web.list.Collection([
|
||||
{id: 1, value: 1},
|
||||
{id: 2, value: 2},
|
||||
{id: 3, value: 3},
|
||||
{id: 4, value: 5},
|
||||
{id: 5, value: 8}
|
||||
]);
|
||||
|
||||
deepEqual(root.succ(root.at(2)).attributes,
|
||||
root.at(3).attributes,
|
||||
"should return the record at (index + 1) from the pivot");
|
||||
equal(root.succ(root.at(4)), null,
|
||||
"should return null as successor to last record");
|
||||
deepEqual(root.succ(root.at(4), {wraparound: true}).attributes,
|
||||
root.at(0).attributes,
|
||||
"should return index 0 as successor to last record if" +
|
||||
" wraparound is set");
|
||||
deepEqual(root.succ(root.at(2), {wraparound: true}).attributes,
|
||||
root.at(3).attributes,
|
||||
"wraparound should have no effect if not succ(last_record)");
|
||||
});
|
||||
test('successor', function () {
|
||||
var root = new openerp.web.list.Collection();
|
||||
root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]);
|
||||
root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]);
|
||||
root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]);
|
||||
|
||||
deepEqual(root.succ(root.get(3)).attributes,
|
||||
root.get(4).attributes,
|
||||
"should get successor");
|
||||
equal(root.succ(root.get(4)),
|
||||
null,
|
||||
"successors do not cross collections");
|
||||
deepEqual(root.succ(root.get(4), {wraparound: true}).attributes,
|
||||
root.get(3).attributes,
|
||||
"should wraparound within a collection");
|
||||
});
|
||||
test('degenerate-predecessor', function () {
|
||||
var root = new openerp.web.list.Collection([
|
||||
{id: 1, value: 1},
|
||||
{id: 2, value: 2},
|
||||
{id: 3, value: 3},
|
||||
{id: 4, value: 5},
|
||||
{id: 5, value: 8}
|
||||
]);
|
||||
|
||||
deepEqual(root.pred(root.at(2)).attributes,
|
||||
root.at(1).attributes,
|
||||
"should return the record at (index - 1) from the pivot");
|
||||
equal(root.pred(root.at(0)), null,
|
||||
"should return null as predecessor to first record");
|
||||
deepEqual(root.pred(root.at(0), {wraparound: true}).attributes,
|
||||
root.at(4).attributes,
|
||||
"should return last record as predecessor to first record" +
|
||||
" if wraparound is set");
|
||||
deepEqual(root.pred(root.at(1), {wraparound: true}).attributes,
|
||||
root.at(0).attributes,
|
||||
"wraparound should have no effect if not pred(first_record)");
|
||||
});
|
||||
test('predecessor', function () {
|
||||
var root = new openerp.web.list.Collection();
|
||||
root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]);
|
||||
root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]);
|
||||
root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]);
|
||||
|
||||
deepEqual(root.pred(root.get(4)).attributes,
|
||||
root.get(3).attributes,
|
||||
"should get predecessor");
|
||||
equal(root.pred(root.get(3)),
|
||||
null,
|
||||
"predecessor do not cross collections");
|
||||
deepEqual(root.pred(root.get(3), {wraparound: true}).attributes,
|
||||
root.get(4).attributes,
|
||||
"should wraparound within a collection");
|
||||
});
|
||||
|
||||
module('list-hofs', {
|
||||
setup: function () {
|
||||
|
@ -338,4 +414,33 @@ $(document).ready(function () {
|
|||
ids, [1, 2, 3, 10, 20, 30],
|
||||
'tree collections should be deeply iterated');
|
||||
});
|
||||
|
||||
module("list-weirds", {
|
||||
setup: function () {
|
||||
openerp = window.openerp.init([]);
|
||||
window.openerp.web.corelib(openerp);
|
||||
window.openerp.web.coresetup(openerp);
|
||||
window.openerp.web.chrome(openerp);
|
||||
// views loader stuff
|
||||
window.openerp.web.data(openerp);
|
||||
window.openerp.web.views(openerp);
|
||||
window.openerp.web.list(openerp);
|
||||
}
|
||||
});
|
||||
test('set-from-noid', function () {
|
||||
var root = new openerp.web.list.Collection();
|
||||
root.add({v: 3});
|
||||
root.at(0).set('id', 42);
|
||||
var record = root.get(42);
|
||||
equal(root.length, 1);
|
||||
equal(record.get('v'), 3, "should have fetched the original record");
|
||||
});
|
||||
test('set-from-previd', function () {
|
||||
var root = new openerp.web.list.Collection();
|
||||
root.add({id: 1, v: 2});
|
||||
root.get(1).set('id', 42);
|
||||
var record = root.get(42);
|
||||
equal(root.length, 1);
|
||||
equal(record.get('v'), 2, "should have fetched the original record");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,21 +9,6 @@ $(document).ready(function () {
|
|||
openerp.web.Foo2 = {};
|
||||
}
|
||||
});
|
||||
test('key fetch', function () {
|
||||
var reg = new openerp.web.Registry({
|
||||
foo: 'openerp.web.Foo',
|
||||
bar: 'openerp.web.Bar',
|
||||
quux: 'openerp.web.Quux'
|
||||
});
|
||||
|
||||
strictEqual(reg.get_object('foo'), openerp.web.Foo);
|
||||
raises(function () { reg.get_object('qux'); },
|
||||
openerp.web.KeyNotFound,
|
||||
"Unknown keys should raise KeyNotFound");
|
||||
raises(function () { reg.get_object('quux'); },
|
||||
openerp.web.ObjectNotFound,
|
||||
"Incorrect file paths should raise ObjectNotFound");
|
||||
});
|
||||
test('key set', function () {
|
||||
var reg = new openerp.web.Registry();
|
||||
|
||||
|
|
|
@ -1,37 +1,8 @@
|
|||
$(document).ready(function () {
|
||||
var xhr = QWeb2.Engine.prototype.get_xhr();
|
||||
xhr.open('GET', '/web/static/src/xml/base.xml', false);
|
||||
xhr.send(null);
|
||||
var doc = xhr.responseXML;
|
||||
|
||||
var noop = function () {};
|
||||
/**
|
||||
* Make connection RPC responses mockable by setting keys on the
|
||||
* Connection#responses object (key is the URL, value is the function to
|
||||
* call with the RPC request payload)
|
||||
*
|
||||
* @param {openerp.web.Connection} connection connection instance to mockify
|
||||
* @param {Object} [responses] url:function mapping to seed the mock connection
|
||||
*/
|
||||
var mockifyRPC = function (connection, responses) {
|
||||
connection.responses = responses || {};
|
||||
connection.rpc_function = function (url, payload) {
|
||||
if (!(url.url in this.responses)) {
|
||||
return $.Deferred().reject({}, 'failed', _.str.sprintf("Url %s not found in mock responses", url.url)).promise();
|
||||
}
|
||||
return $.when(this.responses[url.url](payload));
|
||||
};
|
||||
};
|
||||
|
||||
var instance;
|
||||
module('query', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
}
|
||||
});
|
||||
test('Adding a facet to the query creates a facet and a value', function () {
|
||||
|
@ -167,16 +138,11 @@ $(document).ready(function () {
|
|||
|
||||
module('defaults', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -404,18 +370,11 @@ $(document).ready(function () {
|
|||
|
||||
module('completions', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
// date complete
|
||||
window.openerp.web.formats(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
asyncTest('calling', 4, function () {
|
||||
|
@ -432,10 +391,7 @@ $(document).ready(function () {
|
|||
}
|
||||
});
|
||||
view.appendTo($('#qunit-fixture'))
|
||||
.always(start)
|
||||
.fail(function (error) { ok(false, error.message); })
|
||||
.done(function () {
|
||||
stop();
|
||||
view.complete_global_search({term: "dum"}, function (completions) {
|
||||
start();
|
||||
equal(completions.length, 1, "should have a single completion");
|
||||
|
@ -454,7 +410,11 @@ $(document).ready(function () {
|
|||
var completion = {
|
||||
label: "Dummy",
|
||||
facet: {
|
||||
field: {get_domain: noop, get_context: noop, get_groupby: noop},
|
||||
field: {
|
||||
get_domain: openerp.testing.noop,
|
||||
get_context: openerp.testing.noop,
|
||||
get_groupby: openerp.testing.noop
|
||||
},
|
||||
category: 'Dummy',
|
||||
values: [{label: 'dummy', value: 42}]
|
||||
}
|
||||
|
@ -476,7 +436,11 @@ $(document).ready(function () {
|
|||
});
|
||||
});
|
||||
asyncTest('facet selection: new value existing facet', 3, function () {
|
||||
var field = {get_domain: noop, get_context: noop, get_groupby: noop};
|
||||
var field = {
|
||||
get_domain: openerp.testing.noop,
|
||||
get_context: openerp.testing.noop,
|
||||
get_groupby: openerp.testing.noop
|
||||
};
|
||||
var completion = {
|
||||
label: "Dummy",
|
||||
facet: {
|
||||
|
@ -663,16 +627,11 @@ $(document).ready(function () {
|
|||
|
||||
module('search-serialization', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
asyncTest('No facet, no call', 6, function () {
|
||||
|
@ -940,16 +899,11 @@ $(document).ready(function () {
|
|||
|
||||
module('removal', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
asyncTest('clear button', function () {
|
||||
|
@ -975,16 +929,11 @@ $(document).ready(function () {
|
|||
|
||||
module('drawer', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
asyncTest('is-drawn', 2, function () {
|
||||
|
@ -1003,16 +952,11 @@ $(document).ready(function () {
|
|||
|
||||
module('filters', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection, {
|
||||
openerp.testing.mockifyRPC(instance, {
|
||||
'/web/searchview/load': function () {
|
||||
// view with a single group of filters
|
||||
return {result: {fields_view: {
|
||||
|
@ -1117,17 +1061,11 @@ $(document).ready(function () {
|
|||
|
||||
module('saved_filters', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.formats(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
asyncTest('checkboxing', 6, function () {
|
||||
|
@ -1183,17 +1121,11 @@ $(document).ready(function () {
|
|||
|
||||
module('advanced', {
|
||||
setup: function () {
|
||||
instance = window.openerp.init([]);
|
||||
window.openerp.web.corelib(instance);
|
||||
window.openerp.web.coresetup(instance);
|
||||
window.openerp.web.chrome(instance);
|
||||
window.openerp.web.data(instance);
|
||||
window.openerp.web.formats(instance);
|
||||
window.openerp.web.search(instance);
|
||||
instance = openerp.testing.instanceFor('search');
|
||||
|
||||
instance.web.qweb.add_template(doc);
|
||||
openerp.testing.loadTemplate(instance);
|
||||
|
||||
mockifyRPC(instance.connection);
|
||||
openerp.testing.mockifyRPC(instance);
|
||||
}
|
||||
});
|
||||
asyncTest('single-advanced', 6, function () {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<script src="/web/static/lib/backbone/backbone.js" type="text/javascript"></script>
|
||||
|
||||
<!-- jquery -->
|
||||
<script src="/web/static/lib/jquery/jquery-1.7.2b1.js"></script>
|
||||
<script src="/web/static/lib/jquery/jquery-1.7.2.js"></script>
|
||||
<script src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js"></script>
|
||||
<script src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
|
||||
|
||||
|
@ -38,6 +38,12 @@
|
|||
<script src="/web/static/src/js/search.js"></script>
|
||||
<script src="/web/static/src/js/view_form.js"></script>
|
||||
<script src="/web/static/src/js/view_list.js"></script>
|
||||
<script src="/web/static/src/js/view_list_editable.js"></script>
|
||||
|
||||
<script src="/web/static/test/testing.js"></script>
|
||||
<script type="text/javascript">
|
||||
QUnit.config.testTimeout = 500;
|
||||
</script>
|
||||
</head>
|
||||
<body id="oe" class="openerp">
|
||||
<h1 id="qunit-header">OpenERP web Test Suite</h1>
|
||||
|
@ -55,4 +61,5 @@
|
|||
<script type="text/javascript" src="/web/static/test/rpc.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/evals.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/search.js"></script>
|
||||
<script type="text/javascript" src="/web/static/test/list-editable.js"></script>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
// Test support structures and methods for OpenERP
|
||||
openerp.testing = (function () {
|
||||
var xhr = QWeb2.Engine.prototype.get_xhr();
|
||||
xhr.open('GET', '/web/static/src/xml/base.xml', false);
|
||||
xhr.send(null);
|
||||
var doc = xhr.responseXML;
|
||||
|
||||
var dependencies = {
|
||||
corelib: [],
|
||||
coresetup: ['corelib'],
|
||||
data: ['corelib', 'coresetup'],
|
||||
dates: [],
|
||||
formats: ['coresetup', 'dates'],
|
||||
chrome: ['corelib', 'coresetup'],
|
||||
views: ['corelib', 'coresetup', 'data', 'chrome'],
|
||||
search: ['data', 'coresetup', 'formats'],
|
||||
list: ['views', 'data'],
|
||||
form: ['data', 'views', 'list', 'formats'],
|
||||
list_editable: ['list', 'form', 'data'],
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Function which does not do anything
|
||||
*/
|
||||
noop: function () { },
|
||||
/**
|
||||
* Loads 'base.xml' template file into qweb for the provided instance
|
||||
*
|
||||
* @param instance openerp instance being initialized, to load the template file in
|
||||
*/
|
||||
loadTemplate: function (instance) {
|
||||
instance.web.qweb.add_template(doc);
|
||||
},
|
||||
/**
|
||||
* Alter provided instance's ``connection`` attribute to make response
|
||||
* mockable:
|
||||
*
|
||||
* * The ``responses`` parameter can be used to provide a map of (RPC)
|
||||
* paths (e.g. ``/web/view/load``) to a function returning a response
|
||||
* to the query.
|
||||
* * ``instance,connection`` grows a ``responses`` attribute which is
|
||||
* a map of the same (and is in fact initialized to the ``responses``
|
||||
* parameter if one is provided)
|
||||
*
|
||||
* Note that RPC requests to un-mocked URLs will be rejected with an
|
||||
* error message: only explicitly specified urls will get a response.
|
||||
*
|
||||
* Mocked connections will *never* perform an actual RPC connection.
|
||||
*
|
||||
* @param instance openerp instance being initialized
|
||||
* @param {Object} [responses]
|
||||
*/
|
||||
mockifyRPC: function (instance, responses) {
|
||||
var connection = instance.connection;
|
||||
connection.responses = responses || {};
|
||||
connection.rpc_function = function (url, payload) {
|
||||
var fn = this.responses[url.url + ':' + payload.params.method]
|
||||
|| this.responses[url.url];
|
||||
|
||||
if (!fn) {
|
||||
return $.Deferred().reject({}, 'failed',
|
||||
_.str.sprintf("Url %s not found in mock responses, with arguments %s",
|
||||
url.url, JSON.stringify(payload.params))
|
||||
).promise();
|
||||
}
|
||||
return $.when(fn(payload));
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Creates an openerp web instance loading the specified module after
|
||||
* all of its dependencies.
|
||||
*
|
||||
* @param {String} module
|
||||
* @returns OpenERP Web instance
|
||||
*/
|
||||
instanceFor: function (module) {
|
||||
var instance = openerp.init([]);
|
||||
this._load(instance, module);
|
||||
return instance;
|
||||
},
|
||||
_load: function (instance, module, loaded) {
|
||||
if (!loaded) { loaded = []; }
|
||||
|
||||
var deps = dependencies[module];
|
||||
if (!deps) { throw new Error("Unknown dependencies for " + module); }
|
||||
|
||||
var to_load = _.difference(deps, loaded);
|
||||
while (!_.isEmpty(to_load)) {
|
||||
this._load(instance, to_load[0], loaded);
|
||||
to_load = _.difference(deps, loaded);
|
||||
}
|
||||
openerp.web[module](instance);
|
||||
loaded.push(module);
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,49 @@
|
|||
Notes on the usage of the Form View as a sub-widget
|
||||
===================================================
|
||||
|
||||
Undocumented stuff
|
||||
------------------
|
||||
|
||||
* ``initial_mode`` *option* defines the starting mode of the form
|
||||
view, one of ``view`` and ``edit`` (?). Default value is ``view``
|
||||
(non-editable form).
|
||||
|
||||
* ``embedded_view`` *attribute* has to be set separately when
|
||||
providing a view directly, no option available for that usage.
|
||||
|
||||
* View arch **must** contain node with
|
||||
``@class="oe_form_container"``, otherwise everything will break
|
||||
without any info
|
||||
|
||||
* Root element of view arch not being ``form`` may or may not work
|
||||
correctly, no idea.
|
||||
|
||||
* Freeform views => ``@version="7.0"``
|
||||
|
||||
* Form is not entirely loaded (some widgets may not appear) unless
|
||||
``on_record_loaded`` is called (or ``do_show``, which itself calls
|
||||
``on_record_loaded``).
|
||||
|
||||
* "Empty" form => ``on_button_new`` (...), or manually call
|
||||
``default_get`` + ``on_record_loaded``
|
||||
|
||||
* Form fields default to width: 100%, padding, !important margin, can
|
||||
be reached via ``.oe_form_field``
|
||||
|
||||
* Form *will* render buttons and a pager, offers options to locate
|
||||
both outside of form itself (``$buttons`` and ``$pager``), providing
|
||||
empty jquery objects (``$()``) seems to stop displaying both but not
|
||||
sure if there are deleterious side-effects.
|
||||
|
||||
Other options:
|
||||
|
||||
* Pass in ``$(document.createDocumentFragment)`` to ensure it's a
|
||||
DOM-compatible tree completely outside of the actual DOM.
|
||||
|
||||
* ???
|
||||
|
||||
* readonly fields probably don't have a background, beware if need of
|
||||
overlay
|
||||
|
||||
* What is the difference between ``readonly`` and
|
||||
``effective_readonly``?
|
|
@ -18,6 +18,9 @@ Contents:
|
|||
|
||||
search-view
|
||||
|
||||
list-view
|
||||
form-notes
|
||||
|
||||
Older stuff
|
||||
-----------
|
||||
|
||||
|
|
|
@ -0,0 +1,455 @@
|
|||
List View
|
||||
=========
|
||||
|
||||
Style Hooks
|
||||
-----------
|
||||
|
||||
The list view provides a few style hook classes for re-styling of list views in
|
||||
various situations:
|
||||
|
||||
``.oe_list``
|
||||
|
||||
The root element of the list view, styling rules should be rooted
|
||||
on that class.
|
||||
|
||||
``table.oe_list_content``
|
||||
|
||||
The root table for the listview, accessory components may be
|
||||
generated or added outside this section, this is the list view
|
||||
"proper".
|
||||
|
||||
``.oe_list_buttons``
|
||||
|
||||
The action buttons array for the list view, with its sub-elements
|
||||
|
||||
``.oe_list_add``
|
||||
|
||||
The default "Create"/"Add" button of the list view
|
||||
|
||||
``.oe_alternative``
|
||||
|
||||
The "alternative choice" for the list view, by default text
|
||||
along the lines of "or import" with a link.
|
||||
|
||||
``.oe_list_field_cell``
|
||||
|
||||
The cell (``td``) for a given field of the list view, cells which
|
||||
are *not* fields (e.g. name of a group, or number of items in a
|
||||
group) will not have this class. The field cell can be further
|
||||
specified:
|
||||
|
||||
``.oe_number``
|
||||
|
||||
Numeric cell types (integer and float)
|
||||
|
||||
``.oe_button``
|
||||
|
||||
Action button (``button`` tag in the view) inside the cell
|
||||
|
||||
``.oe_readonly``
|
||||
|
||||
Readonly field cell
|
||||
|
||||
``.oe_list_field_$type``
|
||||
|
||||
Additional class for the precise type of the cell, ``$type``
|
||||
is the field's @widget if there is one, otherwise it's the
|
||||
field's type.
|
||||
|
||||
``.oe_list_record_selector``
|
||||
|
||||
Selector cells
|
||||
|
||||
Editable list view
|
||||
++++++++++++++++++
|
||||
|
||||
The editable list view module adds a few supplementary style hook
|
||||
classes, for edition situations:
|
||||
|
||||
``.oe_editing``
|
||||
|
||||
Added to both ``.oe_list`` and ``.oe_list_button`` (as the
|
||||
buttons may be outside of the list view) when a row of the list is
|
||||
currently being edited.
|
||||
|
||||
``tr.oe_edition``
|
||||
|
||||
Class set on the row being edited itself. Note that the edition
|
||||
form is *not* contained within the row, this allows for styling or
|
||||
modifying the row while it's being edited separately. Mostly for
|
||||
fields which can not be edited (e.g. read-only fields).
|
||||
|
||||
|
||||
Editable list view
|
||||
------------------
|
||||
|
||||
List view edition is an extension to the base listview providing the
|
||||
capability of inline record edition by delegating to an embedded form
|
||||
view.
|
||||
|
||||
.. todo::
|
||||
|
||||
cleanup options and settings for editability configuration. Right
|
||||
now there are:
|
||||
|
||||
``defaults.editable``
|
||||
|
||||
``null``, ``"top"`` or ``"bottom"``, generally broken and
|
||||
useless
|
||||
|
||||
``context.set_editable``
|
||||
|
||||
forces ``options.editable`` to ``"bottom"``
|
||||
|
||||
``view.arch.attrs.editable``
|
||||
|
||||
same as ``defaults.editable``, but applied separately (after
|
||||
reloading the view), if absent delegates to
|
||||
``options.editable`` which may have been set previously.
|
||||
|
||||
``options.read_only``
|
||||
|
||||
force options.editable to false, or something?
|
||||
|
||||
.. note:: can probably be replaced by cancelling ``edit:before``
|
||||
|
||||
and :js:func:`~openerp.web.ListView.set_editable` which
|
||||
ultimately behaves weird-as-fuck-ly.
|
||||
|
||||
The editable list view module adds a number of methods to the list
|
||||
view, on top of implementing the :js:class:`EditorDelegate` protocol:
|
||||
|
||||
Interaction Methods
|
||||
+++++++++++++++++++
|
||||
|
||||
.. js:function:: openerp.web.ListView.ensure_saved
|
||||
|
||||
Attempts to resolve the pending edition, if any, by saving the
|
||||
edited row's current state.
|
||||
|
||||
:returns: delegate resolving to all editions having been saved, or
|
||||
rejected if a pending edition could not be saved
|
||||
(e.g. validation failure)
|
||||
|
||||
.. js:function:: openerp.web.ListView.start_edition([record][, options])
|
||||
|
||||
Starts editing the provided record inline, through an overlay form
|
||||
view of editable fields in the record.
|
||||
|
||||
If no record is provided, creates a new one according to the
|
||||
editability configuration of the list view.
|
||||
|
||||
This method resolves any pending edition when invoked, before
|
||||
starting a new edition.
|
||||
|
||||
:param record: record to edit, or null to create a new record
|
||||
:type record: :js:class:`~openerp.web.list.Record`
|
||||
:param EditOptions options:
|
||||
:returns: delegate to the form used for the edition
|
||||
|
||||
.. js:function:: openerp.web.ListView.save_edition
|
||||
|
||||
Resolves the pending edition.
|
||||
|
||||
:returns: delegate to the save being completed, resolves to an
|
||||
object with two attributes ``created`` (flag indicating
|
||||
whether the saved record was just created or was
|
||||
updated) and ``record`` the reloaded record having been
|
||||
edited.
|
||||
|
||||
.. js:function:: openerp.web.ListView.cancel_edition
|
||||
|
||||
Cancels pending edition, cleans up the list view in case of
|
||||
creation (removes the empty record being created).
|
||||
|
||||
Utility Methods
|
||||
+++++++++++++++
|
||||
|
||||
.. js:function:: openerp.web.ListView.get_cells_for(row)
|
||||
|
||||
Extracts the cells from a listview row, and puts them in a
|
||||
{fieldname: cell} mapping for analysis and manipulation.
|
||||
|
||||
:param jQuery row:
|
||||
:rtype: Object
|
||||
|
||||
.. js:function:: openerp.web.ListView.with_event(event_name, event, action[, args][, trigger_params])
|
||||
|
||||
Executes ``action`` in the context of the view's editor,
|
||||
bracketing it with cancellable event signals.
|
||||
|
||||
:param String event_name: base name for the bracketing event, will
|
||||
be postfixed by ``:before`` and
|
||||
``:after`` before being called
|
||||
(respectively before and after
|
||||
``action`` is executed)
|
||||
:param Object event: object passed to the ``:before`` event
|
||||
handlers.
|
||||
:param Function action: function called with the view's editor as
|
||||
its ``this``. May return a deferred.
|
||||
:param Array args: arguments passed to ``action``
|
||||
:param Array trigger_params: arguments passed to the ``:after``
|
||||
event handler alongside the results
|
||||
of ``action``
|
||||
|
||||
Behavioral Customizations
|
||||
+++++++++++++++++++++++++
|
||||
|
||||
.. js:function:: openerp.web.ListView.handle_onwrite(record)
|
||||
|
||||
Implements the handling of the ``onwrite`` listview attribute:
|
||||
calls the RPC methods specified by ``@onwrite``, and if that
|
||||
method returns an array of ids loads or reloads the records
|
||||
corresponding to those ids.
|
||||
|
||||
:param record: record being written having triggered the
|
||||
``onwrite`` callback
|
||||
:type record: openerp.web.list.Record
|
||||
:returns: deferred to all reloadings being done
|
||||
|
||||
Events
|
||||
++++++
|
||||
|
||||
For simpler interactions by/with external users of the listview, the
|
||||
view provides a number of dedicated events to its lifecycle.
|
||||
|
||||
.. note:: if an event is defined as *cancellable*, it means its first
|
||||
parameter is an object on which the ``cancel`` attribute can
|
||||
be set. If the ``cancel`` attribute is set, the view will
|
||||
abort its current behavior as soon as possible, and rollback
|
||||
any state modification.
|
||||
|
||||
``edit:before`` *cancellable*
|
||||
|
||||
Invoked before the list view starts editing a record.
|
||||
|
||||
Provided with an event object with a single property ``record``,
|
||||
holding the attributes of the record being edited (``record`` is
|
||||
empty *but not null* for a new record)
|
||||
|
||||
``edit:after``
|
||||
|
||||
Invoked after the list view has gone into an edition state,
|
||||
provided with the attributes of the record being edited (see
|
||||
``edit:before``) as first parameter and the form used for the
|
||||
edition as second parameter.
|
||||
|
||||
``save:before`` *cancellable*
|
||||
|
||||
Invoked right before saving a pending edition, provided with an
|
||||
event object holding the listview's editor (``editor``) and the
|
||||
edition form (``form``)
|
||||
|
||||
``save:after``
|
||||
|
||||
Invoked after a save has been completed
|
||||
|
||||
``cancel:before`` *cancellable*
|
||||
|
||||
Invoked before cancelling a pending edition, provided with the
|
||||
same information as ``save:before``.
|
||||
|
||||
``cancel:after``
|
||||
|
||||
Invoked after a pending edition has been cancelled.
|
||||
|
||||
DOM events
|
||||
++++++++++
|
||||
|
||||
The list view has grown hooks for the ``keyup`` event on its edition
|
||||
form (during edition): any such event bubbling out of the edition form
|
||||
will be forwarded to a method ``keyup_EVENTNAME``, where ``EVENTNAME``
|
||||
is the name of the key in ``$.ui.keyCode``.
|
||||
|
||||
The method will also get the event object (originally passed to the
|
||||
``keyup`` handler) as its sole parameter.
|
||||
|
||||
The base editable list view has handlers for the ``ENTER`` and
|
||||
``ESCAPE`` keys.
|
||||
|
||||
Editor
|
||||
------
|
||||
|
||||
The list-edition modules does not generally interact with the embedded
|
||||
formview, delegating instead to its
|
||||
:js:class:`~openerp.web.list.Editor`.
|
||||
|
||||
.. js:class:: openerp.web.list.Editor(parent[, options])
|
||||
|
||||
The editor object provides a more convenient interface to form
|
||||
views, and simplifies the usage of form views for semi-arbitrary
|
||||
edition of stuff.
|
||||
|
||||
However, the editor does *not* task itself with being internally
|
||||
consistent at this point: calling
|
||||
e.g. :js:func:`~openerp.web.list.Editor.edit` multiple times in a
|
||||
row without saving or cancelling each edit is undefined.
|
||||
|
||||
:param parent:
|
||||
:type parent: :js:class:`~openerp.web.Widget`
|
||||
:param EditorOptions options:
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.is_editing([record_state])
|
||||
|
||||
Indicates whether the editor is currently in the process of
|
||||
providing edition for a record.
|
||||
|
||||
Can be filtered by the state of the record being edited
|
||||
(whether it's a record being *created* or a record being
|
||||
*altered*), in which case it asserts both that an edition is
|
||||
underway and that the record being edited respectively does
|
||||
not yet exist in the database or already exists there.
|
||||
|
||||
:param record_state: state of the record being edited.
|
||||
Either ``"new"`` or ``"edit"``.
|
||||
:type record_state: String
|
||||
:rtype: Boolean
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.edit(record, configureField[, options])
|
||||
|
||||
Loads the provided record into the internal form view and
|
||||
displays the form view.
|
||||
|
||||
Will also attempt to focus the first visible field of the form
|
||||
view.
|
||||
|
||||
:param Object record: record to load into the form view
|
||||
(key:value mapping similar to the result
|
||||
of a ``read``)
|
||||
:param configureField: function called with each field of the
|
||||
form view right after the form is
|
||||
displayed, lets whoever called this
|
||||
method do some last-minute
|
||||
configuration of form fields.
|
||||
:type configureField: Function<String, openerp.web.form.Field>
|
||||
:param EditOptions options:
|
||||
:returns: jQuery delegate to the form object
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.save
|
||||
|
||||
Attempts to save the internal form, then hide it
|
||||
|
||||
:returns: delegate to the record under edition (with ``id``
|
||||
added for a creation). The record is not updated
|
||||
from when it was passed in, aside from the ``id``
|
||||
attribute.
|
||||
|
||||
.. js:function:: openerp.web.list.Editor.cancel
|
||||
|
||||
Attemps to cancel the edition of the internal form, then hide
|
||||
the form
|
||||
|
||||
:returns: delegate to the record under edition
|
||||
|
||||
.. js:class:: EditorOptions
|
||||
|
||||
.. js:attribute:: EditorOptions.formView
|
||||
|
||||
Form view (sub)-class to instantiate and delegate edition to.
|
||||
|
||||
By default, :js:class:`~openerp.web.FormView`
|
||||
|
||||
.. js:attribute:: EditorOptions.delegate
|
||||
|
||||
Object used to get various bits of information about how to
|
||||
display stuff.
|
||||
|
||||
By default, uses the editor's parent widget. See
|
||||
:js:class:`EditorDelegate` for the methods and attributes to
|
||||
provide.
|
||||
|
||||
.. js:class:: EditorDelegate
|
||||
|
||||
Informal protocol defining the methods and attributes expected of
|
||||
the :js:class:`~openerp.web.list.Editor`'s delegate.
|
||||
|
||||
.. js:attribute:: EditorDelegate.dataset
|
||||
|
||||
The dataset passed to the form view to synchronize the form
|
||||
view and the outer widget.
|
||||
|
||||
.. js:function:: EditorDelegate.edition_view(editor)
|
||||
|
||||
Called by the :js:class:`~openerp.web.list.Editor` object to
|
||||
get a form view (JSON) to pass along to the form view it
|
||||
created.
|
||||
|
||||
The result should be a valid form view, see :doc:`Form Notes
|
||||
<form-notes>` for various peculiarities of the form view
|
||||
format.
|
||||
|
||||
:param editor: editor object asking for the view
|
||||
:type editor: :js:class:`~openerp.web.list.Editor`
|
||||
:returns: form view
|
||||
:rtype: Object
|
||||
|
||||
.. js:function:: EditorDelegate.prepends_on_create
|
||||
|
||||
By default, the :js:class:`~openerp.web.list.Editor` will
|
||||
append the ids of newly created records to the
|
||||
:js:attr:`EditorDelegate.dataset`. If this method returns
|
||||
``true``, it will prepend these ids instead.
|
||||
|
||||
:returns: whether new records should be prepended to the
|
||||
dataset (instead of appended)
|
||||
:rtype: Boolean
|
||||
|
||||
|
||||
.. js:class:: EditOptions
|
||||
|
||||
Options object optionally passed into a method starting an edition
|
||||
to configure its setup and behavior
|
||||
|
||||
.. js:attribute:: focus_field
|
||||
|
||||
Name of the field to set focus on after setting up the edition
|
||||
of the record.
|
||||
|
||||
If this option is not provided, or the requested field can not
|
||||
be focused (invisible, readonly or not in the view), the first
|
||||
visible non-readonly field is focused.
|
||||
|
||||
Changes from 6.1
|
||||
----------------
|
||||
|
||||
* The editable listview behavior has been rewritten pretty much from
|
||||
scratch, any code touching on editability will have to be modified
|
||||
|
||||
* The overloading of :js:class:`~openerp.web.ListView.Groups` and
|
||||
:js:class:`~openerp.web.ListView.List` for editability has been
|
||||
drastically simplified, and most of the behavior has been moved to
|
||||
the list view itself. Only
|
||||
:js:func:`~openerp.web.ListView.List.row_clicked` is still
|
||||
overridden.
|
||||
|
||||
* A new method ``get_row_for(record) -> jQuery(tr) | null`` has been
|
||||
added to both ListView.List and ListView.Group, it can be called
|
||||
from the list view to get the table row matching a record (if such
|
||||
a row exists).
|
||||
|
||||
* :js:func:`~openerp.web.ListView.do_button_action`'s core behavior
|
||||
has been split away to
|
||||
:js:func:`~openerp.web.ListView.handle_button`. This allows bypassing
|
||||
overrides of :js:func:`~openerp.web.ListView.do_button_action` in a
|
||||
parent class.
|
||||
|
||||
Ideally, :js:func:`~openerp.web.ListView.handle_button` should not be
|
||||
overridden.
|
||||
|
||||
* Modifiers handling has been improved (all modifiers information
|
||||
should now be available through :js:func:`~Column.modifiers_for`,
|
||||
not just ``invisible``)
|
||||
|
||||
* Changed some handling of the list view's record: a record may now
|
||||
have no id, and the listview will handle that correctly (for new
|
||||
records being created) as well as correctly handle the ``id`` being
|
||||
set.
|
||||
|
||||
* Extended the internal collections structure of the list view with
|
||||
`#find`_, `#succ`_ and `#pred`_.
|
||||
|
||||
.. _#find: http://underscorejs.org/#find
|
||||
|
||||
.. _#succ: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:succ
|
||||
|
||||
.. _#pred: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:pred
|
Loading…
Reference in New Issue