flectra/addons/web/static/src/js/views/kanban/kanban_model.js

388 lines
13 KiB
JavaScript

flectra.define('web.KanbanModel', function (require) {
"use strict";
/**
* The KanbanModel extends the BasicModel to add Kanban specific features like
* moving a record from a group to another, resequencing records...
*/
var BasicModel = require('web.BasicModel');
var KanbanModel = BasicModel.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Adds a record to a group in the localData, and fetch the record.
*
* @param {string} groupID localID of the group
* @param {integer} resId id of the record
* @returns {Deferred<string>} resolves to the local id of the new record
*/
addRecordToGroup: function (groupID, resId) {
var self = this;
var group = this.localData[groupID];
var new_record = this._makeDataPoint({
res_id: resId,
modelName: group.model,
fields: group.fields,
fieldsInfo: group.fieldsInfo,
viewType: group.viewType,
parentID: groupID,
});
return this._fetchRecord(new_record).then(function (result) {
group.data.unshift(new_record.id);
group.res_ids.unshift(resId);
group.count++;
// update the res_ids and count of the parent
self.localData[group.parentID].count++;
self._updateParentResIDs(group);
return result.id;
});
},
/**
* Creates a new group from a name (performs a name_create).
*
* @param {string} name
* @param {string} parentID localID of the parent of the group
* @returns {Deferred<string>} resolves to the local id of the new group
*/
createGroup: function (name, parentID) {
var self = this;
var parent = this.localData[parentID];
var groupBy = parent.groupedBy[0];
var groupByField = parent.fields[groupBy];
if (!groupByField || groupByField.type !== 'many2one') {
return $.Deferred().reject(); // only supported when grouped on m2o
}
return this._rpc({
model: groupByField.relation,
method: 'name_create',
args: [name],
context: parent.context, // todo: combine with view context
})
.then(function (result) {
var newGroup = self._makeDataPoint({
modelName: parent.model,
context: parent.context,
domain: parent.domain.concat([[groupBy,"=",result[0]]]),
fields: parent.fields,
fieldsInfo: parent.fieldsInfo,
isOpen: true,
limit: parent.limit,
parentID: parent.id,
openGroupByDefault: true,
orderedBy: parent.orderedBy,
value: result,
viewType: parent.viewType,
});
if (parent.progressBar) {
newGroup.progressBarValues = _.extend({
counts: {},
}, parent.progressBar);
}
// newGroup.is_open = true;
parent.data.push(newGroup.id);
return newGroup.id;
});
},
/**
* Add the key `tooltipData` (kanban specific) when performing a `geŧ`.
*
* @override
* @see _readTooltipFields
* @returns {Object}
*/
get: function () {
var result = this._super.apply(this, arguments);
var dp = result && this.localData[result.id];
if (dp && dp.tooltipData) {
result.tooltipData = $.extend(true, {}, dp.tooltipData);
}
if (dp && dp.progressBarValues) {
result.progressBarValues = $.extend(true, {}, dp.progressBarValues);
}
return result;
},
/**
* Same as @see get but getting the parent element whose ID is given.
*
* @param {string} id
* @returns {Object}
*/
getColumn: function (id) {
var element = this.localData[id];
if (element) {
return this.get(element.parentID);
}
return null;
},
/**
* @override
*/
load: function (params) {
this.defaultGroupedBy = params.groupBy;
params.groupedBy = (params.groupedBy && params.groupedBy.length) ? params.groupedBy : this.defaultGroupedBy;
return this._super(params);
},
/**
* Opens a given group and loads its <limit> first records
*
* @param {string} groupID
* @returns {Deferred}
*/
loadColumnRecords: function (groupID) {
var dataPoint = this.localData[groupID];
dataPoint.isOpen = true;
return this.reload(groupID);
},
/**
* Load more records in a group.
*
* @param {string} groupID localID of the group
* @returns {Deferred<string>} resolves to the localID of the group
*/
loadMore: function (groupID) {
var group = this.localData[groupID];
var offset = group.loadMoreOffset + group.limit;
return this.reload(group.id, {
loadMoreOffset: offset,
});
},
/**
* Moves a record from a group to another.
*
* @param {string} recordID localID of the record
* @param {string} groupID localID of the new group of the record
* @param {string} parentID localID of the parent
* @returns {Deferred<string[]>} resolves to a pair [oldGroupID, newGroupID]
*/
moveRecord: function (recordID, groupID, parentID) {
var self = this;
var parent = this.localData[parentID];
var new_group = this.localData[groupID];
var changes = {};
var groupedFieldName = parent.groupedBy[0];
var groupedField = parent.fields[groupedFieldName];
if (groupedField.type === 'many2one') {
changes[groupedFieldName] = {
id: new_group.res_id,
display_name: new_group.value,
};
} else if (groupedField.type === 'selection') {
var value = _.findWhere(groupedField.selection, {1: new_group.value});
changes[groupedFieldName] = value && value[0] || false;
} else {
changes[groupedFieldName] = new_group.value;
}
// Manually updates groups data. Note: this is done before the actual
// save as it might need to perform a read group in some cases so those
// updated data might be overriden again.
var record = self.localData[recordID];
var resID = record.res_id;
// Remove record from its current group
var old_group;
for (var i = 0; i < parent.data.length; i++) {
old_group = self.localData[parent.data[i]];
var index = _.indexOf(old_group.data, recordID);
if (index >= 0) {
old_group.data.splice(index, 1);
old_group.count--;
old_group.res_ids = _.without(old_group.res_ids, resID);
self._updateParentResIDs(old_group);
break;
}
}
// Add record to its new group
new_group.data.push(recordID);
new_group.res_ids.push(resID);
new_group.count++;
return this.notifyChanges(recordID, changes).then(function () {
return self.save(recordID);
}).then(function () {
record.parentID = new_group.id;
return [old_group.id, new_group.id];
});
},
/**
* @override
*/
reload: function (id, options) {
// if the groupBy is given in the options and if it is an empty array,
// fallback on the default groupBy
if (options && options.groupBy && !options.groupBy.length) {
options.groupBy = this.defaultGroupedBy;
}
var def = this._super(id, options);
return this._reloadProgressBarGroupFromRecord(id, def);
},
/**
* @override
*/
save: function (recordID) {
var def = this._super.apply(this, arguments);
return this._reloadProgressBarGroupFromRecord(recordID, def);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_makeDataPoint: function (params) {
var dataPoint = this._super.apply(this, arguments);
if (params.progressBar) {
dataPoint.progressBar = params.progressBar;
}
return dataPoint;
},
/**
* @override
*/
_load: function (dataPoint, options) {
if (dataPoint.progressBar) {
return this._readProgressBarGroup(dataPoint, options);
}
return this._super.apply(this, arguments);
},
/**
* Ensures that there is no nested groups in Kanban (only the first grouping
* level is taken into account).
*
* @override
* @private
* @param {Object} list valid resource object
*/
_readGroup: function (list) {
var self = this;
if (list.groupedBy.length > 1) {
list.groupedBy = [list.groupedBy[0]];
}
return this._super.apply(this, arguments).then(function (result) {
return self._readTooltipFields(list).then(_.constant(result));
});
},
/**
* @private
* @param {Object} dataPoint
* @returns {Deferred<Object>}
*/
_readProgressBarGroup: function (list, options) {
var self = this;
var groupsDef = this._readGroup(list, options);
var progressBarDef = this._rpc({
model: list.model,
method: 'read_progress_bar',
kwargs: {
domain: list.domain,
group_by: list.groupedBy[0],
progress_bar: list.progressBar,
context: list.context,
},
});
return $.when(groupsDef, progressBarDef).then(function (a, data) {
_.each(list.data, function (groupID) {
var group = self.localData[groupID];
group.progressBarValues = _.extend({
counts: data[group.value] || {},
}, list.progressBar);
});
return list;
});
},
/**
* Fetches tooltip specific fields on the group by relation and stores it in
* the column datapoint in a special key `tooltipData`.
* Data for the tooltips (group_by_tooltip) are fetched in batch for all
* groups, to avoid doing multiple calls.
* Data are stored in a special key `tooltipData` on the datapoint.
* Note that the option `group_by_tooltip` is only for m2o fields.
*
* @private
* @param {Object} list a list of groups
* @returns {Deferred}
*/
_readTooltipFields: function (list) {
var self = this;
var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') {
return $.when();
}
var groupIds = _.reduce(list.data, function (groupIds, id) {
var res_id = self.get(id, {raw: true}).res_id;
// The field on which we are grouping might not be set on all records
if (res_id) {
groupIds.push(res_id);
}
return groupIds;
}, []);
var tooltipFields = [];
var groupedByFieldInfo = list.fieldsInfo.kanban[list.groupedBy[0]];
if (groupedByFieldInfo && groupedByFieldInfo.options) {
tooltipFields = Object.keys(groupedByFieldInfo.options.group_by_tooltip || {});
}
if (groupIds.length && tooltipFields.length) {
var fieldNames = _.union(['display_name'], tooltipFields);
return this._rpc({
model: groupedByField.relation,
method: 'read',
args: [groupIds, fieldNames],
context: list.context,
}).then(function (result) {
_.each(list.data, function (id) {
var dp = self.localData[id];
dp.tooltipData = _.findWhere(result, {id: dp.res_id});
});
});
}
return $.when();
},
/**
* Reloads all progressbar data. This is done after given deferred and
* insures that the given deferred's result is not lost.
*
* @private
* @param {string} recordID
* @param {Deferred} def
* @returns {Deferred}
*/
_reloadProgressBarGroupFromRecord: function (recordID, def) {
var element = this.localData[recordID];
if (element.type === 'list' && !element.parentID) {
// we are reloading the whole view, so there is no need to manually
// reload the progressbars
return def;
}
// If we updated a record, then we must potentially update columns'
// progressbars, so we need to load groups info again
var self = this;
while (element) {
if (element.progressBar) {
return def.then(function (data) {
return self._load(element, {
keepEmptyGroups: true,
onlyGroups: true,
}).then(function () {
return data;
});
});
}
element = this.localData[element.parentID];
}
return def;
},
});
return KanbanModel;
});