summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/kanban/kanban_controller.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/kanban/kanban_controller.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/views/kanban/kanban_controller.js')
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js537
1 files changed, 537 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
new file mode 100644
index 00000000..1b6e6301
--- /dev/null
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -0,0 +1,537 @@
+odoo.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 Dialog = require('web.Dialog');
+var Domain = require('web.Domain');
+var view_dialogs = require('web.view_dialogs');
+var viewUtils = require('web.viewUtils');
+
+var _t = core._t;
+var qweb = core.qweb;
+
+var KanbanController = BasicController.extend({
+ buttons_template: 'KanbanView.buttons',
+ custom_events: _.extend({}, BasicController.prototype.custom_events, {
+ add_quick_create: '_onAddQuickCreate',
+ 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_load_more: '_onLoadMore',
+ column_toggle_fold: '_onToggleColumn',
+ kanban_column_records_toggle_active: '_onToggleActiveRecords',
+ }),
+ /**
+ * @override
+ * @param {Object} params
+ * @param {boolean} params.quickCreateEnabled set to false to disable the
+ * quick create feature
+ * @param {SearchPanel} [params.searchPanel]
+ * @param {Array[]} [params.controlPanelDomain=[]] initial domain coming
+ * from the controlPanel
+ */
+ init: function (parent, model, renderer, params) {
+ this._super.apply(this, arguments);
+ this.on_create = params.on_create;
+ this.hasButtons = params.hasButtons;
+ this.quickCreateEnabled = params.quickCreateEnabled;
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @param {jQuery} [$node]
+ */
+ renderButtons: function ($node) {
+ if (!this.hasButtons || !this.is_action_enabled('create')) {
+ return;
+ }
+ this.$buttons = $(qweb.render(this.buttons_template, {
+ btnClass: 'btn-primary',
+ widget: this,
+ }));
+ this.$buttons.on('click', 'button.o-kanban-button-new', this._onButtonNew.bind(this));
+ this.$buttons.on('keydown', this._onButtonsKeyDown.bind(this));
+ if ($node) {
+ this.$buttons.appendTo($node);
+ }
+ },
+ /**
+ * In grouped mode, set 'Create' button as btn-secondary if there is no column
+ * (except if we can't create new columns)
+ *
+ * @override
+ */
+ updateButtons: function () {
+ if (!this.$buttons) {
+ return;
+ }
+ var state = this.model.get(this.handle, {raw: true});
+ var createHidden = this.is_action_enabled('group_create') && state.isGroupedByM2ONoColumn;
+ this.$buttons.find('.o-kanban-button-new').toggleClass('o_hidden', createHidden);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Displays the record quick create widget in the requested column, given its
+ * id (in the first column by default). Ensures that we removed sample data
+ * if any, before displaying the quick create.
+ *
+ * @private
+ * @param {string} [groupId]
+ */
+ _addQuickCreate(groupId) {
+ this._removeSampleData(async () => {
+ await this.update({ shouldUpdateSearchComponents: false }, { reload: false });
+ return this.renderer.addQuickCreate(groupId);
+ });
+ },
+ /**
+ * @override method comes from field manager mixin
+ * @private
+ * @param {string} id local id from the basic record data
+ * @returns {Promise}
+ */
+ _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));
+ },
+ /**
+ * Only display the pager in the ungrouped case, with data.
+ *
+ * @override
+ * @private
+ */
+ _getPagingInfo: function (state) {
+ if (!(state.count && !state.groupedBy.length)) {
+ return null;
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @private
+ * @param {Widget} kanbanRecord
+ * @param {Object} params
+ */
+ _reloadAfterButtonClick: function (kanbanRecord, params) {
+ var self = this;
+ var recordModel = this.model.localData[params.record.id];
+ var group = this.model.localData[recordModel.parentID];
+ var parent = this.model.localData[group.parentID];
+
+ this.model.reload(params.record.id).then(function (db_id) {
+ var data = self.model.get(db_id);
+ kanbanRecord.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) {
+ kanbanRecord.destroy();
+ }
+ });
+ },
+ /**
+ * @param {number[]} ids
+ * @private
+ * @returns {Promise}
+ */
+ _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 {Promise}
+ */
+ _resequenceRecords: function (column_id, ids) {
+ var self = this;
+ return this.model.resequence(this.modelName, ids, column_id);
+ },
+ /**
+ * @override
+ */
+ _shouldBounceOnClick(element) {
+ const state = this.model.get(this.handle, {raw: true});
+ if (!state.count || state.isSample) {
+ const classesList = [
+ 'o_kanban_view',
+ 'o_kanban_group',
+ 'o_kanban_header',
+ 'o_column_quick_create',
+ 'o_view_nocontent_smiling_face',
+ ];
+ return classesList.some(c => element.classList.contains(c));
+ }
+ return false;
+ },
+
+ //--------------------------------------------------------------------------
+ // 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 {OdooEvent} ev
+ */
+ _onAddColumn: function (ev) {
+ var self = this;
+ this.mutex.exec(function () {
+ return self.model.createGroup(ev.data.value, self.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 () {
+ let quickCreateFolded = self.renderer.quickCreate.folded;
+ if (ev.data.foldQuickCreate ? !quickCreateFolded : quickCreateFolded) {
+ self.renderer.quickCreateToggleFold();
+ }
+ self.renderer.trigger_up("quick_create_column_created");
+ });
+ });
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onAddRecordToColumn: function (ev) {
+ var self = this;
+ var record = ev.data.record;
+ var column = ev.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, ev.data.ids)
+ .then(function () {
+ _.each(column_db_ids, function (db_id) {
+ var data = self.model.get(db_id);
+ self.renderer.updateColumn(db_id, data);
+ });
+ });
+ }).guardedCatch(this.reload.bind(this));
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @returns {string} ev.data.groupId
+ */
+ _onAddQuickCreate(ev) {
+ ev.stopPropagation();
+ this._addQuickCreate(ev.data.groupId);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onButtonClicked: function (ev) {
+ var self = this;
+ ev.stopPropagation();
+ var attrs = ev.data.attrs;
+ var record = ev.data.record;
+ var def = Promise.resolve();
+ 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,
+ });
+ }
+ if (attrs.confirm) {
+ def = new Promise(function (resolve, reject) {
+ Dialog.confirm(this, attrs.confirm, {
+ confirm_callback: resolve,
+ cancel_callback: reject,
+ }).on("closed", null, reject);
+ });
+ }
+ def.then(function () {
+ self.trigger_up('execute_action', {
+ action_data: attrs,
+ env: {
+ context: record.getContext(),
+ currentID: record.res_id,
+ model: record.model,
+ resIDs: record.res_ids,
+ },
+ on_closed: self._reloadAfterButtonClick.bind(self, ev.target, ev.data),
+ });
+ });
+ },
+ /**
+ * @private
+ */
+ _onButtonNew: function () {
+ var state = this.model.get(this.handle, {raw: true});
+ var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
+ if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) {
+ // activate the quick create in the first column when the mutex is
+ // unlocked, to ensure that there is no pending re-rendering that
+ // would remove it (e.g. if we are currently adding a new column)
+ this.mutex.getUnlockedDef().then(this._addQuickCreate.bind(this, null));
+ } 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
+ });
+ }
+ },
+ /**
+ * Moves the focus from the controller buttons to the first kanban record
+ *
+ * @private
+ * @param {jQueryEvent} ev
+ */
+ _onButtonsKeyDown: function (ev) {
+ switch(ev.keyCode) {
+ case $.ui.keyCode.DOWN:
+ this._giveFocus();
+ }
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onColumnResequence: function (ev) {
+ this._resequenceRecords(ev.target.db_id, ev.data.ids);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDeleteColumn: function (ev) {
+ var column = ev.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)
+ .then(this.update.bind(this, {}, {}));
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onLoadMore: function (ev) {
+ var self = this;
+ var column = ev.target;
+ this.model.loadMore(column.db_id).then(function (db_id) {
+ var data = self.model.get(db_id);
+ self.renderer.updateColumn(db_id, data);
+ });
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {KanbanColumn} ev.target the column in which the record should
+ * be added
+ * @param {Object} ev.data.values the field values of the record to
+ * create; if values only contains the value of the 'display_name', a
+ * 'name_create' is performed instead of 'create'
+ * @param {function} [ev.data.onFailure] called when the quick creation
+ * failed
+ */
+ _onQuickCreateRecord: function (ev) {
+ var self = this;
+ var values = ev.data.values;
+ var column = ev.target;
+ var onFailure = ev.data.onFailure || function () {};
+
+ // function that updates the kanban view once the record has been added
+ // it receives the local id of the created record in arguments
+ var update = function (db_id) {
+
+ var columnState = self.model.getColumn(db_id);
+ var state = self.model.get(self.handle);
+ return self.renderer
+ .updateColumn(columnState.id, columnState, {openQuickCreate: true, state: state})
+ .then(function () {
+ if (ev.data.openRecord) {
+ self.trigger_up('open_record', {id: db_id, mode: 'edit'});
+ }
+ });
+ };
+
+ this.model.createRecordInGroup(column.db_id, values)
+ .then(update)
+ .guardedCatch(function (reason) {
+ reason.event.preventDefault();
+ var columnState = self.model.get(column.db_id, {raw: true});
+ var context = columnState.getContext();
+ var state = self.model.get(self.handle, {raw: true});
+ var groupedBy = state.groupedBy[0];
+ context['default_' + groupedBy] = viewUtils.getGroupValue(columnState, groupedBy);
+ new view_dialogs.FormViewDialog(self, {
+ res_model: state.model,
+ context: _.extend({default_name: values.name || values.display_name}, context),
+ title: _t("Create"),
+ disable_multiple_selection: true,
+ on_saved: function (record) {
+ self.model.addRecordToGroup(column.db_id, record.res_id)
+ .then(update);
+ },
+ }).open().opened(onFailure);
+ });
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onRecordDelete: function (ev) {
+ this._deleteRecords([ev.data.id]);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onResequenceColumn: function (ev) {
+ var self = this;
+ this._resequenceColumns(ev.data.ids);
+ },
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {boolean} [ev.data.openQuickCreate=false] if true, opens the
+ * QuickCreate in the toggled column (it assumes that we are opening it)
+ */
+ _onToggleColumn: function (ev) {
+ var self = this;
+ const columnID = ev.target.db_id || ev.data.db_id;
+ this.model.toggleGroup(columnID)
+ .then(function (db_id) {
+ var data = self.model.get(db_id);
+ var options = {
+ openQuickCreate: !!ev.data.openQuickCreate,
+ };
+ return self.renderer.updateColumn(db_id, data, options);
+ })
+ .then(function () {
+ if (ev.data.onSuccess) {
+ ev.data.onSuccess();
+ }
+ });
+ },
+ /**
+ * @todo should simply use field_changed event...
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {function} [ev.data.onSuccess] callback to execute after applying
+ * changes
+ */
+ _onUpdateRecord: function (ev) {
+ var onSuccess = ev.data.onSuccess;
+ delete ev.data.onSuccess;
+ var changes = _.clone(ev.data);
+ ev.data.force_save = true;
+ this._applyChanges(ev.target.db_id, changes, ev).then(onSuccess);
+ },
+ /**
+ * Allow the user to archive/restore all the records of a column.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onToggleActiveRecords: function (ev) {
+ var self = this;
+ var archive = ev.data.archive;
+ var column = ev.target;
+ var recordIds = _.pluck(column.records, 'id');
+ if (recordIds.length) {
+ var prom = archive ?
+ this.model.actionArchive(recordIds, column.db_id) :
+ this.model.actionUnarchive(recordIds, column.db_id);
+ prom.then(function (dbID) {
+ var data = self.model.get(dbID);
+ if (data) { // Could be null if a wizard is returned for example
+ self.model.reload(self.handle).then(function () {
+ const state = self.model.get(self.handle);
+ self.renderer.updateColumn(dbID, data, { state });
+ });
+ }
+ });
+ }
+ },
+});
+
+return KanbanController;
+
+});