[IMP] web: list: editable grouped list views
This rev. enables the editable feature in grouped list views. Part of task 1915702 Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Martin Geubelle <mge@odoo.com>
This commit is contained in:
committed by
Aaron Bohy
co-authored by
Aaron Bohy
Martin Geubelle
parent
55676110c8
commit
234d92c4f0
@@ -1240,8 +1240,7 @@ var FieldX2Many = AbstractField.extend({
|
||||
_onEditLine: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('edited_list', { id: this.value.id });
|
||||
var editedRecord = this.value.data[ev.data.index];
|
||||
this.renderer.setRowMode(editedRecord.id, 'edit')
|
||||
this.renderer.setRowMode(ev.data.recordId, 'edit')
|
||||
.then(ev.data.onSuccess);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -88,8 +88,10 @@ var concurrency = require('web.concurrency');
|
||||
var Context = require('web.Context');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var fieldUtils = require('web.field_utils');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
var viewUtils = require('web.viewUtils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
@@ -178,7 +180,7 @@ var BasicModel = AbstractModel.extend({
|
||||
addDefaultRecord: function (listID, options) {
|
||||
var self = this;
|
||||
var list = this.localData[listID];
|
||||
var context = this._getContext(list);
|
||||
var context = _.extend({}, this._getDefaultContext(list), this._getContext(list));
|
||||
|
||||
var position = (options && options.position) || 'top';
|
||||
var params = {
|
||||
@@ -3332,6 +3334,27 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
return toFetch;
|
||||
},
|
||||
/**
|
||||
* Given a dataPoint of type list (that may be a group), returns an object
|
||||
* with 'default_' keys to be used to create new records in that group.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} dataPoint
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getDefaultContext: function (dataPoint) {
|
||||
var defaultContext = {};
|
||||
while (dataPoint.parentID) {
|
||||
var parent = this.localData[dataPoint.parentID];
|
||||
var groupByField = parent.groupedBy[0].split(':')[0];
|
||||
var value = viewUtils.getGroupValue(dataPoint, groupByField);
|
||||
if (value) {
|
||||
defaultContext['default_' + groupByField] = value;
|
||||
}
|
||||
dataPoint = parent;
|
||||
}
|
||||
return defaultContext;
|
||||
},
|
||||
/**
|
||||
* Some records are associated to a/some domain(s). This method allows to
|
||||
* retrieve them, evaluated.
|
||||
|
||||
@@ -91,8 +91,7 @@ var BasicRenderer = AbstractRenderer.extend({
|
||||
*/
|
||||
confirmChange: function (state, id, fields, ev) {
|
||||
this.state = state;
|
||||
|
||||
var record = state.id === id ? state : _.findWhere(state.data, {id: id});
|
||||
var record = this._getRecord(id);
|
||||
if (!record) {
|
||||
return this._render().then(_.constant([]));
|
||||
}
|
||||
@@ -350,6 +349,19 @@ var BasicRenderer = AbstractRenderer.extend({
|
||||
_getModifiersData: function (node) {
|
||||
return _.findWhere(this.allModifiersData, {node: node});
|
||||
},
|
||||
/**
|
||||
* This function is meant to be overriden in renderers. It takes a dataPoint
|
||||
* id (for a dataPoint of type record), and should return the corresponding
|
||||
* dataPoint.
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @param {string} [recordId]
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
_getRecord: function (recordId) {
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryElement} $el
|
||||
|
||||
@@ -345,6 +345,13 @@ var FormRenderer = BasicRenderer.extend({
|
||||
}
|
||||
return idForLabel;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_getRecord: function (recordId) {
|
||||
return this.state.id === recordId ? this.state : null;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
|
||||
@@ -24,6 +24,7 @@ var ListController = BasicController.extend({
|
||||
*/
|
||||
buttons_template: 'ListView.buttons',
|
||||
custom_events: _.extend({}, BasicController.prototype.custom_events, {
|
||||
activate_next_widget: '_onActivateNextWidget',
|
||||
add_record: '_onAddRecord',
|
||||
button_clicked: '_onButtonClicked',
|
||||
edit_line: '_onEditLine',
|
||||
@@ -128,8 +129,8 @@ var ListController = BasicController.extend({
|
||||
|
||||
this._assignCreateKeyboardBehavior(this.$buttons.find('.o_list_button_add'));
|
||||
this.$buttons.find('.o_list_button_add').tooltip({
|
||||
delay: {show: 200, hide:0},
|
||||
title: function(){
|
||||
delay: {show: 200, hide: 0},
|
||||
title: function () {
|
||||
return qweb.render('CreateButton.tooltip');
|
||||
},
|
||||
trigger: 'manual',
|
||||
@@ -230,17 +231,19 @@ var ListController = BasicController.extend({
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Adds a record to the list.
|
||||
* Adds a new record to the a dataPoint of type 'list'.
|
||||
* Disables the buttons to prevent concurrent record creation or edition.
|
||||
*
|
||||
* @todo make record creation a basic controller feature
|
||||
* @private
|
||||
* @param {string} dataPointId a dataPoint of type 'list' (may be grouped)
|
||||
* @return {Promise}
|
||||
*/
|
||||
_addRecord: function () {
|
||||
_addRecord: function (dataPointId) {
|
||||
var self = this;
|
||||
this._disableButtons();
|
||||
return this.renderer.unselectRow().then(function () {
|
||||
return self.model.addDefaultRecord(self.handle, {
|
||||
return self.model.addDefaultRecord(dataPointId, {
|
||||
position: self.editable,
|
||||
});
|
||||
}).then(function (recordID) {
|
||||
@@ -365,6 +368,19 @@ var ListController = BasicController.extend({
|
||||
_toggleArchiveState: function (archive) {
|
||||
this._archive(this.selectedRecords, archive);
|
||||
},
|
||||
/**
|
||||
* Hide the create button in non-empty grouped editable list views, as an
|
||||
* 'Add an item' link is available in each group.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toggleCreateButton: function () {
|
||||
if (this.$buttons) {
|
||||
var state = this.model.get(this.handle);
|
||||
var createHidden = this.editable && state.groupedBy.length && state.data.length;
|
||||
this.$buttons.find('.o_list_button_add').toggleClass('o_hidden', !!createHidden);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Display the sidebar (the 'action' menu in the control panel) if we have
|
||||
* some selected records.
|
||||
@@ -380,8 +396,9 @@ var ListController = BasicController.extend({
|
||||
*/
|
||||
_update: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function() {
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self._toggleSidebar();
|
||||
self._toggleCreateButton();
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -401,15 +418,29 @@ var ListController = BasicController.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Add a record to the list
|
||||
* Triggered when navigating with TAB, when the end of the list has been
|
||||
* reached. Go back to the first row in that case.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_onActivateNextWidget: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.renderer.editFirstRecord();
|
||||
},
|
||||
/**
|
||||
* Add a record to the list
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {string} [ev.data.groupId=this.handle] the id of a dataPoint of
|
||||
* type list to which the record must be added (default: main list)
|
||||
*/
|
||||
_onAddRecord: function (ev) {
|
||||
ev.stopPropagation();
|
||||
var dataPointId = ev.data.groupId || this.handle;
|
||||
if (this.activeActions.create) {
|
||||
this._addRecord();
|
||||
this._addRecord(dataPointId);
|
||||
} else if (ev.data.onFail) {
|
||||
ev.data.onFail();
|
||||
}
|
||||
@@ -441,7 +472,7 @@ var ListController = BasicController.extend({
|
||||
}
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
if (this.editable && !state.groupedBy.length) {
|
||||
this._addRecord();
|
||||
this._addRecord(this.handle);
|
||||
} else {
|
||||
this.trigger_up('switch_view', {view_type: 'form', res_id: undefined});
|
||||
}
|
||||
@@ -473,9 +504,7 @@ var ListController = BasicController.extend({
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('mutexify', {
|
||||
action: function () {
|
||||
var record = self.model.get(self.handle);
|
||||
var editedRecord = record.data[ev.data.index];
|
||||
self._setMode('edit', editedRecord.id)
|
||||
self._setMode('edit', ev.data.recordId)
|
||||
.then(ev.data.onSuccess);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ ListRenderer.include({
|
||||
}),
|
||||
events: _.extend({}, ListRenderer.prototype.events, {
|
||||
'click .o_field_x2many_list_row_add a': '_onAddRecord',
|
||||
'click .o_group_field_row_add a': '_onAddRecordToGroup',
|
||||
'keydown .o_field_x2many_list_row_add a': '_onKeyDownAddRecord',
|
||||
'click tbody td.o_data_cell': '_onCellClick',
|
||||
'click tbody tr:not(.o_data_row)': '_onEmptyRowClick',
|
||||
@@ -34,15 +35,19 @@ ListRenderer.include({
|
||||
* @override
|
||||
* @param {Object} params
|
||||
* @param {boolean} params.addCreateLine
|
||||
* @param {boolean} params.addCreateLineInGroups
|
||||
* @param {boolean} params.addTrashIcon
|
||||
* @param {boolean} params.isMany2Many
|
||||
*/
|
||||
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
|
||||
// if addCreateLine (resp. addCreateLineInGroups) is true, the renderer
|
||||
// will add a 'Add a line' link at the bottom of the list view (resp.
|
||||
// at the bottom of each group)
|
||||
this.addCreateLine = params.addCreateLine;
|
||||
this.addCreateLineInGroups = params.addCreateLineInGroups;
|
||||
|
||||
// Controls allow overriding "add a line" by custom controls.
|
||||
|
||||
@@ -90,19 +95,14 @@ ListRenderer.include({
|
||||
|
||||
this.currentRow = null;
|
||||
this.currentFieldIndex = null;
|
||||
this.allRecordsIds = null; // flat array of records ids used by navigation
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @returns {Promise}
|
||||
*/
|
||||
start: function () {
|
||||
// deliberately use the 'editable' attribute instead of '_isEditable'
|
||||
// function, because the groupBy must not be taken into account to
|
||||
// enable the '_onWindowClicked' handler (otherwise, an editable grouped
|
||||
// list which is reloaded without groupBy wouldn't have this handler
|
||||
// bound, and edited rows couldn't be left by clicking outside the list)
|
||||
if (this.editable) {
|
||||
this.$el.css({height: '100%'}); // seems useless: to remove in master
|
||||
core.bus.on('click', this, this._onWindowClicked.bind(this));
|
||||
}
|
||||
return this._super();
|
||||
@@ -139,15 +139,13 @@ ListRenderer.include({
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
confirmChange: function (state, id) {
|
||||
confirmChange: function (state, recordID) {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function (widgets) {
|
||||
if (widgets.length) {
|
||||
var rowIndex = _.findIndex(state.data, function (r) {
|
||||
return r.id === id;
|
||||
});
|
||||
var $row = self.$('.o_data_row:nth(' + rowIndex + ')');
|
||||
self._setDecorationClasses(state.data[rowIndex], $row);
|
||||
var $row = self._getRow(recordID);
|
||||
var record = self._getRecord(recordID);
|
||||
self._setDecorationClasses(record, $row);
|
||||
self._updateFooter();
|
||||
}
|
||||
return widgets;
|
||||
@@ -177,83 +175,96 @@ ListRenderer.include({
|
||||
confirmUpdate: function (state, id, fields, ev) {
|
||||
var self = this;
|
||||
|
||||
// store the cursor position to restore it once potential onchanges have
|
||||
// been applied
|
||||
var currentRowID, currentWidget, focusedElement, selectionRange;
|
||||
if (this.currentRow !== null) {
|
||||
currentRowID = this.state.data[this.currentRow].id;
|
||||
currentWidget = this.allFieldWidgets[currentRowID][this.currentFieldIndex];
|
||||
if (currentWidget) {
|
||||
focusedElement = currentWidget.getFocusableElement().get(0);
|
||||
if (currentWidget.formatType !== 'boolean') {
|
||||
selectionRange = dom.getSelectionRange(focusedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var oldData = this.state.data;
|
||||
this.state = state;
|
||||
return this.confirmChange(state, id, fields, ev).then(function () {
|
||||
// If no record with 'id' can be found in the state, the
|
||||
// confirmChange method will have rerendered the whole view already,
|
||||
// so no further work is necessary.
|
||||
var record = _.findWhere(state.data, {id: id});
|
||||
var record = self._getRecord(id);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
var oldRowIndex = _.findIndex(oldData, {id: id});
|
||||
var $row = self.$('.o_data_row:nth(' + oldRowIndex + ')');
|
||||
$row.nextAll('.o_data_row').remove();
|
||||
$row.prevAll().remove();
|
||||
|
||||
_.each(oldData, function (rec) {
|
||||
if (rec.id !== id) {
|
||||
self._destroyFieldWidgets(rec.id);
|
||||
}
|
||||
});
|
||||
var newRowIndex = _.findIndex(state.data, {id: id});
|
||||
var $lastRow = $row;
|
||||
var defs = [];
|
||||
self.defs = defs;
|
||||
state.data.forEach(function (record, index) {
|
||||
if (index === newRowIndex) {
|
||||
return;
|
||||
}
|
||||
// FIXME: as we are manipulating the DOM directly, it will
|
||||
// flicker if there is an async widget in the row
|
||||
var $newRow = self._renderRow(record);
|
||||
if (index < newRowIndex) {
|
||||
$newRow.insertBefore($row);
|
||||
} else {
|
||||
$newRow.insertAfter($lastRow);
|
||||
$lastRow = $newRow;
|
||||
}
|
||||
});
|
||||
|
||||
// re-render whole body (outside the dom)
|
||||
self.defs = [];
|
||||
var $newBody = self._renderBody();
|
||||
var defs = self.defs;
|
||||
delete self.defs;
|
||||
|
||||
return Promise.all(defs).then(function () {
|
||||
// update registered modifiers to edit 'mode' because the call to
|
||||
// _renderBody set baseModeByRecord as 'readonly'
|
||||
_.each(self.columns, function (node) {
|
||||
self._registerModifiers(node, record, null, {mode: 'edit'});
|
||||
});
|
||||
|
||||
// store the selection range to restore it once the table will
|
||||
// be re-rendered, and the current cell re-selected
|
||||
var currentRowID, currentWidget, focusedElement, selectionRange;
|
||||
if (self.currentRow !== null) {
|
||||
self.currentRow = newRowIndex;
|
||||
return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true});
|
||||
}
|
||||
}).then(function () {
|
||||
// restore the cursor position
|
||||
currentRowID = self.state.data[newRowIndex].id;
|
||||
currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
|
||||
if (currentWidget) {
|
||||
focusedElement = currentWidget.getFocusableElement().get(0);
|
||||
if (selectionRange) {
|
||||
dom.setSelectionRange(focusedElement, selectionRange);
|
||||
currentRowID = self._getRecordID(self.currentRow);
|
||||
currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
|
||||
if (currentWidget) {
|
||||
focusedElement = currentWidget.getFocusableElement().get(0);
|
||||
if (currentWidget.formatType !== 'boolean') {
|
||||
selectionRange = dom.getSelectionRange(focusedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all rows except the one being edited, and insert rows
|
||||
// of the re-rendered body before and after it
|
||||
var $editedRow = self._getRow(id);
|
||||
$editedRow.nextAll().remove();
|
||||
$editedRow.prevAll().remove();
|
||||
var $newRow = $newBody.find('.o_data_row[data-id="' + id + '"]');
|
||||
var $tbody = self.$('tbody');
|
||||
$newRow.prevAll().each(function (i, prevRow) {
|
||||
$tbody.prepend($(prevRow));
|
||||
});
|
||||
$newRow.nextAll().each(function (i, nextRow) {
|
||||
$tbody.append($(nextRow));
|
||||
});
|
||||
|
||||
if (self.currentRow !== null) {
|
||||
var newRowIndex = $editedRow.prop('rowIndex') - 1;
|
||||
self.currentRow = newRowIndex;
|
||||
return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true})
|
||||
.then(function () {
|
||||
// restore the selection range
|
||||
currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
|
||||
if (currentWidget) {
|
||||
focusedElement = currentWidget.getFocusableElement().get(0);
|
||||
if (selectionRange) {
|
||||
dom.setSelectionRange(focusedElement, selectionRange);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Edit the first record in the list
|
||||
*/
|
||||
editFirstRecord: function () {
|
||||
this._selectCell(this._getFirstDataRowIndex(), 0);
|
||||
},
|
||||
/**
|
||||
* Edit a given record in the list
|
||||
*
|
||||
* @param {string} recordID
|
||||
*/
|
||||
editRecord: function (recordID) {
|
||||
var rowIndex = _.findIndex(this.state.data, {id: recordID});
|
||||
var $row = this._getRow(recordID);
|
||||
var rowIndex = $row.prop('rowIndex') - 1;
|
||||
this._selectCell(rowIndex, 0);
|
||||
},
|
||||
/**
|
||||
@@ -264,7 +275,7 @@ ListRenderer.include({
|
||||
*/
|
||||
getEditableRecordID: function () {
|
||||
if (this.currentRow !== null) {
|
||||
return this.state.data[this.currentRow].id;
|
||||
return this._getRecordID(this.currentRow);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -276,19 +287,18 @@ ListRenderer.include({
|
||||
* @param {string} recordID
|
||||
*/
|
||||
removeLine: function (state, recordID) {
|
||||
var rowIndex = _.findIndex(this.state.data, {id: recordID});
|
||||
this.state = state;
|
||||
if (rowIndex === -1) {
|
||||
var $row = this._getRow(recordID);
|
||||
if ($row.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (rowIndex === this.currentRow) {
|
||||
if ($row.prop('rowIndex') - 1 === this.currentRow) {
|
||||
this.currentRow = null;
|
||||
}
|
||||
|
||||
// destroy widgets first
|
||||
this._destroyFieldWidgets(recordID);
|
||||
// remove the row
|
||||
var $row = this.$('.o_data_row:nth(' + rowIndex + ')');
|
||||
if (this.state.count >= 4) {
|
||||
$row.remove();
|
||||
} else {
|
||||
@@ -305,35 +315,14 @@ ListRenderer.include({
|
||||
*/
|
||||
setRowMode: function (recordID, mode) {
|
||||
var self = this;
|
||||
|
||||
// find the record and its row index (handles ungrouped and grouped cases
|
||||
// as even if the grouped list doesn't support edition, it may contain
|
||||
// a widget allowing the edition in readonly (e.g. priority), so it
|
||||
// should be able to update a record as well)
|
||||
var record;
|
||||
var rowIndex;
|
||||
if (this.state.groupedBy.length) {
|
||||
rowIndex = -1;
|
||||
var count = 0;
|
||||
utils.traverse_records(this.state, function (r) {
|
||||
if (r.id === recordID) {
|
||||
record = r;
|
||||
rowIndex = count;
|
||||
}
|
||||
count++;
|
||||
});
|
||||
} else {
|
||||
rowIndex = _.findIndex(this.state.data, {id: recordID});
|
||||
record = this.state.data[rowIndex];
|
||||
}
|
||||
|
||||
if (rowIndex < 0) {
|
||||
var record = self._getRecord(recordID);
|
||||
if (!record) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
var editMode = (mode === 'edit');
|
||||
|
||||
this.currentRow = editMode ? rowIndex : null;
|
||||
var $row = this.$('.o_data_row:nth(' + rowIndex + ')');
|
||||
var editMode = (mode === 'edit');
|
||||
var $row = this._getRow(recordID);
|
||||
this.currentRow = editMode ? $row.prop('rowIndex') - 1 : null;
|
||||
var $tds = $row.children('.o_data_cell');
|
||||
var oldWidgets = _.clone(this.allFieldWidgets[record.id]);
|
||||
|
||||
@@ -416,14 +405,13 @@ ListRenderer.include({
|
||||
if (this.currentRow === null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var record = this.state.data[this.currentRow];
|
||||
var recordWidgets = this.allFieldWidgets[record.id];
|
||||
var recordID = this._getRecordID(this.currentRow);
|
||||
var recordWidgets = this.allFieldWidgets[recordID];
|
||||
toggleWidgets(true);
|
||||
|
||||
var prom = new Promise(function (resolve, reject) {
|
||||
self.trigger_up('save_line', {
|
||||
recordID: record.id,
|
||||
recordID: recordID,
|
||||
onSuccess: resolve,
|
||||
onFailure: reject,
|
||||
});
|
||||
@@ -458,6 +446,30 @@ ListRenderer.include({
|
||||
delete this.allFieldWidgets[recordID];
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @returns {integer}
|
||||
*/
|
||||
_getFirstDataRowIndex: function () {
|
||||
return this.$('.o_data_row:first').prop('rowIndex') - 1;
|
||||
},
|
||||
/**
|
||||
* Given a table row inside a group, returns the index of the first data
|
||||
* row of the next group (if any).
|
||||
*
|
||||
* @param {jQuery} $row this row must be inside a group
|
||||
* @returns {integer|null}
|
||||
*/
|
||||
_getNextGroupFirstRowIndex: function ($row) {
|
||||
var $nextBody = $row.closest('tbody').next();
|
||||
while ($nextBody.length && !$nextBody.find('.o_data_row').length) {
|
||||
$nextBody = $nextBody.next();
|
||||
}
|
||||
if ($nextBody.find('.o_data_row').length) {
|
||||
return $nextBody.find('.o_data_row:first').prop('rowIndex') - 1;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* Returns the current number of columns. The editable renderer may add a
|
||||
* trash icon on the right of a record, so we need to take this into account
|
||||
@@ -473,49 +485,170 @@ ListRenderer.include({
|
||||
return n;
|
||||
},
|
||||
/**
|
||||
* Returns true iff the list is editable, i.e. if it isn't grouped and if
|
||||
* the editable attribute is set on the root node of its arch.
|
||||
* Traverse this.state to find and return the record with given dataPoint id
|
||||
* (for grouped list views, the record could be deep down in state tree).
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isEditable: function () {
|
||||
return !this.state.groupedBy.length && this.editable;
|
||||
_getRecord: function (recordId) {
|
||||
var record;
|
||||
utils.traverse_records(this.state, function (r) {
|
||||
if (r.id === recordId) {
|
||||
record = r;
|
||||
}
|
||||
});
|
||||
return record;
|
||||
},
|
||||
/**
|
||||
* Move the cursor on the end of the previous line, if possible.
|
||||
* If there is no previous line, then we create a new record.
|
||||
* Retrieve the record dataPoint id from a rowIndex as the row DOM element
|
||||
* stores the record id in data.
|
||||
*
|
||||
* @private
|
||||
* @param {integer} rowIndex
|
||||
* @returns {string} record dataPoint id
|
||||
*/
|
||||
_getRecordID: function (rowIndex) {
|
||||
var $tr = this.$('table.o_list_view > tbody tr').eq(rowIndex);
|
||||
return $tr.data('id');
|
||||
},
|
||||
/**
|
||||
* Return the jQuery tr element corresponding to the given record dataPoint
|
||||
* id.
|
||||
*
|
||||
* @private
|
||||
* @param {string} [recordId]
|
||||
* @returns {jQueryElement}
|
||||
*/
|
||||
_getRow: function (recordId) {
|
||||
return this.$('.o_data_row[data-id="' + recordId + '"]');
|
||||
},
|
||||
/**
|
||||
* Move the cursor on the end of the previous line (or of the last line if
|
||||
* we are on the first one).
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_moveToPreviousLine: function () {
|
||||
if (this.currentRow > 0) {
|
||||
this._selectCell(this.currentRow - 1, this.columns.length - 1);
|
||||
} else {
|
||||
this.unselectRow().then(this.trigger_up.bind(this, 'add_record'));
|
||||
var self = this;
|
||||
if (!this.allRecordsIds) {
|
||||
// compute the flat array of all records ids only once
|
||||
this.allRecordIds = [];
|
||||
utils.traverse_records(this.state, function (data) {
|
||||
self.allRecordIds.push(data.id);
|
||||
});
|
||||
}
|
||||
var curRecordId = this._getRecordID(this.currentRow);
|
||||
var curRecordIndex = this.allRecordIds.indexOf(curRecordId);
|
||||
var prevRecordIndex = curRecordIndex === 0 ? this.allRecordIds.length - 1 : curRecordIndex - 1;
|
||||
this.commitChanges(curRecordId).then(function () {
|
||||
var $prevRow = self._getRow(self.allRecordIds[prevRecordIndex]);
|
||||
var prevRowIndex = $prevRow.prop('rowIndex') - 1;
|
||||
self._selectCell(prevRowIndex, self.columns.length - 1);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Move the cursor on the beginning of the next line, if possible.
|
||||
* If there is no next line, then we create a new record.
|
||||
* If we are on the last line (of a group in the grouped case) and the list
|
||||
* is editable="bottom", we create a new record, otherwise, we move the
|
||||
* cursor to the first line (of the next group in the grouped case).
|
||||
*
|
||||
* @private
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.forceCreate=false] typically set to true when
|
||||
* navigating with ENTER ; in this case, if the next row is the 'Add a
|
||||
* line' row, always create a new record (never skip it, like TAB does
|
||||
* under some conditions)
|
||||
*/
|
||||
_moveToNextLine: function () {
|
||||
_moveToNextLine: function (options) {
|
||||
var self = this;
|
||||
var record = this.state.data[this.currentRow];
|
||||
this.commitChanges(record.id).then(function () {
|
||||
var fieldNames = self.canBeSaved(record.id);
|
||||
if (fieldNames.length) {
|
||||
options = options || {};
|
||||
var recordID = this._getRecordID(this.currentRow);
|
||||
var record = this._getRecord(recordID);
|
||||
|
||||
this.commitChanges(recordID).then(function () {
|
||||
var fieldNames = self.canBeSaved(recordID);
|
||||
if (fieldNames.length && (record.isDirty() || options.forceCreate)) {
|
||||
// the current row is invalid, we only leave it if it is not dirty
|
||||
// (we didn't make any change on this row, which is a new one) and
|
||||
// we are navigating with TAB (forceCreate=false)
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.currentRow < self.state.data.length - 1) {
|
||||
self._selectCell(self.currentRow + 1, 0);
|
||||
// compute the index of the next (record) row to select, if any
|
||||
var nextRowIndex = null;
|
||||
var groupId;
|
||||
if (!self.isGrouped) {
|
||||
// ungrouped case
|
||||
if (self.currentRow < self.state.data.length - 1) {
|
||||
nextRowIndex = self.currentRow + 1;
|
||||
} else if (!options.forceCreate && !record.isDirty()) {
|
||||
self.trigger_up('discard_changes', {
|
||||
recordID: recordID,
|
||||
onSuccess: function () {
|
||||
self.trigger_up('activate_next_widget');
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// grouped case
|
||||
var $currentRow = self._getRow(recordID);
|
||||
var $nextRow = $currentRow.next();
|
||||
if ($nextRow.hasClass('o_data_row')) {
|
||||
// the next row is a record row (in same group), select it
|
||||
nextRowIndex = self.currentRow + 1;
|
||||
} else if ($nextRow.hasClass('o_add_record_row') && self.editable === "bottom") {
|
||||
// the next row is the 'Add a line' row (i.e. the current one is the last record
|
||||
// row of the group)
|
||||
if (options.forceCreate || record.isDirty()) {
|
||||
// if we modified the current record, add a row to create a new record
|
||||
groupId = $nextRow.data('groupID');
|
||||
} else {
|
||||
// if we didn't change anything to the current line (e.g. we pressed TAB on
|
||||
// each cell without modifying/entering any data), we discard that line (if
|
||||
// it was a new one) and move to the first record of the next group
|
||||
nextRowIndex = self._getNextGroupFirstRowIndex($currentRow);
|
||||
self.trigger_up('discard_changes', {
|
||||
recordID: recordID,
|
||||
onSuccess: function () {
|
||||
if (nextRowIndex !== null) {
|
||||
if (!record.res_id) {
|
||||
// the current record was a new one, so we decrement
|
||||
// nextRowIndex as that row has been removed meanwhile
|
||||
nextRowIndex--;
|
||||
}
|
||||
self._selectCell(nextRowIndex, 0);
|
||||
} else {
|
||||
// we were in the last group, so go back to the top
|
||||
self._selectCell(self._getFirstDataRowIndex(), 0, {});
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// there is no 'Add a line' row (i.e. the create feature is disabled), or the
|
||||
// list is editable="top", we focus the first record of the next group if any
|
||||
nextRowIndex = self._getNextGroupFirstRowIndex($currentRow);
|
||||
if (nextRowIndex === null) {
|
||||
// we were on the last group, so we go back to the top of the list
|
||||
nextRowIndex = self._getFirstDataRowIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a (record) row to select, select it, otherwise, add a new record (in the
|
||||
// correct group, if the view is grouped)
|
||||
if (nextRowIndex !== null) {
|
||||
self._selectCell(nextRowIndex, 0);
|
||||
} else {
|
||||
self.unselectRow().then(function () {
|
||||
// if for some reason (e.g. create feature is disabled) we can't add a new
|
||||
// record, select the first record row
|
||||
self.trigger_up('add_record', {
|
||||
onFail: self._selectCell.bind(self, 0, 0, {}),
|
||||
groupId: groupId,
|
||||
onFail: self._selectCell.bind(self, self._getFirstDataRowIndex(), 0, {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -530,6 +663,31 @@ ListRenderer.include({
|
||||
this.currentFieldIndex = null;
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Override to add the 'Add an item' link to the end of last-level opened
|
||||
* groups.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderGroup: function (group) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (!group.groupedBy.length && this.addCreateLineInGroups) {
|
||||
var $groupBody = result[0];
|
||||
var $a = $('<a href="#" role="button">')
|
||||
.text(_t("Add a line"))
|
||||
.data('groupID', group.id);
|
||||
var $td = $('<td>')
|
||||
.attr('colspan', this._getNumberOfCols())
|
||||
.addClass('o_group_field_row_add')
|
||||
.append($a);
|
||||
var $tr = $('<tr>', {class: 'o_add_record_row'})
|
||||
.data('groupID', group.id)
|
||||
.append($td);
|
||||
$groupBody.append($tr.prepend($('<td>').html(' ')));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* The renderer needs to support reordering lines. This is only active in
|
||||
* edit mode. The handleField attribute is set when there is a sequence
|
||||
@@ -618,8 +776,9 @@ ListRenderer.include({
|
||||
_renderView: function () {
|
||||
var self = this;
|
||||
this.currentRow = null;
|
||||
this.allRecordsIds = null;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (self._isEditable()) {
|
||||
if (self.editable) {
|
||||
self.$('table').addClass('o_editable_list');
|
||||
}
|
||||
});
|
||||
@@ -635,7 +794,7 @@ ListRenderer.include({
|
||||
var self = this;
|
||||
var movedRecordID = ui.item.data('id');
|
||||
var rows = this.state.data;
|
||||
var row = _.findWhere(rows, {id: movedRecordID});
|
||||
var row = self._getRecord(movedRecordID);
|
||||
var index0 = rows.indexOf(row);
|
||||
var index1 = ui.item.index();
|
||||
var lower = Math.min(index0, index1);
|
||||
@@ -714,7 +873,8 @@ ListRenderer.include({
|
||||
// Select the row then activate the widget in the correct cell
|
||||
var self = this;
|
||||
return this._selectRow(rowIndex).then(function () {
|
||||
var record = self.state.data[rowIndex];
|
||||
var recordID = self._getRecordID(rowIndex);
|
||||
var record = self._getRecord(recordID);
|
||||
if (fieldIndex >= (self.allFieldWidgets[record.id] || []).length) {
|
||||
return Promise.reject();
|
||||
}
|
||||
@@ -746,11 +906,11 @@ ListRenderer.include({
|
||||
if (rowIndex === this.currentRow) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var recordId = this._getRecordID(rowIndex);
|
||||
// To select a row, the currently selected one must be unselected first
|
||||
var self = this;
|
||||
return this.unselectRow().then(function () {
|
||||
if (self.state.data.length <= rowIndex) {
|
||||
if (!recordId) {
|
||||
// The row to selected doesn't exist anymore (probably because
|
||||
// an onchange triggered when unselecting the previous one
|
||||
// removes rows)
|
||||
@@ -759,7 +919,7 @@ ListRenderer.include({
|
||||
// Notify the controller we want to make a record editable
|
||||
return new Promise(function (resolve) {
|
||||
self.trigger_up('edit_line', {
|
||||
index: rowIndex,
|
||||
recordId: recordId,
|
||||
onSuccess: resolve,
|
||||
});
|
||||
});
|
||||
@@ -770,10 +930,31 @@ ListRenderer.include({
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This method is called when we click on the 'Add a line' button in a groupby
|
||||
* list view.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddRecordToGroup: function (ev) {
|
||||
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
|
||||
ev.stopPropagation();
|
||||
|
||||
var self = this;
|
||||
var groupId = $(ev.target).data('groupID');
|
||||
this.unselectRow().then(function () {
|
||||
self.trigger_up('add_record', {
|
||||
groupId: groupId,
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onAddRecord: function (ev) {
|
||||
@@ -799,12 +980,12 @@ ListRenderer.include({
|
||||
_onCellClick: function (event) {
|
||||
// The special_click property explicitely allow events to bubble all
|
||||
// the way up to bootstrap's level rather than being stopped earlier.
|
||||
if (!this._isEditable() || $(event.target).prop('special_click')) {
|
||||
if (!this.editable || $(event.target).prop('special_click')) {
|
||||
return;
|
||||
}
|
||||
var $td = $(event.currentTarget);
|
||||
var $tr = $td.parent();
|
||||
var rowIndex = this.$('.o_data_row').index($tr);
|
||||
var rowIndex = $tr.prop('rowIndex') - 1;
|
||||
var fieldIndex = Math.max($tr.find('.o_data_cell').not('.o_list_button').index($td), 0);
|
||||
this._selectCell(rowIndex, fieldIndex, {event: event});
|
||||
},
|
||||
@@ -822,11 +1003,11 @@ ListRenderer.include({
|
||||
_onFooterClick: function () {
|
||||
this.unselectRow();
|
||||
},
|
||||
/**
|
||||
* @param {KeyDownEvent} e
|
||||
* @private
|
||||
*/
|
||||
_onKeyDownAddRecord: function(e) {
|
||||
/**
|
||||
* @private
|
||||
* @param {KeyDownEvent} e
|
||||
*/
|
||||
_onKeyDownAddRecord: function (e) {
|
||||
switch(e.keyCode) {
|
||||
case $.ui.keyCode.ENTER:
|
||||
e.stopPropagation();
|
||||
@@ -842,8 +1023,8 @@ ListRenderer.include({
|
||||
* @returns {Class} Widget returns last widget
|
||||
*/
|
||||
_getLastWidget: function () {
|
||||
var record = this.state.data[this.currentRow];
|
||||
var recordWidgets = this.allFieldWidgets[record.id];
|
||||
var recordID = this._getRecordID(this.currentRow);
|
||||
var recordWidgets = this.allFieldWidgets[recordID];
|
||||
var lastWidget = _.chain(recordWidgets).filter(function (widget) {
|
||||
var isLast =
|
||||
widget.$el.is(':visible') &&
|
||||
@@ -856,7 +1037,6 @@ ListRenderer.include({
|
||||
}).last().value();
|
||||
return lastWidget;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the keyboard navigation according to events triggered by field
|
||||
* widgets.
|
||||
@@ -885,23 +1065,10 @@ ListRenderer.include({
|
||||
}
|
||||
break;
|
||||
case 'next':
|
||||
// When navigating with the keyboard, we want to get out of the list editable if the
|
||||
// entire line is left unmodified and we are on the next line.
|
||||
var column = this.columns[this.currentFieldIndex];
|
||||
var lastWidget = this._getLastWidget();
|
||||
if (column.attrs.name === lastWidget.name) {
|
||||
if (this.currentRow + 1 < this.state.data.length) {
|
||||
this._selectCell(this.currentRow+1, 0, {wrap:false})
|
||||
.guardedCatch(this._moveToNextLine.bind(this));
|
||||
} else {
|
||||
var currentRowData = this.state.data[this.currentRow];
|
||||
if (currentRowData.isDirty(currentRowData.id)) {
|
||||
this._moveToNextLine();
|
||||
}
|
||||
else {
|
||||
this.trigger_up('activate_next_widget');
|
||||
}
|
||||
}
|
||||
this._moveToNextLine();
|
||||
} else {
|
||||
if (this.currentFieldIndex + 1 < this.columns.length) {
|
||||
this._selectCell(this.currentRow, this.currentFieldIndex + 1, {wrap: false})
|
||||
@@ -912,7 +1079,7 @@ ListRenderer.include({
|
||||
}
|
||||
break;
|
||||
case 'next_line':
|
||||
this._moveToNextLine();
|
||||
this._moveToNextLine({forceCreate: true});
|
||||
break;
|
||||
case 'cancel':
|
||||
// stop the original event (typically an ESCAPE keydown), to
|
||||
@@ -922,7 +1089,7 @@ ListRenderer.include({
|
||||
this.trigger_up('discard_changes', {
|
||||
recordID: ev.target.dataPointID,
|
||||
onSuccess: function () {
|
||||
self.$('.o_field_x2many_list_row_add a:first').focus();
|
||||
self.$('.o_field_x2many_list_row_add a:first').focus(); // FIXME
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -955,7 +1122,7 @@ ListRenderer.include({
|
||||
* @private
|
||||
*/
|
||||
_onRowClicked: function () {
|
||||
if (!this._isEditable()) {
|
||||
if (!this.editable) {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -471,6 +471,27 @@ var ListRenderer = BasicRenderer.extend({
|
||||
.append($th)
|
||||
.append($cells);
|
||||
},
|
||||
/**
|
||||
* Render the content of a given opened group.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} group
|
||||
* @param {integer} groupLevel the nesting level (0 for root groups)
|
||||
* @returns {jQueryElement} a <tr> element
|
||||
*/
|
||||
_renderGroup: function (group, groupLevel) {
|
||||
var self = this;
|
||||
if (group.groupedBy.length) {
|
||||
// the opened group contains subgroups
|
||||
return this._renderGroups(group.data, groupLevel + 1);
|
||||
} else {
|
||||
// the opened group contains records
|
||||
var $records = _.map(group.data, function (record) {
|
||||
return self._renderRow(record);
|
||||
});
|
||||
return [$('<tbody>').append($records)];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Render all groups in the view. We assume that the view is in grouped
|
||||
* mode.
|
||||
@@ -495,17 +516,7 @@ var ListRenderer = BasicRenderer.extend({
|
||||
$tbody.append(self._renderGroupRow(group, groupLevel));
|
||||
if (group.data.length) {
|
||||
result.push($tbody);
|
||||
// render an opened group
|
||||
if (group.groupedBy.length) {
|
||||
// the opened group contains subgroups
|
||||
result = result.concat(self._renderGroups(group.data, groupLevel + 1));
|
||||
} else {
|
||||
// the opened group contains records
|
||||
var $records = _.map(group.data, function (record) {
|
||||
return self._renderRow(record);
|
||||
});
|
||||
result.push($('<tbody>').append($records));
|
||||
}
|
||||
result = result.concat(self._renderGroup(group, groupLevel));
|
||||
$tbody = null;
|
||||
}
|
||||
});
|
||||
@@ -594,7 +605,7 @@ var ListRenderer = BasicRenderer.extend({
|
||||
});
|
||||
|
||||
var $tr = $('<tr/>', { class: 'o_data_row' })
|
||||
.data('id', record.id)
|
||||
.attr('data-id', record.id)
|
||||
.append($cells);
|
||||
if (this.hasSelectors) {
|
||||
$tr.prepend(this._renderSelector('td', !record.res_id));
|
||||
|
||||
@@ -50,6 +50,8 @@ var ListView = BasicView.extend({
|
||||
'hasSelectors' in params ? params.hasSelectors : true;
|
||||
this.rendererParams.editable = params.readonly ? false : this.arch.attrs.editable;
|
||||
this.rendererParams.selectedRecords = selectedRecords;
|
||||
this.rendererParams.addCreateLine = false;
|
||||
this.rendererParams.addCreateLineInGroups = this.rendererParams.editable && this.controllerParams.activeActions.create;
|
||||
|
||||
this.loadParams.limit = this.loadParams.limit || 80;
|
||||
this.loadParams.type = 'list';
|
||||
|
||||
@@ -19,10 +19,15 @@ var viewUtils = {
|
||||
case 'many2one':
|
||||
return group.res_id || false;
|
||||
case 'selection':
|
||||
var descriptor = _.findWhere(groupedByField.selection, group.value);
|
||||
var descriptor = _.find(groupedByField.selection, function (option) {
|
||||
return option[1] === group.value;
|
||||
});
|
||||
return descriptor && descriptor[0];
|
||||
default:
|
||||
case 'char':
|
||||
case 'boolean':
|
||||
return group.value;
|
||||
default:
|
||||
return false; // other field types are not handled
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -3022,11 +3022,11 @@ QUnit.module('fields', {}, function () {
|
||||
},
|
||||
});
|
||||
|
||||
// edit mode, then click on Add an item, then click elsewhere
|
||||
// edit mode, then click on Add an item, then press enter
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'enter');
|
||||
assert.hasClass(form.$('input[name="turtle_foo"]'),'o_field_invalid',
|
||||
assert.hasClass(form.$('input[name="turtle_foo"]'), 'o_field_invalid',
|
||||
"input should be marked invalid");
|
||||
assert.verifySteps(['read', 'default_get']);
|
||||
form.destroy();
|
||||
|
||||
@@ -1722,7 +1722,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('list view, editable, without data', async function (assert) {
|
||||
assert.expect(13);
|
||||
assert.expect(12);
|
||||
|
||||
this.data.foo.records = [];
|
||||
|
||||
@@ -1763,10 +1763,8 @@ QUnit.module('Views', {
|
||||
assert.containsNone(list, '.o_view_nocontent',
|
||||
"should not have a no content helper displayed");
|
||||
assert.containsOnce(list, 'table', "should have rendered a table");
|
||||
assert.strictEqual(list.$('.o_content').css('height'), list.$('div.table-responsive').css('height'),
|
||||
"the div for the table should take the full height");
|
||||
|
||||
assert.hasClass(list.$('tbody tr:eq(0)'),'o_selected_row',
|
||||
assert.hasClass(list.$('tbody tr:eq(0)'), 'o_selected_row',
|
||||
"the date field td should be in edit mode");
|
||||
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
|
||||
"the date field td should not have any content");
|
||||
@@ -3086,100 +3084,6 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('grouped list are not editable (ungrouped first)', async function (assert) {
|
||||
// Editable grouped list views are not supported, so the purpose of this
|
||||
// test is to check that when a list view is grouped, its editable
|
||||
// attribute is ignored
|
||||
// In this test, the view isn't grouped at the beginning, so it is first
|
||||
// editable, and then it is reloaded with a groupBy and is no longer
|
||||
// editable
|
||||
assert.expect(5);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
intercepts: {
|
||||
switch_view: function (event) {
|
||||
var resID = event.data.res_id || false;
|
||||
assert.step('switch view ' + event.data.view_type + ' ' + resID);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
assert.verifySteps([], 'no switch view should have been requested');
|
||||
assert.containsOnce(list, '.o_selected_row',
|
||||
"a row should be in edition");
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
|
||||
|
||||
// reload with groupBy
|
||||
await list.reload({groupBy: ['bar']});
|
||||
|
||||
// clicking on record should open the form view
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
|
||||
// clicking on create button should open the form view
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
|
||||
assert.verifySteps(['switch view form 1', 'switch view form false'],
|
||||
'two switch view to form should have been requested');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('grouped list are not editable (grouped first)', async function (assert) {
|
||||
// Editable grouped list views are not supported, so the purpose of this
|
||||
// test is to check that when a list view is grouped, its editable
|
||||
// attribute is ignored
|
||||
// In this test, the view is grouped at the beginning, so it isn't
|
||||
// editable, and then it is reloaded with no groupBy and becomes editable
|
||||
assert.expect(6);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
intercepts: {
|
||||
switch_view: function (event) {
|
||||
var resID = event.data.res_id || false;
|
||||
assert.step('switch view ' + event.data.view_type + ' ' + resID);
|
||||
},
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// the view being grouped, it is not editable, so clicking on a record
|
||||
// should open the form view
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
|
||||
// for the same reason, clicking on 'Create' should open the form view
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
|
||||
|
||||
assert.verifySteps(['switch view form 1', 'switch view form false'],
|
||||
"two switch view to form should have been requested");
|
||||
|
||||
// reload without groupBy
|
||||
await list.reload({groupBy: []});
|
||||
|
||||
// as the view is no longer grouped, it is editable, so clicking on a
|
||||
// row should switch it in edition
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
|
||||
assert.verifySteps([], "no more switch view should have been requested");
|
||||
assert.containsOnce(list, '.o_selected_row', "a row should be in edition");
|
||||
|
||||
// clicking on the body should leave the edition
|
||||
await testUtils.dom.click($('body'));
|
||||
assert.containsNone(list, '.o_selected_row',
|
||||
"the row should no longer be in edition");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('field values are escaped', async function (assert) {
|
||||
assert.expect(1);
|
||||
var value = '<script>throw Error();</script>';
|
||||
@@ -4160,6 +4064,857 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
delete widgetRegistry.map.asyncWidget;
|
||||
});
|
||||
|
||||
QUnit.test('editable grouped lists', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
|
||||
// enter edition (grouped case)
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
assert.containsOnce(list, '.o_selected_row .o_data_cell:first');
|
||||
|
||||
// click on the body should leave the edition
|
||||
await testUtils.dom.click($('body'));
|
||||
assert.containsNone(list, '.o_selected_row');
|
||||
|
||||
// reload without groupBy
|
||||
await list.reload({groupBy: []});
|
||||
|
||||
// enter edition (ungrouped case)
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
assert.containsOnce(list, '.o_selected_row .o_data_cell:first');
|
||||
|
||||
// click on the body should leave the edition
|
||||
await testUtils.dom.click($('body'));
|
||||
assert.containsNone(list, '.o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('grouped lists are editable (ungrouped first)', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
});
|
||||
|
||||
// enter edition (ungrouped case)
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
assert.containsOnce(list, '.o_selected_row .o_data_cell:first');
|
||||
|
||||
// reload with groupBy
|
||||
await list.reload({groupBy: ['bar']});
|
||||
|
||||
// open first group
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
|
||||
// enter edition (grouped case)
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
assert.containsOnce(list, '.o_selected_row .o_data_cell:first');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('char field edition in editable grouped list', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
await testUtils.fields.editAndTrigger(list.$('tr.o_selected_row .o_data_cell:first input[name="foo"]'), 'pla', 'input');
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
|
||||
|
||||
assert.strictEqual(this.data.foo.records[0].foo, 'pla',
|
||||
"the edition should have been properly saved");
|
||||
assert.containsOnce(list, '.o_data_row:first:contains(pla)');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('control panel buttons in editable grouped list views', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
assert.isNotVisible(list.$buttons.find('.o_list_button_add'));
|
||||
|
||||
// reload without groupBy
|
||||
await list.reload({groupBy: []});
|
||||
assert.isVisible(list.$buttons.find('.o_list_button_add'));
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('edit a line and discard it in grouped editable', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="int_field"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(2) > td:contains(gnap)'));
|
||||
assert.ok(list.$('.o_data_row:nth(2)').is('.o_selected_row'),
|
||||
"third group row should be in edition");
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(0) > td:contains(yop)'));
|
||||
assert.ok(list.$('.o_data_row:eq(0)').is('.o_selected_row'),
|
||||
"first group row should be in edition");
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
|
||||
assert.containsNone(list, '.o_selected_row');
|
||||
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(2) > td:contains(gnap)'));
|
||||
assert.containsOnce(list, '.o_selected_row');
|
||||
assert.ok(list.$('.o_data_row:nth(2)').is('.o_selected_row'),
|
||||
"third group row should be in edition");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add and discard a record in a multi-level grouped list view', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
testUtils.mock.patch(basicFields.FieldChar, {
|
||||
destroy: function () {
|
||||
assert.step('destroy');
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['foo', 'bar'],
|
||||
});
|
||||
|
||||
// unfold first subgroup
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)'));
|
||||
assert.hasClass(list.$('.o_group_header:first'), 'o_group_open');
|
||||
assert.hasClass(list.$('.o_group_header:eq(1)'), 'o_group_open');
|
||||
assert.containsOnce(list, '.o_data_row');
|
||||
|
||||
// add a record to first subgroup
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a'));
|
||||
assert.containsN(list, '.o_data_row', 2);
|
||||
|
||||
// discard
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
|
||||
assert.containsOnce(list, '.o_data_row');
|
||||
|
||||
assert.verifySteps(['destroy']);
|
||||
|
||||
testUtils.mock.unpatch(basicFields.FieldChar);
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('inputs are disabled when unselecting rows in grouped editable', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $input;
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'write') {
|
||||
assert.strictEqual($input.prop('disabled'), true,
|
||||
"input should be disabled");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
await testUtils.dom.click(list.$('td:contains(yop)'));
|
||||
$input = list.$('tr.o_selected_row input[name="foo"]');
|
||||
await testUtils.fields.editAndTrigger($input, 'lemon', 'input');
|
||||
await testUtils.fields.triggerKeydown($input, 'tab');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing ESC in editable grouped list should discard the current line changes', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
assert.containsN(list, 'tr.o_data_row', 3);
|
||||
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
|
||||
// update name by "foo"
|
||||
await testUtils.fields.editAndTrigger(list.$('tr.o_selected_row .o_data_cell:first input[name="foo"]'), 'new_value', 'input');
|
||||
// discard by pressing ESC
|
||||
await testUtils.fields.triggerKeydown(list.$('input[name="foo"]'), 'escape');
|
||||
await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
|
||||
|
||||
assert.containsOnce(list, 'tbody tr td:contains(yop)');
|
||||
assert.containsN(list, 'tr.o_data_row', 3);
|
||||
assert.containsNone(list, 'tr.o_data_row.o_selected_row');
|
||||
assert.isNotVisible(list.$buttons.find('.o_list_button_save'));
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing TAB in editable="bottom" grouped list', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// open two groups
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)'));
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to next line (still in first group)
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to next line (still in first group)
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to first line of next group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(3)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go back to first line of first group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing TAB in editable="top" grouped list', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// open two groups
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)'));
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
|
||||
assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to next line (still in first group)
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to next line (still in first group)
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to first line of next group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(3)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go back to first line of first group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing TAB in editable grouped list with create=0', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom" create="0"><field name="foo"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// open two groups
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)'));
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
await testUtils.dom.click(list.$('.o_data_cell:first'));
|
||||
|
||||
assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to next line (still in first group)
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to next line (still in first group)
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go to first line of next group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:nth(3)'), 'o_selected_row');
|
||||
|
||||
// Press 'Tab' -> should go back to first line of first group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing SHIFT-TAB in editable="bottom" grouped list', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
// navigate inside a group
|
||||
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell')); // select second row of first group
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press Shft+tab
|
||||
list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
await testUtils.nextTick();
|
||||
assert.hasClass(list.$('tr.o_data_row:first'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// navigate between groups
|
||||
await testUtils.dom.click(list.$('.o_data_cell:eq(3)')); // select row of second group
|
||||
|
||||
// press Shft+tab
|
||||
list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
await testUtils.nextTick();
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing SHIFT-TAB in editable="top" grouped list', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
// navigate inside a group
|
||||
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell')); // select second row of first group
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press Shft+tab
|
||||
list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
await testUtils.nextTick();
|
||||
assert.hasClass(list.$('tr.o_data_row:first'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// navigate between groups
|
||||
await testUtils.dom.click(list.$('.o_data_cell:eq(3)')); // select row of second group
|
||||
|
||||
// press Shft+tab
|
||||
list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
await testUtils.nextTick();
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing SHIFT-TAB in editable grouped list with create="0"', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top" create="0"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
// navigate inside a group
|
||||
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell')); // select second row of first group
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press Shft+tab
|
||||
list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
await testUtils.nextTick();
|
||||
assert.hasClass(list.$('tr.o_data_row:first'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// navigate between groups
|
||||
await testUtils.dom.click(list.$('.o_data_cell:eq(3)')); // select row of second group
|
||||
|
||||
// press Shft+tab
|
||||
list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
await testUtils.nextTick();
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editing then pressing TAB in editable grouped list', async function (assert) {
|
||||
assert.expect(19);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
// open two groups
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
assert.containsN(list, '.o_data_row', 3, 'first group contains 3 rows');
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)'));
|
||||
assert.containsN(list, '.o_data_row', 4, 'first group contains 1 row');
|
||||
|
||||
// select and edit last row of first group
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(2) .o_data_cell'));
|
||||
assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
|
||||
await testUtils.fields.editInput(list.$('.o_selected_row input[name="foo"]'), 'new value');
|
||||
|
||||
// Press 'Tab' -> should create a new record as we edited the previous one
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.containsN(list, '.o_data_row', 5);
|
||||
assert.hasClass(list.$('.o_data_row:nth(3)'), 'o_selected_row');
|
||||
|
||||
// fill foo field for the new record and press 'tab' -> should create another record
|
||||
await testUtils.fields.editInput(list.$('.o_selected_row input[name="foo"]'), 'new record');
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
|
||||
assert.containsN(list, '.o_data_row', 6);
|
||||
assert.hasClass(list.$('.o_data_row:nth(4)'), 'o_selected_row');
|
||||
|
||||
// leave this new row empty and press tab -> should discard the new record and move to the
|
||||
// next group
|
||||
await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
|
||||
assert.containsN(list, '.o_data_row', 5);
|
||||
assert.hasClass(list.$('.o_data_row:nth(4)'), 'o_selected_row');
|
||||
|
||||
assert.verifySteps([
|
||||
'read_group',
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/search_read',
|
||||
'write',
|
||||
'read',
|
||||
'default_get',
|
||||
'create',
|
||||
'read',
|
||||
'default_get',
|
||||
]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editing then pressing TAB (with a readonly field) in grouped list', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/><field name="int_field" readonly="1"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
fieldDebounce: 1
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
// click on first td and press TAB
|
||||
await testUtils.dom.click(list.$('td:contains(yop)'));
|
||||
await testUtils.fields.editAndTrigger(list.$('tr.o_selected_row input[name="foo"]'), 'new value', 'input');
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input[name="foo"]'), 'tab');
|
||||
|
||||
assert.containsOnce(list, 'tbody tr td:contains(new value)');
|
||||
assert.verifySteps([
|
||||
'read_group',
|
||||
'/web/dataset/search_read',
|
||||
'write',
|
||||
'read',
|
||||
]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing ENTER in editable="bottom" grouped list view', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)')); // open second group
|
||||
assert.containsN(list, 'tr.o_data_row', 4);
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(1) .o_data_cell')); // click on second line
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press enter in input should move to next record
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input'), 'enter');
|
||||
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press enter on last row should create a new record
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input'), 'enter');
|
||||
|
||||
assert.containsN(list, 'tr.o_data_row', 5);
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(3)'), 'o_selected_row');
|
||||
|
||||
assert.verifySteps([
|
||||
'/web/dataset/call_kw/foo/read_group',
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/search_read',
|
||||
'default_get',
|
||||
]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing ENTER in editable="top" grouped list view', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)')); // open second group
|
||||
assert.containsN(list, 'tr.o_data_row', 4);
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(1) .o_data_cell')); // click on second line
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press enter in input should move to next record
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input'), 'enter');
|
||||
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press enter on last row should move to first record of next group
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input'), 'enter');
|
||||
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(3)'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
|
||||
assert.verifySteps([
|
||||
'read_group',
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/search_read',
|
||||
]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('pressing ENTER in editable grouped list view with create=0', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom" create="0"><field name="foo"/></tree>',
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
|
||||
await testUtils.dom.click(list.$('.o_group_header:nth(1)')); // open second group
|
||||
assert.containsN(list, 'tr.o_data_row', 4);
|
||||
await testUtils.dom.click(list.$('.o_data_row:nth(1) .o_data_cell')); // click on second line
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press enter in input should move to next record
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input'), 'enter');
|
||||
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row');
|
||||
|
||||
// press enter on last row should move to first record of next group
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input'), 'enter');
|
||||
|
||||
assert.hasClass(list.$('tr.o_data_row:eq(3)'), 'o_selected_row');
|
||||
assert.doesNotHaveClass(list.$('tr.o_data_row:eq(2)'), 'o_selected_row');
|
||||
|
||||
assert.verifySteps([
|
||||
'read_group',
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/search_read',
|
||||
]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a new row in grouped editable="top" list', async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open group
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a'));// add a new row
|
||||
assert.strictEqual(list.$('.o_selected_row .o_input[name=foo]')[0], document.activeElement,
|
||||
'The first input of the line should have the focus');
|
||||
assert.containsN(list, 'tbody:nth(1) .o_data_row', 4);
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard')); // discard new row
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
assert.containsOnce(list, 'tbody:nth(3) .o_data_row');
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a:eq(1)')); // create row in second group
|
||||
assert.strictEqual(list.$('.o_group_name:eq(1)').text(), 'false (2)',
|
||||
"group should have correct name and count");
|
||||
assert.containsN(list, 'tbody:nth(3) .o_data_row', 2);
|
||||
assert.hasClass(list.$('.o_data_row:nth(3)'), 'o_selected_row');
|
||||
|
||||
await testUtils.fields.editAndTrigger(list.$('tr.o_selected_row input[name="foo"]'), 'pla', 'input');
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
|
||||
assert.containsN(list, 'tbody:nth(3) .o_data_row', 2);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a new row in grouped editable="bottom" list', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="bottom"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open group
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a'));// add a new row
|
||||
assert.hasClass(list.$('.o_data_row:nth(3)'), 'o_selected_row');
|
||||
assert.containsN(list, 'tbody:nth(1) .o_data_row', 4);
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard')); // discard new row
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
assert.containsOnce(list, 'tbody:nth(3) .o_data_row');
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a:eq(1)')); // create row in second group
|
||||
assert.hasClass(list.$('.o_data_row:nth(4)'), 'o_selected_row');
|
||||
|
||||
await testUtils.fields.editAndTrigger(list.$('tr.o_selected_row input[name="foo"]'), 'pla', 'input');
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
|
||||
assert.containsN(list, 'tbody:nth(3) .o_data_row', 2);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable grouped list with create="0"', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top" create="0"><field name="foo" required="1"/></tree>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open group
|
||||
assert.containsNone(list, '.o_group_field_row_add a',
|
||||
"Add a line should not be available in readonly");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a new row in (selection) grouped editable list', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
this.data.foo.fields.priority = {
|
||||
string: "Priority",
|
||||
type: "selection",
|
||||
selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
|
||||
default: 1,
|
||||
};
|
||||
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1, priority: 2});
|
||||
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 1, priority: 3});
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top">' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="priority"/>' +
|
||||
'<field name="m2o"/>' +
|
||||
'</tree>',
|
||||
groupBy: ['priority'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
assert.step(args.kwargs.context.default_priority.toString());
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open group
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a')); // add a new row
|
||||
await testUtils.dom.click($('body')); // unselect row
|
||||
assert.verifySteps(['1']);
|
||||
assert.strictEqual(list.$('.o_data_row .o_data_cell:eq(1)').text(), 'Low',
|
||||
"should have a column name with a value from the groupby");
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a:eq(1)')); // create row in second group
|
||||
await testUtils.dom.click($('body')); // unselect row
|
||||
assert.strictEqual(list.$('.o_data_row:nth(5) .o_data_cell:eq(1)').text(), 'Medium',
|
||||
"should have a column name with a value from the groupby");
|
||||
assert.verifySteps(['2']);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('add a new row in (m2o) grouped editable list', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree editable="top">' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="m2o"/>' +
|
||||
'</tree>',
|
||||
groupBy: ['m2o'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
assert.step(args.kwargs.context.default_m2o.toString());
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first'));
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a'));
|
||||
await testUtils.dom.click($('body')); // unselect row
|
||||
assert.strictEqual(list.$('tbody:eq(1) .o_data_row:first .o_data_cell:eq(1)').text(), 'Value 1',
|
||||
"should have a column name with a value from the groupby");
|
||||
assert.verifySteps(['1']);
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:eq(1)')); // open second group
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a:eq(1)')); // create row in second group
|
||||
await testUtils.dom.click($('body')); // unselect row
|
||||
assert.strictEqual(list.$('tbody:eq(3) .o_data_row:first .o_data_cell:eq(1)').text(), 'Value 2',
|
||||
"should have a column name with a value from the groupby");
|
||||
assert.verifySteps(['2']);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
// TODO: write test on:
|
||||
// - default_get with a field not in view
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user