[IMP] web: support many2many grouping
PURPOSE
Allow to group records by many2many fields.
Use case: turn the user_id m2o field on project.task into a user_ids m2m field
in order to assign multiple collaborators on a task
SPECIFICATION
Add many2many fields to the 'list of groupable fields'.
When grouping a set of records by a many2many, records linked to multiple
records of the comodel will essentially be in multiple 'groups' (even though
those are not really 'groups' anymore).
There will also be a "null" group, for records that have an empty value of the
many2many, like "unassigned" records.
Example: grouping a project.task linked to 10 res.users by the user_ids
many2many field, the project.task will be 'rendered/considered/displayed' 10
times, once per res.users.
Kind of like if we grouped the m2m relation table instead.
When grouping by a m2m field:
- listview
editing an instance of a record also updates each other instance(if any)
- the same goes when scheduling an activity, etc.
disable deletion
- the risk here is that the user might expected that this will 'unlike'
the record from a specific record of the many2many field
disable archive/unarchive
- kanban
disable drag&drop between groups
disable deletion
disable archive/unarchive
- searchview
display many2many fields custom groupby dropdown
With this commit, we also support groupby many2many field in mockServer so that
test cases are written based on that.
task-2508608
closes odoo/odoo#74985
Related: odoo/enterprise#20192
Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com>
This commit is contained in:
@@ -57,12 +57,14 @@ odoo.define('web.GroupByMenu', function (require) {
|
|||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @see {@link odoo/fields.py} Field._description_sortable
|
||||||
|
* @see {@link odoo/fields.py} Many2Many.groupable
|
||||||
* @private
|
* @private
|
||||||
* @param {Object} field
|
* @param {Object} field
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
_validateField(field) {
|
_validateField(field) {
|
||||||
return field.sortable &&
|
return (field.sortable || (field.type === "many2many" && field.store)) &&
|
||||||
field.name !== "id" &&
|
field.name !== "id" &&
|
||||||
GROUPABLE_TYPES.includes(field.type);
|
GROUPABLE_TYPES.includes(field.type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ odoo.define('web.searchUtils', function (require) {
|
|||||||
'datetime',
|
'datetime',
|
||||||
'integer',
|
'integer',
|
||||||
'many2one',
|
'many2one',
|
||||||
|
'many2many',
|
||||||
'selection',
|
'selection',
|
||||||
];
|
];
|
||||||
const DEFAULT_INTERVAL = 'month';
|
const DEFAULT_INTERVAL = 'month';
|
||||||
|
|||||||
@@ -647,6 +647,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
groupsOffset: element.groupsOffset,
|
groupsOffset: element.groupsOffset,
|
||||||
id: element.id,
|
id: element.id,
|
||||||
isDirty: element.isDirty,
|
isDirty: element.isDirty,
|
||||||
|
isM2MGrouped: element.isM2MGrouped,
|
||||||
isNew: element.isNew,
|
isNew: element.isNew,
|
||||||
isOpen: element.isOpen,
|
isOpen: element.isOpen,
|
||||||
isSample: this.isSampleModel,
|
isSample: this.isSampleModel,
|
||||||
@@ -1205,6 +1206,9 @@ var BasicModel = AbstractModel.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
prom.then(function () {
|
prom.then(function () {
|
||||||
|
self._updateDuplicateRecords(record.id, (id) => {
|
||||||
|
Object.assign(self.localData[id].data, record.data);
|
||||||
|
});
|
||||||
record._isDirty = false;
|
record._isDirty = false;
|
||||||
});
|
});
|
||||||
return prom;
|
return prom;
|
||||||
@@ -4098,6 +4102,13 @@ var BasicModel = AbstractModel.extend({
|
|||||||
var res_ids = params.res_ids || [];
|
var res_ids = params.res_ids || [];
|
||||||
var data = params.data || (type === 'record' ? {} : []);
|
var data = params.data || (type === 'record' ? {} : []);
|
||||||
var context = params.context;
|
var context = params.context;
|
||||||
|
var fields = _.extend({
|
||||||
|
display_name: {type: 'char'},
|
||||||
|
id: {type: 'integer'},
|
||||||
|
}, params.fields);
|
||||||
|
|
||||||
|
const groupedBy = params.groupedBy || [];
|
||||||
|
let isM2MGrouped = false;
|
||||||
if (type === 'record') {
|
if (type === 'record') {
|
||||||
res_id = params.res_id || (params.data && params.data.id);
|
res_id = params.res_id || (params.data && params.data.id);
|
||||||
if (res_id) {
|
if (res_id) {
|
||||||
@@ -4112,13 +4123,12 @@ var BasicModel = AbstractModel.extend({
|
|||||||
var isValueArray = params.value instanceof Array;
|
var isValueArray = params.value instanceof Array;
|
||||||
res_id = isValueArray ? params.value[0] : undefined;
|
res_id = isValueArray ? params.value[0] : undefined;
|
||||||
value = isValueArray ? params.value[1] : params.value;
|
value = isValueArray ? params.value[1] : params.value;
|
||||||
|
isM2MGrouped = groupedBy.some((group) => {
|
||||||
|
const [fieldName] = group.split(':');
|
||||||
|
return fields[fieldName].type === "many2many";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = _.extend({
|
|
||||||
display_name: {type: 'char'},
|
|
||||||
id: {type: 'integer'},
|
|
||||||
}, params.fields);
|
|
||||||
|
|
||||||
var dataPoint = {
|
var dataPoint = {
|
||||||
_cache: type === 'list' ? {} : undefined,
|
_cache: type === 'list' ? {} : undefined,
|
||||||
_changes: null,
|
_changes: null,
|
||||||
@@ -4131,11 +4141,12 @@ var BasicModel = AbstractModel.extend({
|
|||||||
domain: params.domain || [],
|
domain: params.domain || [],
|
||||||
fields: fields,
|
fields: fields,
|
||||||
fieldsInfo: params.fieldsInfo,
|
fieldsInfo: params.fieldsInfo,
|
||||||
groupedBy: params.groupedBy || [],
|
groupedBy,
|
||||||
groupsCount: 0,
|
groupsCount: 0,
|
||||||
groupsLimit: type === 'list' && params.groupsLimit || null,
|
groupsLimit: type === 'list' && params.groupsLimit || null,
|
||||||
groupsOffset: 0,
|
groupsOffset: 0,
|
||||||
id: `${params.modelName}_${++this.__id}`,
|
id: `${params.modelName}_${++this.__id}`,
|
||||||
|
isM2MGrouped,
|
||||||
isOpen: params.isOpen,
|
isOpen: params.isOpen,
|
||||||
limit: type === 'record' ? 1 : (params.limit || Number.MAX_SAFE_INTEGER),
|
limit: type === 'record' ? 1 : (params.limit || Number.MAX_SAFE_INTEGER),
|
||||||
loadMoreOffset: 0,
|
loadMoreOffset: 0,
|
||||||
@@ -4959,6 +4970,10 @@ var BasicModel = AbstractModel.extend({
|
|||||||
}
|
}
|
||||||
if (options.groupBy !== undefined) {
|
if (options.groupBy !== undefined) {
|
||||||
element.groupedBy = options.groupBy;
|
element.groupedBy = options.groupBy;
|
||||||
|
element.isM2MGrouped = element.groupedBy.some((group) => {
|
||||||
|
const [fieldName] = group.split(':');
|
||||||
|
return element.fields[fieldName].type === "many2many";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (options.limit !== undefined) {
|
if (options.limit !== undefined) {
|
||||||
element.limit = options.limit;
|
element.limit = options.limit;
|
||||||
@@ -5274,6 +5289,40 @@ var BasicModel = AbstractModel.extend({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Used to apply a updateFn to other records with same res_id, that are
|
||||||
|
* under the same topmost list groupedby many2many.
|
||||||
|
*
|
||||||
|
* When a list datapoint is grouped by many2many, same res_id record could
|
||||||
|
* be in multiple groups, so use this method to apply the same callback to
|
||||||
|
* each one of them.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @param {string} recordID local resource
|
||||||
|
* @param {(recordID: string)=>void} updateFn called for each record datapoint
|
||||||
|
* representing the same res_id record
|
||||||
|
*/
|
||||||
|
_updateDuplicateRecords(recordID, updateFn) {
|
||||||
|
const { model, res_id } = this.localData[recordID];
|
||||||
|
// Get the topmost groupedby m2m list
|
||||||
|
const getTopmostID = () => {
|
||||||
|
let element = this.localData[recordID];
|
||||||
|
while (element.parentID && this.localData[element.parentID].type === "list") {
|
||||||
|
element = this.localData[element.parentID];
|
||||||
|
}
|
||||||
|
return element.type === "list" && element.isM2MGrouped ? element.id : false;
|
||||||
|
};
|
||||||
|
const topmostID = getTopmostID();
|
||||||
|
if (!topmostID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const topmostList = this.get(topmostID);
|
||||||
|
utils.traverse_records(topmostList, (r) => {
|
||||||
|
if (r.res_id === res_id && r.id !== recordID) {
|
||||||
|
updateFn(r.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Helper method. Recursively traverses the data, starting from the element
|
* Helper method. Recursively traverses the data, starting from the element
|
||||||
* record (or list), then following all relations. This is useful when one
|
* record (or list), then following all relations. This is useful when one
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ var KanbanColumn = Widget.extend({
|
|||||||
this.quickCreateView = options.quickCreateView;
|
this.quickCreateView = options.quickCreateView;
|
||||||
this.groupedBy = options.groupedBy;
|
this.groupedBy = options.groupedBy;
|
||||||
this.grouped_by_m2o = options.grouped_by_m2o;
|
this.grouped_by_m2o = options.grouped_by_m2o;
|
||||||
|
this.grouped_by_m2m = options.grouped_by_m2m;
|
||||||
this.editable = options.editable;
|
this.editable = options.editable;
|
||||||
this.deletable = options.deletable;
|
this.deletable = options.deletable;
|
||||||
this.archivable = options.archivable;
|
this.archivable = options.archivable;
|
||||||
@@ -77,8 +78,8 @@ var KanbanColumn = Widget.extend({
|
|||||||
|
|
||||||
this.record_options = _.clone(recordOptions);
|
this.record_options = _.clone(recordOptions);
|
||||||
|
|
||||||
if (options.grouped_by_m2o || options.grouped_by_date ) {
|
if (options.grouped_by_m2o || options.grouped_by_date || options.grouped_by_m2m) {
|
||||||
// For many2one and datetime, a false value means that the field is not set.
|
// For many2x and datetime, a false value means that the field is not set.
|
||||||
this.title = value ? value : _t('Undefined');
|
this.title = value ? value : _t('Undefined');
|
||||||
} else {
|
} else {
|
||||||
// False and 0 might be valid values for these fields.
|
// False and 0 might be valid values for these fields.
|
||||||
@@ -250,6 +251,9 @@ var KanbanColumn = Widget.extend({
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
_addRecord: function (recordState, options) {
|
_addRecord: function (recordState, options) {
|
||||||
|
if (this.grouped_by_m2m) {
|
||||||
|
this.record_options.deletable = false;
|
||||||
|
}
|
||||||
var record = new this.KanbanRecord(this, recordState, this.record_options);
|
var record = new this.KanbanRecord(this, recordState, this.record_options);
|
||||||
this.records.push(record);
|
this.records.push(record);
|
||||||
if (options && options.position === 'before') {
|
if (options && options.position === 'before') {
|
||||||
|
|||||||
@@ -491,51 +491,42 @@ var KanbanRenderer = BasicRenderer.extend({
|
|||||||
*
|
*
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
_setState: function () {
|
_setState() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
const groupedBy = this.state.groupedBy[0];
|
||||||
|
const groupByFieldName = viewUtils.getGroupByField(groupedBy);
|
||||||
|
const field = this.state.fields[groupByFieldName] || {};
|
||||||
|
const fieldInfo = this.state.fieldsInfo.kanban[groupByFieldName] || {};
|
||||||
|
|
||||||
var groupByField = this.state.groupedBy[0];
|
const group_by_tooltip = fieldInfo.options && fieldInfo.options.group_by_tooltip;
|
||||||
var cleanGroupByField = viewUtils.getGroupByField(groupByField);
|
const grouped_by_date = ["date", "datetime"].includes(field.type);
|
||||||
var groupByFieldAttrs = this.state.fields[cleanGroupByField];
|
const grouped_by_m2m = field.type === "many2many";
|
||||||
var groupByFieldInfo = this.state.fieldsInfo.kanban[cleanGroupByField];
|
const grouped_by_m2o = field.type === "many2one";
|
||||||
// Deactivate the drag'n'drop:
|
const readonly = !!field.readonly || !!fieldInfo.readonly;
|
||||||
|
const relation = (grouped_by_m2o || grouped_by_m2m) && field.relation;
|
||||||
|
const quick_create = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(this.state);
|
||||||
|
|
||||||
|
// Deactivate the drag'n'drop either:
|
||||||
// - if the groupedBy field is readonly (on the field attrs or in the view)
|
// - if the groupedBy field is readonly (on the field attrs or in the view)
|
||||||
|
// - if the groupedBy field is of type many2many
|
||||||
// - for date and datetime if :
|
// - for date and datetime if :
|
||||||
// - allowGroupRangeValue is not true
|
// - allowGroupRangeValue is not true
|
||||||
var readonly = false;
|
const draggable = !readonly && !grouped_by_m2m &&
|
||||||
var draggable = true;
|
(!grouped_by_date || fieldInfo.allowGroupRangeValue);
|
||||||
var grouped_by_date = false;
|
|
||||||
if (groupByFieldAttrs) {
|
Object.assign(this.columnOptions, {
|
||||||
if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
|
draggable,
|
||||||
draggable = false;
|
groupedBy,
|
||||||
grouped_by_date = true;
|
grouped_by_date,
|
||||||
}
|
grouped_by_m2o,
|
||||||
if (groupByFieldAttrs.readonly !== undefined) {
|
grouped_by_m2m,
|
||||||
readonly = groupByFieldAttrs.readonly;
|
group_by_tooltip,
|
||||||
}
|
quick_create,
|
||||||
}
|
relation,
|
||||||
if (groupByFieldInfo) {
|
|
||||||
if (grouped_by_date) {
|
|
||||||
draggable = groupByFieldInfo.allowGroupRangeValue;
|
|
||||||
}
|
|
||||||
if (!readonly && groupByFieldInfo.readonly !== undefined) {
|
|
||||||
readonly = groupByFieldInfo.readonly;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
draggable = !readonly && draggable;
|
|
||||||
this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
|
|
||||||
var relation = this.groupedByM2O && groupByFieldAttrs.relation;
|
|
||||||
var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
|
|
||||||
this.columnOptions = _.extend(this.columnOptions, {
|
|
||||||
draggable: draggable,
|
|
||||||
group_by_tooltip: groupByTooltip,
|
|
||||||
groupedBy: groupByField,
|
|
||||||
grouped_by_m2o: this.groupedByM2O,
|
|
||||||
grouped_by_date: grouped_by_date,
|
|
||||||
relation: relation,
|
|
||||||
quick_create: this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(this.state),
|
|
||||||
});
|
});
|
||||||
this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
|
|
||||||
|
this.createColumnEnabled = grouped_by_m2o && this.columnOptions.group_creatable;
|
||||||
|
this.groupedByM2O = grouped_by_m2o;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Moves the focus on the first card of the next column in a given direction
|
* Moves the focus on the first card of the next column in a given direction
|
||||||
|
|||||||
@@ -312,12 +312,14 @@ var ListController = BasicController.extend({
|
|||||||
* @override
|
* @override
|
||||||
* @param {string} id a basicmodel valid resource handle. It is supposed to
|
* @param {string} id a basicmodel valid resource handle. It is supposed to
|
||||||
* be a record from the list view.
|
* be a record from the list view.
|
||||||
|
* @param {Object} [options] render options
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
_confirmSave: function (id) {
|
_confirmSave(id, options = {}) {
|
||||||
var state = this.model.get(this.handle);
|
const state = this.model.get(this.handle);
|
||||||
return this._updateRendererState(state, { noRender: true })
|
return this._updateRendererState(state, { noRender: !state.isM2MGrouped, ...options }).then(
|
||||||
.then(this._setMode.bind(this, 'readonly', id));
|
this._setMode.bind(this, "readonly", id)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Deletes records matching the current domain. We limit the number of
|
* Deletes records matching the current domain. We limit the number of
|
||||||
@@ -423,6 +425,7 @@ var ListController = BasicController.extend({
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getActionMenuItems: function (state) {
|
_getActionMenuItems: function (state) {
|
||||||
|
const { isM2MGrouped } = state;
|
||||||
if (!this.hasActionMenus || !this.selectedRecords.length) {
|
if (!this.hasActionMenus || !this.selectedRecords.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -434,7 +437,7 @@ var ListController = BasicController.extend({
|
|||||||
callback: () => this._onExportData()
|
callback: () => this._onExportData()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.archiveEnabled) {
|
if (this.archiveEnabled && !isM2MGrouped) {
|
||||||
otherActionItems.push({
|
otherActionItems.push({
|
||||||
description: _t("Archive"),
|
description: _t("Archive"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
@@ -450,7 +453,7 @@ var ListController = BasicController.extend({
|
|||||||
callback: () => this._toggleArchiveState(false)
|
callback: () => this._toggleArchiveState(false)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.activeActions.delete) {
|
if (this.activeActions.delete && !isM2MGrouped) {
|
||||||
otherActionItems.push({
|
otherActionItems.push({
|
||||||
description: _t("Delete"),
|
description: _t("Delete"),
|
||||||
callback: () => this._onDeleteSelectedRecords()
|
callback: () => this._onDeleteSelectedRecords()
|
||||||
@@ -492,13 +495,10 @@ var ListController = BasicController.extend({
|
|||||||
this.model.saveRecords(this.handle, recordId, validRecordIds, Object.keys(changes))
|
this.model.saveRecords(this.handle, recordId, validRecordIds, Object.keys(changes))
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
this.updateButtons('readonly');
|
this.updateButtons('readonly');
|
||||||
const state = this.model.get(this.handle);
|
// If there are changes and the list was multi-editable,
|
||||||
// We need to check the current multi-editable state here
|
// we do not want to select the next row.
|
||||||
// in case the selection is changed. If there are changes
|
|
||||||
// and the list was multi-editable, we do not want to select
|
|
||||||
// the next row.
|
|
||||||
this.selectedRecords = [];
|
this.selectedRecords = [];
|
||||||
await this._updateRendererState(state, {
|
await this._confirmSave(this.handle, {
|
||||||
keepWidths: true,
|
keepWidths: true,
|
||||||
selectedRecords: [],
|
selectedRecords: [],
|
||||||
});
|
});
|
||||||
@@ -910,6 +910,19 @@ var ListController = BasicController.extend({
|
|||||||
this._enableButtons();
|
this._enableButtons();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Overridden to always reload the main record when grouped by M2M.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_onReload(ev) {
|
||||||
|
const { isM2MGrouped } = this.model.get(this.handle);
|
||||||
|
if (isM2MGrouped) {
|
||||||
|
// Ask for the main record to be reloaded.
|
||||||
|
ev.data.db_id = this.handle;
|
||||||
|
}
|
||||||
|
this._super(...arguments);
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Called when the renderer displays an editable row and the user tries to
|
* Called when the renderer displays an editable row and the user tries to
|
||||||
* leave it -> Saves the record associated to that line.
|
* leave it -> Saves the record associated to that line.
|
||||||
|
|||||||
@@ -73,12 +73,20 @@
|
|||||||
context: context,
|
context: context,
|
||||||
});
|
});
|
||||||
}).then(function (results) {
|
}).then(function (results) {
|
||||||
results.forEach(function (data) {
|
const updateLocalRecord = (id, data) => {
|
||||||
var record = _.findWhere(records, {res_id: data.id});
|
const record = self.localData[id];
|
||||||
record.data = _.extend({}, record.data, data);
|
record.data = _.extend({}, record.data, data);
|
||||||
record._changes = {};
|
record._changes = {};
|
||||||
record._isDirty = false;
|
record._isDirty = false;
|
||||||
self._parseServerData(fieldNames, record, record.data);
|
self._parseServerData(fieldNames, record, record.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
results.forEach(function (data) {
|
||||||
|
const record = _.findWhere(records, { res_id: data.id });
|
||||||
|
updateLocalRecord(record.id, data);
|
||||||
|
|
||||||
|
// Also update same resId records
|
||||||
|
self._updateDuplicateRecords(record.id, (id) => updateLocalRecord(id, data));
|
||||||
});
|
});
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
if (!list.groupedBy.length) {
|
if (!list.groupedBy.length) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<a t-if="widget.editable and widget.id" role="menuitem" class="dropdown-item o_column_edit" href="#">Edit Stage</a>
|
<a t-if="widget.editable and widget.id" role="menuitem" class="dropdown-item o_column_edit" href="#">Edit Stage</a>
|
||||||
<a t-if="widget.deletable and widget.id" role="menuitem" class="dropdown-item o_column_delete" href="#">Delete</a>
|
<a t-if="widget.deletable and widget.id" role="menuitem" class="dropdown-item o_column_delete" href="#">Delete</a>
|
||||||
</t>
|
</t>
|
||||||
<t t-if="widget.has_active_field and widget.archivable">
|
<t t-if="widget.has_active_field and widget.archivable and !widget.grouped_by_m2m">
|
||||||
<a role="menuitem" href="#" class="dropdown-item o_column_archive_records">Archive All</a>
|
<a role="menuitem" href="#" class="dropdown-item o_column_archive_records">Archive All</a>
|
||||||
<a role="menuitem" href="#" class="dropdown-item o_column_unarchive_records">Unarchive All</a>
|
<a role="menuitem" href="#" class="dropdown-item o_column_unarchive_records">Unarchive All</a>
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
@@ -7119,7 +7119,7 @@ QUnit.module('Views', {
|
|||||||
"the kanban view should not be ungrouped");
|
"the kanban view should not be ungrouped");
|
||||||
|
|
||||||
kanban.update({domain: []}); // 1st update on kanban view
|
kanban.update({domain: []}); // 1st update on kanban view
|
||||||
kanban.update({groupBy: false}); // 2n update on kanban view
|
kanban.update({groupBy: []}); // 2n update on kanban view
|
||||||
prom.resolve(); // simulate slow 1st update of kanban view
|
prom.resolve(); // simulate slow 1st update of kanban view
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|||||||
Reference in New Issue
Block a user