[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:
Sébastien Theys
2018-07-17 11:50:35 +02:00
parent d1f1cc9c27
commit f241441433
10 changed files with 257 additions and 15 deletions
@@ -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
+50
View File
@@ -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
+19
View File
@@ -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>
+1
View File
@@ -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"/>
+2 -2
View File
@@ -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/*')
)