diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/kanban/kanban_controller.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js | 537 |
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; + +}); |
