summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js53
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js32
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js58
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js25
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_view.js2
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js28
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js66
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js3
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_one2many_tests.js55
-rw-r--r--addons/web/static/tests/views/kanban_tests.js76
-rw-r--r--doc/reference/javascript_reference.rst5
-rw-r--r--doc/reference/views.rst20
12 files changed, 301 insertions, 122 deletions
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 642fa55b33e..85342269617 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -812,7 +812,7 @@ var FieldX2Many = AbstractField.extend({
open_record: '_onOpenRecord',
kanban_record_delete: '_onRemoveRecord',
list_record_remove: '_onRemoveRecord',
- resequence: '_onResequence',
+ resequence_records: '_onResequenceRecords',
save_line: '_onSaveLine',
toggle_column_order: '_onToggleColumnOrder',
activate_next_widget: '_onActiveNextWidget',
@@ -1043,6 +1043,7 @@ var FieldX2Many = AbstractField.extend({
};
_.extend(rendererParams, {
record_options: record_options,
+ readOnlyMode: this.isReadonly,
});
}
@@ -1351,38 +1352,52 @@ var FieldX2Many = AbstractField.extend({
* Forces a resequencing of the records.
*
* @private
- * @param {OdooEvent} event
+ * @param {OdooEvent} ev
+ * @param {string[]} ev.data.recordIds
+ * @param {integer} ev.data.offset
+ * @param {string} ev.data.handleField
*/
- _onResequence: function (event) {
- event.stopPropagation();
+ _onResequenceRecords: function (ev) {
+ ev.stopPropagation();
var self = this;
- this.trigger_up('edited_list', { id: this.value.id });
- var rowIDs = event.data.rowIDs.slice();
- var rowID = rowIDs.pop();
- var defs = _.map(rowIDs, function (rowID, index) {
+ if (this.view.arch.tag === 'tree') {
+ this.trigger_up('edited_list', { id: this.value.id });
+ }
+ var handleField = ev.data.handleField;
+ var offset = ev.data.offset;
+ var recordIds = ev.data.recordIds.slice();
+ // trigger an update of all records but the last one with option
+ // 'notifyChanges' set to false, and once all those changes have been
+ // validated by the model, trigger the change on the last record
+ // (without the option, s.t. the potential onchange on parent record
+ // is triggered)
+ var recordId = recordIds.pop();
+ var proms = recordIds.map(function (recordId, index) {
var data = {};
- data[event.data.handleField] = event.data.offset + index;
+ data[handleField] = offset + index;
return self._setValue({
operation: 'UPDATE',
- id: rowID,
+ id: recordId,
data: data,
}, {
notifyChange: false,
});
});
- Promise.all(defs).then(function () {
+ Promise.all(proms).then(function () {
function always() {
- self.trigger_up('toggle_column_order', {
- id: self.value.id,
- name: event.data.handleField,
- });
+ if (self.view.arch.tag === 'tree') {
+ self.trigger_up('toggle_column_order', {
+ id: self.value.id,
+ name: handleField,
+ });
+ }
}
-
- // trigger only once the onchange for parent record
+ var data = {};
+ data[handleField] = offset + recordIds.length;
self._setValue({
operation: 'UPDATE',
- id: rowID,
- data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]),
+ id: recordId,
+ data: data,
}).then(always).guardedCatch(always);
});
},
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 064f7b84a52..610e7e6dc6b 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -19,6 +19,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
discard_changes: '_onDiscardChanges',
reload: '_onReload',
+ resequence_records: '_onResequenceRecords',
set_dirty: '_onSetDirty',
sidebar_data_asked: '_onSidebarDataAsked',
translate: '_onTranslate',
@@ -642,6 +643,37 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
},
/**
+ * Resequence records in the given order.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {string[]} ev.data.recordIds
+ * @param {integer} ev.data.offset
+ * @param {string} ev.data.handleField
+ */
+ _onResequenceRecords: function (ev) {
+ var self = this;
+
+ this.trigger_up('mutexify', {
+ action: function () {
+ var state = self.model.get(self.handle);
+ var resIDs = _.map(ev.data.recordIds, function (recordID) {
+ return _.findWhere(state.data, {id: recordID}).res_id;
+ });
+ var options = {
+ offset: ev.data.offset,
+ field: ev.data.handleField,
+ };
+ return self.model.resequence(self.modelName, resIDs, self.handle, options)
+ .then(function () {
+ self._updateEnv();
+ state = self.model.get(self.handle);
+ return self.renderer.updateState(state, {noRender: true});
+ });
+ },
+ });
+ },
+ /**
* @private
* @param {OdooEvent} ev
*/
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 4c03d9b77a1..4ee2ef8fbb3 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -31,6 +31,9 @@ var BasicRenderer = AbstractRenderer.extend({
this.viewType = params.viewType;
this.mode = params.mode || 'readonly';
this.widgets = [];
+ // This attribute lets us know if there is a handle widget on a field,
+ // and on which field it is set.
+ this.handleField = null;
},
/**
* This method has two responsabilities: find every invalid fields in the
@@ -389,6 +392,61 @@ var BasicRenderer = AbstractRenderer.extend({
return this.state.count !== 0;
},
/**
+ * Force the resequencing of the records after moving one of them to a given
+ * index.
+ *
+ * @private
+ * @param {string} recordId datapoint id of the moved record
+ * @param {integer} toIndex new index of the moved record
+ */
+ _moveRecord: function (recordId, toIndex) {
+ var self = this;
+ var records = this.state.data;
+ var record = _.findWhere(records, {id: recordId});
+ var fromIndex = records.indexOf(record);
+ var lowerIndex = Math.min(fromIndex, toIndex);
+ var upperIndex = Math.max(fromIndex, toIndex) + 1;
+ var order = _.findWhere(this.state.orderedBy, {name: this.handleField});
+ var asc = !order || order.asc;
+ var reorderAll = false;
+ var sequence = (asc ? -1 : 1) * Infinity;
+
+ // determine if we need to reorder all records
+ _.each(records, function (record, index) {
+ if ((index < lowerIndex || index >= upperIndex) &&
+ ((asc && sequence >= record.data[self.handleField]) ||
+ (!asc && sequence <= record.data[self.handleField]))) {
+ reorderAll = true;
+ }
+ sequence = record.data[self.handleField];
+ });
+
+ if (reorderAll) {
+ records = _.without(records, record);
+ records.splice(toIndex, 0, record);
+ } else {
+ records = records.slice(lowerIndex, upperIndex);
+ records = _.without(records, record);
+ if (fromIndex > toIndex) {
+ records.unshift(record);
+ } else {
+ records.push(record);
+ }
+ }
+
+ var sequences = _.pluck(_.pluck(records, 'data'), this.handleField);
+ var recordIds = _.pluck(records, 'id');
+ if (!asc) {
+ recordIds.reverse();
+ }
+
+ this.trigger_up('resequence_records', {
+ handleField: this.handleField,
+ offset: _.min(sequences),
+ recordIds: recordIds,
+ });
+ },
+ /**
* This function is called each time a field widget is created, when it is
* ready (after its willStart and Start methods are complete). This is the
* place where work having to do with $el should be done.
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index 4c5326f755b..9e86c857085 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -121,6 +121,10 @@ var KanbanRenderer = BasicRenderer.extend({
this.columnOptions.progressBarStates = {};
}
this.quickCreateEnabled = params.quickCreateEnabled;
+ if (!params.readOnlyMode) {
+ var handleField = _.findWhere(this.state.fieldsInfo.kanban, {widget: 'handle'});
+ this.handleField = handleField && handleField.name;
+ }
this._setState(state);
},
/**
@@ -384,6 +388,27 @@ var KanbanRenderer = BasicRenderer.extend({
self.defs.push(def);
});
+ // enable record resequencing if there is a field with widget='handle'
+ // and if there is no orderBy (in this case we assume that the widget
+ // has been put on the first default order field of the model), or if
+ // the first orderBy field is the one with widget='handle'
+ var orderedBy = this.state.orderedBy;
+ var hasHandle = this.handleField &&
+ (orderedBy.length === 0 || orderedBy[0].name === this.handleField);
+ if (hasHandle) {
+ this.$el.sortable({
+ items: '.o_kanban_record:not(.o_kanban_ghost)',
+ cursor: 'move',
+ revert: 0,
+ delay: 0,
+ tolerance: 'pointer',
+ forcePlaceholderSize: true,
+ stop: function (event, ui) {
+ self._moveRecord(ui.item.data('record').db_id, ui.item.index());
+ },
+ });
+ }
+
// append ghost divs to ensure that all kanban records are left aligned
var prom = Promise.all(self.defs).then(function () {
return self._renderGhostDivs(fragment, 6);
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 49ba9b74034..3764cf7ca22 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -77,13 +77,13 @@ var KanbanView = BasicView.extend({
read_only_mode: params.readOnlyMode,
};
this.rendererParams.quickCreateEnabled = this._isQuickCreateEnabled();
+ this.rendererParams.readOnlyMode = params.readOnlyMode;
var examples = archAttrs.examples;
if (examples) {
this.rendererParams.examples = kanbanExamplesRegistry.get(examples);
}
this.controllerParams.on_create = archAttrs.on_create;
- this.controllerParams.readOnlyMode = false;
this.controllerParams.hasButtons = true;
this.controllerParams.quickCreateEnabled = this.rendererParams.quickCreateEnabled;
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 5db0323c87d..10d3c0d54c5 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -29,7 +29,6 @@ var ListController = BasicController.extend({
group_edit_button_clicked: '_onEditGroupClicked',
edit_line: '_onEditLine',
save_line: '_onSaveLine',
- resequence: '_onResequence',
selection_changed: '_onSelectionChanged',
toggle_column_order: '_onToggleColumnOrder',
toggle_group: '_onToggleGroup',
@@ -630,33 +629,6 @@ var ListController = BasicController.extend({
this._super.apply(this, arguments);
},
/**
- * Force a resequence of the records curently on this page.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onResequence: function (ev) {
- var self = this;
-
- this.trigger_up('mutexify', {
- action: function () {
- var state = self.model.get(self.handle);
- var resIDs = _.map(ev.data.rowIDs, function(rowID) {
- return _.findWhere(state.data, {id: rowID}).res_id;
- });
- var options = {
- offset: ev.data.offset,
- field: ev.data.handleField,
- };
- return self.model.resequence(self.modelName, resIDs, self.handle, options).then(function () {
- self._updateEnv();
- state = self.model.get(self.handle);
- return self.renderer.updateState(state, {noRender: true});
- });
- },
- });
- },
- /**
* Called when the renderer displays an editable row and the user tries to
* leave it -> Saves the record associated to that line.
*
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 818739c6f48..c68f5ab573a 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -775,6 +775,7 @@ ListRenderer.include({
* @override
*/
_renderBody: function () {
+ var self = this;
var $body = this._super.apply(this, arguments);
if (this.hasHandle) {
$body.sortable({
@@ -782,7 +783,11 @@ ListRenderer.include({
items: '> tr.o_data_row',
helper: 'clone',
handle: '.o_row_handle',
- stop: this._resequence.bind(this),
+ stop: function (event, ui) {
+ self.unselectRow().then(function () {
+ self._moveRecord(ui.item.data('id'), ui.item.index());
+ });
+ },
});
}
return $body;
@@ -891,65 +896,6 @@ ListRenderer.include({
});
},
/**
- * Force the resequencing of the items in the list.
- *
- * @private
- * @param {jQuery.Event} event
- * @param {Object} ui jqueryui sortable widget
- */
- _resequence: function (event, ui) {
- var self = this;
- var movedRecordID = ui.item.data('id');
- var rows = this.state.data;
- var row = self._getRecord(movedRecordID);
- var index0 = rows.indexOf(row);
- var index1 = ui.item.index();
- var lower = Math.min(index0, index1);
- var upper = Math.max(index0, index1) + 1;
-
- var order = _.findWhere(self.state.orderedBy, {name: self.handleField});
- var asc = !order || order.asc;
- var reorderAll = false;
- var sequence = (asc ? -1 : 1) * Infinity;
-
- // determine if we need to reorder all lines
- _.each(rows, function (row, index) {
- if ((index < lower || index >= upper) &&
- ((asc && sequence >= row.data[self.handleField]) ||
- (!asc && sequence <= row.data[self.handleField]))) {
- reorderAll = true;
- }
- sequence = row.data[self.handleField];
- });
-
- if (reorderAll) {
- rows = _.without(rows, row);
- rows.splice(index1, 0, row);
- } else {
- rows = rows.slice(lower, upper);
- rows = _.without(rows, row);
- if (index0 > index1) {
- rows.unshift(row);
- } else {
- rows.push(row);
- }
- }
-
- var sequences = _.pluck(_.pluck(rows, 'data'), self.handleField);
- var rowIDs = _.pluck(rows, 'id');
-
- if (!asc) {
- rowIDs.reverse();
- }
- this.unselectRow().then(function () {
- self.trigger_up('resequence', {
- rowIDs: rowIDs,
- offset: _.min(sequences),
- handleField: self.handleField,
- });
- });
- },
- /**
* This is one of the trickiest method in the editable renderer. It has to
* do a lot of stuff: it has to determine which cell should be selected (if
* the target cell is readonly, we need to find another suitable cell), then
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 471598ca4e4..4d8be05d252 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -51,9 +51,6 @@ var ListRenderer = BasicRenderer.extend({
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
- // This attribute lets us know if there is a handle widget on a field,
- // and on which field it is set.
- this.handleField = null;
this.rowDecorations = _.chain(this.arch.attrs)
.pick(function (value, key) {
return DECORATIONS.indexOf(key) >= 0;
diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
index 85975d99231..40d362d220c 100644
--- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
@@ -2,7 +2,6 @@ odoo.define('web.field_one_to_many_tests', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
-var concurrency = require('web.concurrency');
var FormView = require('web.FormView');
var KanbanRecord = require('web.KanbanRecord');
var ListRenderer = require('web.ListRenderer');
@@ -121,6 +120,7 @@ QUnit.module('fields', {}, function () {
display_name: "leonardo",
turtle_bar: true,
turtle_foo: "yop",
+ turle_int: 1,
partner_ids: [],
}, {
id: 2,
@@ -8290,6 +8290,59 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+
+ QUnit.test('one2many kanban with widget handle', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<kanban>' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><field name="turtle_foo"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], {
+ turtles: [
+ [1, 2, {turtle_int: 0}],
+ [1, 3, {turtle_int: 1}],
+ [1, 1, {turtle_int: 2}],
+ ],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'yopblipkawa');
+ assert.doesNotHaveClass(form.$('.o_field_one2many .o_kanban_view'), 'ui-sortable');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_field_one2many .o_kanban_view'), 'ui-sortable');
+
+ var $record = form.$('.o_field_one2many[name=turtles] .o_kanban_view .o_kanban_record:first');
+ var $to = form.$('.o_field_one2many[name=turtles] .o_kanban_view .o_kanban_record:nth-child(3)');
+ await testUtils.dom.dragAndDrop($record, $to, {position: "bottom"});
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'blipkawayop');
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
});
});
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index f0dc078f26c..8fbbab8c27b 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -5571,6 +5571,82 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('ungrouped kanban with handle field', async function (assert) {
+ assert.expect(4);
+
+ var envIDs = [1, 2, 3, 4]; // the ids that should be in the environment during this test
+
+ var kanban = await createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<field name="int_field" widget="handle" />' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ assert.deepEqual(args.ids, envIDs,
+ "should write the sequence in correct order");
+ return Promise.resolve(true);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.hasClass(kanban.$('.o_kanban_view'), 'ui-sortable');
+ assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(),
+ 'yopblipgnapblip');
+
+ var $record = kanban.$('.o_kanban_view .o_kanban_record:first');
+ var $to = kanban.$('.o_kanban_view .o_kanban_record:nth-child(4)');
+ envIDs = [2, 3, 4, 1]; // first record of moved after last one
+ await testUtils.dom.dragAndDrop($record, $to, {position: "bottom"});
+
+ assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(),
+ 'blipgnapblipyop');
+
+ kanban.destroy();
+ });
+
+ QUnit.test('ungrouped kanban without handle field', async function (assert) {
+ assert.expect(3);
+
+ var kanban = await createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</t></templates></kanban>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ assert.ok(false, "should not trigger a resequencing");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.doesNotHaveClass(kanban.$('.o_kanban_view'), 'ui-sortable');
+ assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(),
+ 'yopblipgnapblip');
+
+ var $draggedRecord = kanban.$('.o_kanban_view .o_kanban_record:first');
+ var $to = kanban.$('.o_kanban_view .o_kanban_record:nth-child(4)');
+ await testUtils.dom.dragAndDrop($draggedRecord, $to, {position: "bottom"});
+
+ assert.strictEqual(kanban.$('.o_kanban_record:not(.o_kanban_ghost)').text(),
+ 'yopblipgnapblip');
+
+ kanban.destroy();
+ });
+
});
});
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index 05e546dfec6..0e6d2578bad 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -1673,9 +1673,10 @@ order.
- handle (HandleWidget)
- This field's job is to be displayed as a *handle* in a list view, and allows
- reordering the various records by drag and dropping lines.
+ This field's job is to be displayed as a *handle*, and allows reordering the
+ various records by drag and dropping them.
+ .. warning:: It has to be specified on the field by which records are sorted.
.. warning:: Having more than one field with a handle widget on the same list is not supported.
- Supported field types: *integer*
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index 56dca21bf1d..767bb8bbe5b 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -309,16 +309,14 @@ Possible children elements of the list view are:
lists the groups which should be able to see the field
``widget``
alternate representations for a field's display. Possible list view
- values are:
+ values are (among others):
``progressbar``
displays ``float`` fields as a progress bar.
- ``many2onebutton``
- replaces the m2o field's value by a checkmark if the field is
- filled, and a cross if it is not
``handle``
- for ``sequence`` fields, instead of displaying the field's value
- just displays a drag&drop icon
+ for ``sequence`` (or ``integer``) fields by which records are
+ sorted, instead of displaying the field's value just displays a
+ drag&drop icon to reorder records.
``sum``, ``avg``
displays the corresponding aggregate at the bottom of the column. The
aggregation is only computed on *currently displayed* records. The
@@ -1101,8 +1099,14 @@ Possible children of the view element are:
While most of the Kanban templates are standard :ref:`reference/qweb`, the
Kanban view processes ``field``, ``button`` and ``a`` elements specially:
- * by default fields are replaced by their formatted value, unless they
- match specific kanban view widgets
+ * by default fields are replaced by their formatted value, unless the
+ ``widget`` attribute is specified, in which case their rendering and
+ behavior depends on the corresponding widget. Possible values are (among
+ others):
+
+ ``handle``
+ for ``sequence`` (or ``integer``) fields by which records are
+ sorted, allows to drag&drop records to reorder them.
.. todo:: list widgets?