From 905e01921f3c3ef43ace6fc537d1d8c0d280002c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Sun, 5 Mar 2017 10:05:56 +0100 Subject: [REF] web, *: redesign all JS views This commit introduce a full redesign of all JS views. We started basically from scratch. The goal was to unify all the various views under a common framework, to make them testable, to make then usable in different conditions (in studio, or in the frontend), and to make our lives easier. Some important points are: - we introduced new coding guidelines (camelCase, 80 chars width, ...) - we have a brand new testing framework (still QUnit based) - kanban view moved to the web addon - calendar view (formerly web_calender) moved to web as well - the tree view was removed - all new code should be documented We hope that this code is the start of a new era for the Odoo web client, we want to have a high quality codebase, well documented, well tested, well designed. Work done by the framework team: mostly aab, ged, chm, dmo, qsm --- .../static/src/js/views/basic/basic_controller.js | 178 +++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 addons/web/static/src/js/views/basic/basic_controller.js (limited to 'addons/web/static/src/js/views/basic/basic_controller.js') diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js new file mode 100644 index 00000000000..24b5c7fa3d0 --- /dev/null +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -0,0 +1,178 @@ +odoo.define('web.BasicController', function (require) { +"use strict"; + +/** + * The BasicController is mostly here to share code between views that will use + * a BasicModel (or a subclass). Currently, the BasicViews are the form, list + * and kanban views. + */ + +var AbstractController = require('web.AbstractController'); +var FieldManagerMixin = require('web.FieldManagerMixin'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var Pager = require('web.Pager'); + +var _t = core._t; + +return AbstractController.extend(FieldManagerMixin, { + custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, { + reload: '_onReload', + }), + /** + * @override + * @param {Object} params + * @param {boolean} params.archiveEnabled + * @param {boolean} params.confirmOnDelete + * @param {boolean} params.hasButtons + */ + init: function (parent, model, renderer, params) { + this._super.apply(this, arguments); + this.archiveEnabled = params.archiveEnabled; + this.confirmOnDelete = params.confirmOnDelete; + this.hasButtons = params.hasButtons; + FieldManagerMixin.init.call(this, this.model); + this.handle = params.initialState.id; + }, + /** + * @override + * @returns {Deferred} + */ + start: function () { + return this._super.apply(this, arguments) + .then(this._updateEnv.bind(this)); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + renderPager: function ($node, options) { + var data = this.model.get(this.handle, {raw: true}); + this.pager = new Pager(this, data.count, data.offset + 1, data.limit, options); + + this.pager.on('pager_changed', this, function (newState) { + var self = this; + this.pager.disable(); + var limitChanged = (data.limit !== newState.limit); + this.reload({limit: newState.limit, offset: newState.current_min - 1}) + .then(function () { + // Reset the scroll position to the top on page changed only + if (!limitChanged) { + self.trigger_up('scrollTo', {offset: 0}); + } + }) + .then(this.pager.enable.bind(this.pager)); + }); + this.pager.appendTo($node); + this._updatePager(); // to force proper visibility + }, + /** + * @override + * @returns {Deferred} + */ + update: function (params, options) { + var self = this; + return this._super(params, options).then(function () { + self._updateEnv(); + self._updatePager(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Delete records (and ask for confirmation if necessary) + * + * @param {string[]} ids list of local record ids + */ + _deleteRecords: function (ids) { + var self = this; + function doIt() { + return self.model + .deleteRecords(ids, self.modelName, self.handle) + .then(self._onDeletedRecords.bind(self, ids)); + } + if (this.confirmOnDelete) { + Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), { + confirm_callback: doIt, + }); + } else { + doIt(); + } + }, + /** + * Hook method, called when record(s) has been deleted. + * + * @see _deleteRecord + * @param {string[]} ids list of deleted ids (basic model local handles) + */ + _onDeletedRecords: function (ids) { + this.update({}); + }, + /** + * Helper method, to get the current environment variables from the model + * and notifies the component chain (by bubbling an event up) + */ + _updateEnv: function () { + var env = this.model.get(this.handle, {env: true}); + this.trigger_up('env_updated', env); + }, + /** + * Helper method, to make sure the information displayed by the pager is up + * to date. + */ + _updatePager: function () { + if (this.pager) { + var data = this.model.get(this.handle, {raw: true}); + this.pager.updateState({ + current_min: data.offset + 1, + size: data.count, + }); + var isRecord = data.type === 'record'; + var hasData = !!data.count; + var isGrouped = data.groupedBy ? !!data.groupedBy.length : false; + var isNew = this.model.isNew(this.handle); + var isPagerVisible = isRecord ? !isNew : (hasData && !isGrouped); + + this.pager.do_toggle(isPagerVisible); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * When a reload event triggers up, we need to reload the full view. + * For example, after a form view dialog saved some data. + * + * @todo: rename db_id into handle + * + * @param {OdooEvent} event + * @param {Object} event.data + * @param {string} [event.data.db_id] handle of the data to reload and + * re-render (reload the whole form by default) + * @param {string[]} [event.data.fieldNames] list of the record's fields to + * reload + */ + _onReload: function (event) { + var data = event && event.data || {}; + var handle = data.db_id; + if (handle) { + // reload the relational field given its db_id + this.model.reload(handle).then(this._confirmSave.bind(this, handle)); + } else { + // no db_id given, so reload the main record + this.reload({fieldNames: data.fieldNames}); + } + }, + +}); + +}); -- cgit v1.2.3 From 1a0702f1e20a8347a1135ade60858f5905bbca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 27 Feb 2017 16:15:54 +0100 Subject: [FIX] web: properly handles dirty data in form views Before this commit, we only consider the BasicModel point of view to determine if a record is dirty. It is good when we want to determine if it is actually necessary to save the data, but not correct when we want to know if some changes can be discarded. With this commit, the form controller keeps track of its 'dirty' state, coming from user interactions. It uses this flag to determine if changes can be discarded. --- addons/web/static/src/js/views/basic/basic_controller.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'addons/web/static/src/js/views/basic/basic_controller.js') diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 24b5c7fa3d0..2847003f5df 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -33,6 +33,7 @@ return AbstractController.extend(FieldManagerMixin, { this.hasButtons = params.hasButtons; FieldManagerMixin.init.call(this, this.model); this.handle = params.initialState.id; + this.isDirty = false; }, /** * @override @@ -148,6 +149,18 @@ return AbstractController.extend(FieldManagerMixin, { // Handlers //-------------------------------------------------------------------------- + /** + * We need to keep manually track of the isDirty property. Note that it is + * not the same as the one from the basic model. A new record from the + * basic model is considered dirty, but from the controller point of view, + * there was no user interaction and should not be dirty. + * + * @private + */ + _onFieldChanged: function () { + this.isDirty = true; + FieldManagerMixin._onFieldChanged.apply(this, arguments); + }, /** * When a reload event triggers up, we need to reload the full view. * For example, after a form view dialog saved some data. -- cgit v1.2.3 From 6bad524bb356654208a9bc9dada0de2cfc67b0af Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 22 Mar 2017 14:17:17 +0100 Subject: [FIX] web: grouped Kanban: don't display nocontent helper When the kanban is grouped and its groups contain no record, the nocontent helper shouldn't be displayed. This wasn't the case before this rev. --- addons/web/static/src/js/views/basic/basic_controller.js | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'addons/web/static/src/js/views/basic/basic_controller.js') diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 2847003f5df..c21d7a43335 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -107,6 +107,17 @@ return AbstractController.extend(FieldManagerMixin, { doIt(); } }, + /** + * Used by list and kanban views to determine whether or not to display + * the no content helper (if there is no data in the state to display) + * + * @private + * @param {Object} state + * @returns {boolean} + */ + _hasContent: function (state) { + return state.count !== 0; + }, /** * Hook method, called when record(s) has been deleted. * -- cgit v1.2.3