[IMP] web: add control + create tags to customize "add a line"
The default "add a line" button creates a new line with the default values from the model. Sometimes we want to create a new line with different default values (or more generally a customized context), which is possible with this commit. Moreover it is possible to create multiple buttons for multiple default values/contexts.
This works both for in-line creation ("editable=") and dialog creation.
The new tags are:
<control> defines custom controls for the current view.
Does not support any attribute, but can have children:
<create> adds a button to create a new element on the current list.
This makes sense when the parent tree view is inside a One2many field.
If any create is defined, it will overwrite the default "add a line" button.
For more information and example, have a look at the Views documentation.
Technically, the commit will:
- parse the new XML tags (+ add appropriate validation)
- display the buttons based on the XML
- on button click/keyboard: pull up the additional context information from the button event up to the default_get
- add unit tests
- add documentation
PR #25209
This commit is contained in:
@@ -1266,11 +1266,18 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.context] We allow additional context, this is
|
||||
* used for example to define default values when adding new lines to
|
||||
* a one2many with control/create tags.
|
||||
*/
|
||||
_openFormDialog: function (params) {
|
||||
var context = this.record.getContext(_.extend({},
|
||||
this.recordParams,
|
||||
{ additionalContext: params.context }
|
||||
));
|
||||
this.trigger_up('open_one2many_record', _.extend(params, {
|
||||
domain: this.record.getDomain(this.recordParams),
|
||||
context: this.record.getContext(this.recordParams),
|
||||
context: context,
|
||||
field: this.field,
|
||||
fields_view: this.attrs.views && this.attrs.views.form,
|
||||
parentID: this.value.id,
|
||||
@@ -1297,6 +1304,7 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
|
||||
// we don't want interference with the components upstream.
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.editable) {
|
||||
if (!this.activeActions.create) {
|
||||
if (data.onFail) {
|
||||
@@ -1308,12 +1316,14 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
this._setValue({
|
||||
operation: 'CREATE',
|
||||
position: this.editable,
|
||||
context: data.context,
|
||||
}).always(function () {
|
||||
self.creatingRecord = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._openFormDialog({
|
||||
context: data.context,
|
||||
on_saved: function (record) {
|
||||
self._setValue({ operation: 'ADD', id: record.id });
|
||||
},
|
||||
|
||||
@@ -1315,13 +1315,17 @@ var BasicModel = AbstractModel.extend({
|
||||
* @param {Object} [options]
|
||||
* @param {string} [options.position=top] if the new record should be added
|
||||
* on top or on bottom of the list
|
||||
* @param {Object} [options.context] additional context to be merged before
|
||||
* calling the default_get (eg. to set default values)
|
||||
* @returns {Deferred<string>} resolves to the new record id
|
||||
*/
|
||||
_addX2ManyDefaultRecord: function (list, options) {
|
||||
var self = this;
|
||||
var position = options && options.position || 'top';
|
||||
var additionalContext = options && options.context;
|
||||
var context = this._getContext(list, {additionalContext: additionalContext});
|
||||
var params = {
|
||||
context: this._getContext(list),
|
||||
context: context,
|
||||
fields: list.fields,
|
||||
fieldsInfo: list.fieldsInfo,
|
||||
parentID: list.id,
|
||||
@@ -1799,7 +1803,10 @@ var BasicModel = AbstractModel.extend({
|
||||
}
|
||||
break;
|
||||
case 'CREATE':
|
||||
var options = {position: command.position};
|
||||
var options = {
|
||||
context: command.context,
|
||||
position: command.position,
|
||||
};
|
||||
def = this._addX2ManyDefaultRecord(list, options).then(function (id) {
|
||||
if (command.position === 'bottom' && list.orderedResIDs && list.orderedResIDs.length >= list.limit) {
|
||||
list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1;
|
||||
|
||||
@@ -46,7 +46,7 @@ var FieldManagerMixin = {
|
||||
*/
|
||||
_applyChanges: function (dataPointID, changes, event) {
|
||||
var self = this;
|
||||
var options = _.pick(event.data, 'viewType', 'doNotSetDirty', 'notifyChange');
|
||||
var options = _.pick(event.data, 'context', 'doNotSetDirty', 'notifyChange', 'viewType');
|
||||
return this.model.notifyChanges(dataPointID, changes, options)
|
||||
.then(function (result) {
|
||||
if (event.data.force_save) {
|
||||
|
||||
@@ -15,6 +15,7 @@ var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
var utils = require('web.utils');
|
||||
var Context = require('web.Context');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -37,12 +38,49 @@ ListRenderer.include({
|
||||
* @param {boolean} params.addTrashIcon
|
||||
*/
|
||||
init: function (parent, state, params) {
|
||||
var self = this
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// if addCreateLine is true, the renderer will add a 'Add a line' link
|
||||
// at the bottom of the list view
|
||||
this.addCreateLine = params.addCreateLine;
|
||||
|
||||
// Controls allow overriding "add a line" by custom controls.
|
||||
|
||||
// Each <control> (only one is actually needed) is a container for (multiple) <create>.
|
||||
// Each <create> will be a "add a line" button with custom text and context.
|
||||
|
||||
// The following code will browse the arch to find
|
||||
// all the <create> that are inside <control>
|
||||
|
||||
if (this.addCreateLine) {
|
||||
this.creates = [];
|
||||
|
||||
_.each(this.arch.children, function (child) {
|
||||
if (child.tag !== 'control') {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(child.children, function (child) {
|
||||
if (child.tag !== 'create') {
|
||||
return;
|
||||
}
|
||||
|
||||
self.creates.push({
|
||||
'context': child.attrs.context,
|
||||
'string': child.attrs.string,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add the default button if we didn't find any custom button.
|
||||
if (this.creates.length === 0) {
|
||||
this.creates.push({
|
||||
string: _t("Add a line"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if addTrashIcon is true, there will be a small trash icon at the end
|
||||
// of each line, so the user can delete a record.
|
||||
this.addTrashIcon = params.addTrashIcon;
|
||||
@@ -525,15 +563,23 @@ ListRenderer.include({
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_renderRows: function () {
|
||||
var self = this;
|
||||
var $rows = this._super();
|
||||
|
||||
if (this.addCreateLine) {
|
||||
var $a = $('<a href="#" role="button">').text(_t("Add a line"));
|
||||
var $td = $('<td>')
|
||||
.attr('colspan', this._getNumberOfCols())
|
||||
.addClass('o_field_x2many_list_row_add')
|
||||
.append($a);
|
||||
.attr('colspan', self._getNumberOfCols())
|
||||
.addClass('o_field_x2many_list_row_add');
|
||||
var $tr = $('<tr>').append($td);
|
||||
$rows.push($tr);
|
||||
|
||||
_.each(self.creates, function (create) {
|
||||
var $a = $('<a href="#" role="button">')
|
||||
.attr('data-context', create.context)
|
||||
.addClass('ml16')
|
||||
.text(create.string);
|
||||
$td.append($a);
|
||||
});
|
||||
}
|
||||
return $rows;
|
||||
},
|
||||
@@ -701,20 +747,20 @@ ListRenderer.include({
|
||||
* This method is called when we click on the 'Add a line' button in a sub
|
||||
* list such as a one2many in a form view.
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddRecord: function (event) {
|
||||
_onAddRecord: function (ev) {
|
||||
// we don't want the browser to navigate to a the # url
|
||||
event.preventDefault();
|
||||
ev.preventDefault();
|
||||
|
||||
// we don't want the click to cause other effects, such as unselecting
|
||||
// the row that we are creating, because it counts as a click on a tr
|
||||
event.stopPropagation();
|
||||
ev.stopPropagation();
|
||||
|
||||
// but we do want to unselect current row
|
||||
var self = this;
|
||||
this.unselectRow().then(function () {
|
||||
self.trigger_up('add_record'); // TODO write a test, the deferred was not considered
|
||||
self.trigger_up('add_record', {context: ev.currentTarget.dataset.context}); // TODO write a test, the deferred was not considered
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -184,6 +184,9 @@ var ListRenderer = BasicRenderer.extend({
|
||||
var self = this;
|
||||
self.handleField = null;
|
||||
this.columns = _.reject(this.arch.children, function (c) {
|
||||
if (c.tag === 'control') {
|
||||
return true;
|
||||
}
|
||||
var reject = c.attrs.modifiers.column_invisible;
|
||||
// If there is an evaluated domain for the field we override the node
|
||||
// attribute to have the evaluated modifier value.
|
||||
|
||||
@@ -9797,6 +9797,112 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a line custom control editable', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form string="Partners">' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<control>' +
|
||||
'<create string="Add food" context="" />' +
|
||||
'<create string="Add pizza" context="{\'default_display_name\': \'pizza\'}"/>' +
|
||||
'</control>' +
|
||||
|
||||
'<control>' +
|
||||
'<create string="Add pasta" context="{\'default_display_name\': \'pasta\'}"/>' +
|
||||
'</control>' +
|
||||
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
// new controls correctly added
|
||||
assert.strictEqual($('.o_field_x2many_list_row_add').length, 1);
|
||||
assert.strictEqual($('.o_field_x2many_list_row_add').text(), "Add foodAdd pizzaAdd pasta");
|
||||
|
||||
// click add food
|
||||
// check it's empty
|
||||
form.$('.o_field_x2many_list_row_add a:eq(0)').click();
|
||||
assert.strictEqual($('.o_data_cell').text(), "");
|
||||
|
||||
// click add pizza
|
||||
// save the modal
|
||||
// check it's pizza
|
||||
form.$('.o_field_x2many_list_row_add a:eq(1)').click();
|
||||
// click add pasta
|
||||
form.$('.o_field_x2many_list_row_add a:eq(2)').click();
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
assert.strictEqual($('.o_data_cell').text(), "pizzapasta");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a line custom control non-editable', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form string="Partners">' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<control>' +
|
||||
'<create string="Add food" context="" />' +
|
||||
'<create string="Add pizza" context="{\'default_display_name\': \'pizza\'}" />' +
|
||||
'</control>' +
|
||||
|
||||
'<control>' +
|
||||
'<create string="Add pasta" context="{\'default_display_name\': \'pasta\'}" />' +
|
||||
'</control>' +
|
||||
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
// new controls correctly added
|
||||
assert.strictEqual($('.o_field_x2many_list_row_add').length, 1);
|
||||
assert.strictEqual($('.o_field_x2many_list_row_add').text(), "Add foodAdd pizzaAdd pasta");
|
||||
|
||||
// click add food
|
||||
// check it's empty
|
||||
form.$('.o_field_x2many_list_row_add a:eq(0)').click();
|
||||
$('.modal .modal-footer .btn-primary:first').click();
|
||||
assert.strictEqual($('.o_data_cell').text(), "");
|
||||
|
||||
// click add pizza
|
||||
// save the modal
|
||||
// check it's pizza
|
||||
form.$('.o_field_x2many_list_row_add a:eq(1)').click();
|
||||
$('.modal .modal-footer .btn-primary:first').click();
|
||||
assert.strictEqual($('.o_data_cell').text(), "pizza");
|
||||
|
||||
// click add pasta
|
||||
// save the whole record
|
||||
// check it's pizzapasta
|
||||
form.$('.o_field_x2many_list_row_add a:eq(2)').click();
|
||||
$('.modal .modal-footer .btn-primary:first').click();
|
||||
assert.strictEqual($('.o_data_cell').text(), "pizzapasta");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many form view with action button', function (assert) {
|
||||
// once the action button is clicked, the record is reloaded (via the
|
||||
// on_close handler, executed because the python method does not return
|
||||
|
||||
@@ -308,6 +308,56 @@ Possible children elements of the list view are:
|
||||
:ref:`form view <reference/views/form>` is also valid and will
|
||||
be used when setting up the inline form view
|
||||
|
||||
``control``
|
||||
defines custom controls for the current view.
|
||||
|
||||
This makes sense if the parent ``tree`` view is inside a One2many field.
|
||||
|
||||
Does not support any attribute, but can have children:
|
||||
|
||||
``create``
|
||||
adds a button to create a new element on the current list.
|
||||
|
||||
.. note:: If any ``create`` is defined, it will overwrite the default
|
||||
"add a line" button.
|
||||
|
||||
The following attributes are supported:
|
||||
|
||||
``string`` (required)
|
||||
The text displayed on the button.
|
||||
|
||||
``context``
|
||||
This context will be merged into the existing context
|
||||
when retrieving the default value of the new record.
|
||||
|
||||
For example it can be used to override default values.
|
||||
|
||||
|
||||
The following example will override the default "add a line" button
|
||||
by replacing it with 3 new buttons:
|
||||
"Add a product", "Add a section" and "Add a note".
|
||||
|
||||
"Add a product" will set the field 'display_type' to its default value.
|
||||
|
||||
The two other buttons will set the field 'display_type'
|
||||
to be respectively 'line_section' and 'line_note'.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<control>
|
||||
<create
|
||||
string="Add a product"
|
||||
/>
|
||||
<create
|
||||
string="Add a section"
|
||||
context="{'default_display_type': 'line_section'}"
|
||||
/>
|
||||
<create
|
||||
string="Add a note"
|
||||
context="{'default_display_type': 'line_note'}"
|
||||
/>
|
||||
</control>
|
||||
|
||||
.. _reference/views/form:
|
||||
|
||||
Forms
|
||||
|
||||
@@ -389,4 +389,23 @@
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="create">
|
||||
<rng:element name="create">
|
||||
<rng:ref name="overload"/>
|
||||
<rng:attribute name="string"/>
|
||||
<rng:attribute name="context"/>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="control">
|
||||
<rng:element name="control">
|
||||
<rng:ref name="overload"/>
|
||||
<rng:oneOrMore>
|
||||
<rng:choice>
|
||||
<rng:ref name="create"/>
|
||||
</rng:choice>
|
||||
</rng:oneOrMore>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
</rng:grammar>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
</rng:optional>
|
||||
<rng:zeroOrMore>
|
||||
<rng:choice>
|
||||
<rng:ref name="control"/>
|
||||
<rng:ref name="field"/>
|
||||
<rng:ref name="separator"/>
|
||||
<rng:ref name="tree"/>
|
||||
|
||||
@@ -77,9 +77,9 @@ def valid_field_in_graph(arch):
|
||||
|
||||
@validate('tree')
|
||||
def valid_field_in_tree(arch):
|
||||
""" Children of ``tree`` view must be ``field`` or ``button``."""
|
||||
""" Children of ``tree`` view must be ``field`` or ``button`` or ``control``."""
|
||||
return all(
|
||||
child.tag in ('field', 'button')
|
||||
child.tag in ('field', 'button', 'control')
|
||||
for child in arch.xpath('/tree/*')
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user