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

483 lines
18 KiB
JavaScript

flectra.define('web.KanbanController', function (require) {
"use strict";
/**
* The KanbanController is the class that coordinates the kanban model and the
* kanban renderer. It also makes sure that update from the search view are
* properly interpreted.
*/
var BasicController = require('web.BasicController');
var Context = require('web.Context');
var core = require('web.core');
var Domain = require('web.Domain');
var view_dialogs = require('web.view_dialogs');
var _t = core._t;
var qweb = core.qweb;
var KanbanController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, {
quick_create_add_column: '_onAddColumn',
quick_create_record: '_onQuickCreateRecord',
resequence_columns: '_onResequenceColumn',
button_clicked: '_onButtonClicked',
kanban_record_delete: '_onRecordDelete',
kanban_record_update: '_onUpdateRecord',
kanban_column_delete: '_onDeleteColumn',
kanban_column_add_record: '_onAddRecordToColumn',
kanban_column_resequence: '_onColumnResequence',
kanban_column_archive_records: '_onArchiveRecords',
kanban_load_more: '_onLoadMore',
kanban_load_records: '_onLoadColumnRecords',
column_toggle_fold: '_onToggleColumn',
}),
/**
* @override
* @param {Object} params
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.on_create = params.on_create;
this.hasButtons = params.hasButtons;
this.createColumnEnabled = this._isCreateColumnEnabled();
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @param {jQueryElement} $node
*/
renderButtons: function ($node) {
if (this.hasButtons && this.is_action_enabled('create')) {
this.$buttons = $(qweb.render('KanbanView.buttons', {widget: this}));
this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this));
this._updateButtons();
this.$buttons.appendTo($node);
}
},
/**
* Override update method to recompute createColumnEnabled.
*
* @returns {Deferred}
*/
update: function () {
this.createColumnEnabled = this._isCreateColumnEnabled();
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override method comes from field manager mixin
* @private
* @param {string} id local id from the basic record data
* @returns {Deferred}
*/
_confirmSave: function (id) {
var data = this.model.get(this.handle, {raw: true});
var grouped = data.groupedBy.length;
if (grouped) {
var columnState = this.model.getColumn(id);
return this.renderer.updateColumn(columnState.id, columnState);
}
return this.renderer.updateRecord(this.model.get(id));
},
/**
* The column quick create should be displayed in kanban iff grouped by an
* m2o field and group_create action enabled.
*
* @private
* @returns {boolean}
*/
_isCreateColumnEnabled: function () {
var groupCreate = this.is_action_enabled('group_create');
if (!groupCreate) {
// pre-return to avoid a lot of the following processing
return false;
}
var state = this.model.get(this.handle, {raw: true});
var groupByField = state.fields[state.groupedBy[0]];
var groupedByM2o = groupByField && (groupByField.type === 'many2one');
return groupedByM2o;
},
/**
* @param {number[]} ids
* @private
* @returns {Deferred}
*/
_resequenceColumns: function (ids) {
var state = this.model.get(this.handle, {raw: true});
var model = state.fields[state.groupedBy[0]].relation;
return this.model.resequence(model, ids, this.handle);
},
/**
* This method calls the server to ask for a resequence. Note that this
* does not rerender the user interface, because in most case, the
* resequencing operation has already been displayed by the renderer.
*
* @private
* @param {string} column_id
* @param {string[]} ids
* @returns {Deferred}
*/
_resequenceRecords: function (column_id, ids) {
var self = this;
return this.model.resequence(this.modelName, ids, column_id).then(function () {
self._updateEnv();
});
},
/**
* In grouped mode, set 'Create' button as btn-default if there is no column
* (except if we can't create new columns)
*
* @private
* @override from abstract controller
*/
_updateButtons: function () {
if (this.$buttons) {
var data = this.model.get(this.handle, {raw: true});
var grouped = data.groupedBy.length;
var createMuted = grouped && data.data.length === 0 && this.createColumnEnabled;
this.$buttons.find('.o-kanban-button-new')
.toggleClass('btn-primary', !createMuted)
.toggleClass('btn-default', createMuted);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* This handler is called when an event (from the quick create add column)
* event bubbles up. When that happens, we need to ask the model to create
* a group and to update the renderer
*
* @private
* @param {FlectraEvent} event
*/
_onAddColumn: function (event) {
var self = this;
this.model.createGroup(event.data.value, this.handle).then(function () {
var state = self.model.get(self.handle, {raw: true});
var ids = _.pluck(state.data, 'res_id').filter(_.isNumber);
return self._resequenceColumns(ids);
}).then(function () {
return self.update({}, {reload: false});
}).then(function () {
self._updateButtons();
self.renderer.quickCreateToggleFold();
});
},
/**
* @private
* @param {FlectraEvent} event
*/
_onAddRecordToColumn: function (event) {
var self = this;
var record = event.data.record;
var column = event.target;
this.alive(this.model.moveRecord(record.db_id, column.db_id, this.handle))
.then(function (column_db_ids) {
return self._resequenceRecords(column.db_id, event.data.ids)
.then(function () {
_.each(column_db_ids, function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
});
});
}).fail(this.reload.bind(this));
},
/**
* The interface allows in some case the user to archive a column. This is
* what this handler is for.
*
* @private
* @param {FlectraEvent} event
*/
_onArchiveRecords: function (event) {
var self = this;
var active_value = !event.data.archive;
var column = event.target;
var record_ids = _.pluck(column.records, 'db_id');
if (record_ids.length) {
this.model
.toggleActive(record_ids, active_value, column.db_id)
.then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
self._updateEnv();
});
}
},
/**
* @private
* @param {FlectraEvent} event
*/
_onButtonClicked: function (event) {
event.stopPropagation();
var self = this;
var attrs = event.data.attrs;
var record = event.data.record;
if (attrs.context) {
attrs.context = new Context(attrs.context)
.set_eval_context({
active_id: record.res_id,
active_ids: [record.res_id],
active_model: record.model,
});
}
this.trigger_up('execute_action', {
action_data: attrs,
env: {
context: record.getContext(),
currentID: record.res_id,
model: record.model,
resIDs: record.res_ids,
},
on_closed: function () {
var recordModel = self.model.localData[record.id];
var group = self.model.localData[recordModel.parentID];
var parent = self.model.localData[group.parentID];
self.model.reload(record.id).then(function (db_id) {
var data = self.model.get(db_id);
var kanban_record = event.target;
kanban_record.update(data);
// Check if we still need to display the record. Some fields of the domain are
// not guaranteed to be in data. This is for example the case if the action
// contains a domain on a field which is not in the Kanban view. Therefore,
// we need to handle multiple cases based on 3 variables:
// domInData: all domain fields are in the data
// activeInDomain: 'active' is already in the domain
// activeInData: 'active' is available in the data
var domain = (parent ? parent.domain : group.domain) || [];
var domInData = _.every(domain, function (d) {
return d[0] in data.data;
});
var activeInDomain = _.pluck(domain, 0).indexOf('active') !== -1;
var activeInData = 'active' in data.data;
// Case # | domInData | activeInDomain | activeInData
// 1 | true | true | true => no domain change
// 2 | true | true | false => not possible
// 3 | true | false | true => add active in domain
// 4 | true | false | false => no domain change
// 5 | false | true | true => no evaluation
// 6 | false | true | false => no evaluation
// 7 | false | false | true => replace domain
// 8 | false | false | false => no evaluation
// There are 3 cases which cannot be evaluated since we don't have all the
// necessary information. The complete solution would be to perform a RPC in
// these cases, but this is out of scope. A simpler one is to do a try / catch.
if (domInData && !activeInDomain && activeInData) {
domain = domain.concat([['active', '=', true]]);
} else if (!domInData && !activeInDomain && activeInData) {
domain = [['active', '=', true]];
}
try {
var visible = new Domain(domain).compute(data.evalContext);
} catch (e) {
return;
}
if (!visible) {
kanban_record.destroy();
}
});
},
});
},
/**
* @private
*/
_onButtonNew: function () {
var state = this.model.get(this.handle, {raw: true});
var hasColumns = state.groupedBy.length > 0 && state.data.length > 0;
if (hasColumns && this.on_create === 'quick_create') {
// Activate the quick create in the first column
this.renderer.addQuickCreate();
} else if (this.on_create && this.on_create !== 'quick_create') {
// Execute the given action
this.do_action(this.on_create, {
on_close: this.reload.bind(this),
additional_context: state.context,
});
} else {
// Open the form view
this.trigger_up('switch_view', {
view_type: 'form',
res_id: undefined
});
}
},
/**
* @private
* @param {FlectraEvent} event
*/
_onColumnResequence: function (event) {
this._resequenceRecords(event.target.db_id, event.data.ids);
},
/**
* @private
* @param {FlectraEvent} event
*/
_onDeleteColumn: function (event) {
var self = this;
var column = event.target;
var state = this.model.get(this.handle, {raw: true});
var relatedModelName = state.fields[state.groupedBy[0]].relation;
this.model
.deleteRecords([column.db_id], relatedModelName)
.done(function () {
if (column.isEmpty()) {
self.renderer.removeWidget(column);
self._updateButtons();
} else {
self.reload();
}
});
},
/**
* Loads the record of a given column (used in mobile, as the columns are
* lazy loaded)
*
* @private
* @param {FlectraEvent} event
*/
_onLoadColumnRecords: function (event) {
var self = this;
this.model.loadColumnRecords(event.data.columnID).then(function (dbID) {
var data = self.model.get(dbID);
self.renderer.updateColumn(dbID, data);
self._updateEnv();
if (event.data.onSuccess) {
event.data.onSuccess();
}
});
},
/**
* @private
* @param {FlectraEvent} event
*/
_onLoadMore: function (event) {
var self = this;
var column = event.target;
this.model.loadMore(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
self._updateEnv();
});
},
/**
* @private
* @param {FlectraEvent} event
*/
_onQuickCreateRecord: function (event) {
var self = this;
var column = event.target;
var name = event.data.value;
var state = this.model.get(this.handle, {raw: true});
var columnState = this.model.get(column.db_id, {raw: true});
var context = columnState.getContext();
context['default_' + state.groupedBy[0]] = columnState.res_id;
this._rpc({
model: state.model,
method: 'name_create',
args: [name],
context: context,
})
.then(add_record)
.fail(function (error, event) {
event.preventDefault();
new view_dialogs.FormViewDialog(self, {
res_model: state.model,
context: _.extend({default_name: name}, context),
title: _t("Create"),
disable_multiple_selection: true,
on_saved: function (record) {
add_record([record.res_id]);
},
}).open();
});
function add_record(records) {
return self.model
.addRecordToGroup(columnState.id, records[0])
.then(function (db_id) {
self._updateEnv();
var columnState = self.model.getColumn(db_id);
return self.renderer
.updateColumn(columnState.id, columnState, {openQuickCreate: true})
.then(function () {
if (event.data.openRecord) {
self.trigger_up('open_record', {id: db_id, mode: 'edit'});
}
});
});
}
},
/**
* @private
* @param {FlectraEvent} event
*/
_onRecordDelete: function (event) {
this._deleteRecords([event.data.id]);
},
/**
* @private
* @param {FlectraEvent} event
*/
_onResequenceColumn: function (event) {
var self = this;
this._resequenceColumns(event.data.ids).then(function () {
self._updateEnv();
});
},
/**
* @private
* @param {FlectraEvent} event
* @param {boolean} [event.data.openQuickCreate=false] if true, opens the
* QuickCreate in the toggled column (it assumes that we are opening it)
*/
_onToggleColumn: function (event) {
var self = this;
var column = event.target;
this.model.toggleGroup(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
var options = {
openQuickCreate: !!event.data.openQuickCreate,
};
self.renderer.updateColumn(db_id, data, options);
self._updateEnv();
});
},
/**
* @todo should simply use field_changed event...
*
* @private
* @param {FlectraEvent} ev
*/
_onUpdateRecord: function (ev) {
var changes = _.clone(ev.data);
ev.data.force_save = true;
this._applyChanges(ev.target.db_id, changes, ev);
},
});
return KanbanController;
});