diff options
| author | qsm-odoo <qsm@odoo.com> | 2017-09-14 22:43:43 +0200 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2017-09-14 23:48:56 +0200 |
| commit | 33f9e90bd5a9ad3e9239f1b414ea012bb94fa1c0 (patch) | |
| tree | 18f7ec62c340b1e6e63e5520565b0bd4b7d8a10c /addons/web/static/src/js | |
| parent | 031dc1d952837c0d9740e1caac1ab1d923863545 (diff) | |
[IMP] web, *: add kanban column progress bars
* crm, project
Add a new feature which allows to put a progressbar in the kanban
columns. The progressbar shows with the same color the amount of
records whose value of a given field are the same in the column.
It also indicate the sum of another given field or simply the total
number of records. It also allows to subgroup the column content.
To define a progressbar, add this as a direct child of the kanban
arch:
<progressbar field="<name of the field to use for subgroups>"
colors="{<one possible value for the above field>: <success, warning or danger>, ...}"
sum="<name of the field to sum or nothing to use total number of records>"/>
Also:
- Properly update record model data's parentID when moving a record
- ...
Diffstat (limited to 'addons/web/static/src/js')
7 files changed, 463 insertions, 18 deletions
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 779073b51ed..bcccefbb984 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1576,7 +1576,7 @@ var BasicModel = AbstractModel.extend({ if (change.id) { relRecord = self.localData[change.id]; } - switch(change.operation) { + switch (change.operation) { case 'ADD': list.count++; var resID = relRecord ? relRecord.res_id : change.resID; @@ -1666,7 +1666,7 @@ var BasicModel = AbstractModel.extend({ var result = {}; var self = this; var evalContext; - function evalModifier (mod) { + function evalModifier(mod) { if (mod === undefined || mod === false || mod === true) { return !!mod; } @@ -2775,7 +2775,7 @@ var BasicModel = AbstractModel.extend({ var context = _.extend({}, record.data, record._changes); // calls _generateX2ManyCommands for a given field, and returns the array of commands - function _generateX2ManyCommands (fieldName) { + function _generateX2ManyCommands(fieldName) { var commands = self._generateX2ManyCommands(record, {fieldNames: [fieldName]}); return commands[fieldName]; } diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js index ab071d0e930..dac28c536dd 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column.js +++ b/addons/web/static/src/js/views/kanban/kanban_column.js @@ -8,6 +8,7 @@ var kanban_quick_create = require('web.kanban_quick_create'); var KanbanRecord = require('web.KanbanRecord'); var view_dialogs = require('web.view_dialogs'); var Widget = require('web.Widget'); +var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); var _t = core._t; var QWeb = core.qweb; @@ -19,6 +20,8 @@ var KanbanColumn = Widget.extend({ cancel_quick_create: '_onCancelQuickCreate', kanban_record_delete: '_onDeleteRecord', quick_create_add_record: '_onQuickCreateAddRecord', + tweak_column: '_onTweakColumn', + tweak_column_records: '_onTweakColumnRecords', }, events: { 'click .o_column_edit': '_onEditColumn', @@ -58,6 +61,13 @@ var KanbanColumn = Widget.extend({ this.offset = 0; this.remaining = this.size - this.data_records.length; + if (options.hasProgressBar) { + this.barOptions = { + columnID: this.db_id, + progressBarStates: options.progressBarStates, + }; + } + this.record_options = _.clone(recordOptions); if (options.grouped_by_m2o) { @@ -81,6 +91,7 @@ var KanbanColumn = Widget.extend({ */ start: function () { var self = this; + var defs = [this._super.apply(this, arguments)]; this.$header = this.$('.o_kanban_header'); for (var i = 0; i < this.data_records.length; i++) { @@ -128,9 +139,12 @@ var KanbanColumn = Widget.extend({ self._onToggleFold(event); } }); - this._update(); + if (this.barOptions) { + this.progressBar = new KanbanColumnProgressBar(this, this.barOptions, this.data); + defs.push(this.progressBar.appendTo(this.$header)); + } - return this._super.apply(this, arguments); + return $.when.apply($, defs).then(this._update.bind(this)); }, //-------------------------------------------------------------------------- @@ -180,6 +194,18 @@ var KanbanColumn = Widget.extend({ isEmpty: function () { return !this.records.length; }, + /** + * Updates the column progressBar and sets the new data. New data are + * supposed to already match column rendering (except for the progressBar). + * + * @param {Object} data + */ + updateProgressBar: function (data) { + this.data = data; + if (!this.folded && this.progressBar) { + this.progressBar.update(this.data); + } + }, //-------------------------------------------------------------------------- // Private @@ -221,6 +247,7 @@ var KanbanColumn = Widget.extend({ } else { this.$('.o_kanban_load_more').html(QWeb.render('KanbanView.LoadMore', {widget: this})); } + this.updateProgressBar(this.data); }, //-------------------------------------------------------------------------- @@ -321,6 +348,22 @@ var KanbanColumn = Widget.extend({ }, /** * @private + * @param {OdooEvent} ev + */ + _onTweakColumn: function (ev) { + ev.data.callback(this.$el); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onTweakColumnRecords: function (ev) { + _.each(this.records, function (record) { + ev.data.callback(record.$el, record.state.data); + }); + }, + /** + * @private * @param {MouseEvent} event */ _onUnarchiveRecords: function (event) { diff --git a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js new file mode 100644 index 00000000000..c4a62989b97 --- /dev/null +++ b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js @@ -0,0 +1,247 @@ +odoo.define('web.KanbanColumnProgressBar', function (require) { +'use strict'; + +var Widget = require('web.Widget'); +var session = require('web.session'); +var utils = require('web.utils'); + +var KanbanColumnProgressBar = Widget.extend({ + template: 'KanbanView.ColumnProgressBar', + events: { + 'click .o_kanban_counter_progress': '_onProgressBarParentClick', + 'click .progress-bar': '_onProgressBarClick', + }, + /** + * Allows to disable animations for tests. + * @type {boolean} + */ + ANIMATE: true, + + /** + * @constructor + */ + init: function (parent, options, columnState) { + this._super.apply(this, arguments); + + this.columnID = options.columnID; + + // <progressbar/> attributes + this.fieldName = columnState.progressBarValues.field; + this.colors = columnState.progressBarValues.colors; + this.sumField = columnState.progressBarValues.sum; + + // Previous progressBar state + var state = options.progressBarStates[this.columnID]; + this.groupCount = state ? state.groupCount : 0; + this.subgroupCounts = state ? state.subgroupCounts : {}; + this.totalCounterValue = state ? state.totalCounterValue : 0; + this.activeFilter = state ? state.activeFilter : false; + + // Prepare currency (TODO this should be automatic... use a field ?) + this.counterPrefix = ''; + this.counterSuffix = ''; + var sumFieldInfo = this.sumField && columnState.fieldsInfo.kanban[this.sumField]; + var currencyField = sumFieldInfo && sumFieldInfo.options && sumFieldInfo.options.currency_field; + if (currencyField && columnState.data.length) { + var currency = session.currencies[columnState.data[0].data[currencyField].res_id]; + if (currency.position === 'before') { + this.counterPrefix = currency.symbol + ' '; + } else { + this.counterSuffix = ' ' + currency.symbol; + } + } + }, + /** + * @override + */ + start: function () { + var self = this; + + this.$bars = {}; + _.each(this.colors, function (val, key) { + self.$bars[val] = self.$('.bg-' + val + '-full'); + }); + this.$counter = this.$('.o_kanban_counter_side'); + + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Updates internal data and rendering according to new received column + * state. + * + * @param {Object} columnState + */ + update: function (columnState) { + var self = this; + + var subgroupCounts = {}; + _.each(self.colors, function (val, key) { + var subgroupCount = columnState.progressBarValues.counts[key] || 0; + if (self.activeFilter === val && subgroupCount === 0) { + self.activeFilter = false; + } + subgroupCounts[key] = subgroupCount; + }); + + this.groupCount = columnState.count; + this.subgroupCounts = subgroupCounts; + this.prevTotalCounterValue = this.totalCounterValue; + this.totalCounterValue = this.sumField ? (columnState.aggregateValues[this.sumField] || 0) : columnState.count; + this._notifyState(); + this._render(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Updates the rendering according to internal data. This is done without + * qweb rendering because there are animations. + * + * @private + */ + _render: function () { + var self = this; + + // Update column display according to active filter + this.trigger_up('tweak_column', { + callback: function ($el) { + $el.removeClass('o_kanban_group_show'); + _.each(self.colors, function (val, key) { + $el.removeClass('o_kanban_group_show_' + val); + }); + if (self.activeFilter) { + $el.addClass('o_kanban_group_show o_kanban_group_show_' + self.colors[self.activeFilter]); + } + }, + }); + this.trigger_up('tweak_column_records', { + callback: function ($el, recordData) { + var categoryValue = recordData[self.fieldName]; + _.each(self.colors, function (val, key) { + $el.removeClass('oe_kanban_card_' + val); + }); + if (self.colors[categoryValue]) { + $el.addClass('oe_kanban_card_' + self.colors[categoryValue]); + } + }, + }); + + // Display and animate the progress bars + _.each(self.colors, function (val, key) { + var $bar = self.$bars[val]; + var count = self.subgroupCounts[key]; + + if (!$bar) { + return; + } + + // Adapt tooltip + $bar.attr('data-original-title', count + ' ' + key); + $bar.tooltip({ + delay: '0', + trigger:'hover', + placement: 'top' + }); + + // Adapt active state + $bar.toggleClass('active progress-bar-striped', key === self.activeFilter); + + // Adapt width + $bar.removeClass('o_bar_has_records transition-off'); + window.getComputedStyle($bar[0]).getPropertyValue('width'); // Force reflow so that animations work + if (count > 0) { + $bar.addClass('o_bar_has_records'); + $bar.css('width', (count * 100 / self.groupCount) + '%'); + } else { + $bar.css('width', ''); + } + }); + + // Display and animate the counter number + var humanNumber; + var suffix = this.counterSuffix; + var end = this.totalCounterValue; + if (end >= 10000) { + humanNumber = utils.human_number(end, 1); + end = parseFloat(humanNumber.substr(0, humanNumber.length - 1)); + suffix = humanNumber[humanNumber.length - 1] + suffix; + } + var start = this.prevTotalCounterValue || 0; + if (start >= 10000) { + humanNumber = utils.human_number(start, 1); + start = parseFloat(humanNumber.substr(0, humanNumber.length - 1)); + } + if (end > start && this.ANIMATE) { + $({currentValue: start}).animate({currentValue: end}, { + duration: 1000, + start: function () { + self.$counter.addClass('o_kanban_grow'); + }, + step: function () { + self.$counter.html(self.counterPrefix + Math.round(this.currentValue) + suffix); + }, + complete: function () { + self.$counter.html(self.counterPrefix + this.currentValue + suffix); + self.$counter.removeClass('o_kanban_grow'); + }, + }); + } else { + this.$counter.html(this.counterPrefix + end + suffix); + } + }, + /** + * Notifies the new progressBar state so that if a full rerender occurs, the + * new progressBar that would replace this one will be initialized with + * current state, so that animations are correct. + * + * @private + */ + _notifyState: function () { + this.trigger_up('set_progress_bar_state', { + columnID: this.columnID, + values: { + groupCount: this.groupCount, + subgroupCounts: this.subgroupCounts, + totalCounterValue: this.totalCounterValue, + activeFilter: this.activeFilter, + }, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onProgressBarClick: function (ev) { + this.$clickedBar = $(ev.currentTarget); + var filter = this.$clickedBar.data('filter'); + this.activeFilter = (this.activeFilter === filter ? false : filter); + this._notifyState(); + this._render(); + }, + /** + * @private + * @param {Event} ev + */ + _onProgressBarParentClick: function (ev) { + if (ev.target !== ev.currentTarget) { + return; + } + this.activeFilter = false; + this._notifyState(); + this._render(); + }, +}); +return KanbanColumnProgressBar; +}); diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index 7e018327060..8ba0e3fb7c3 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -78,6 +78,12 @@ var KanbanController = BasicController.extend({ * @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)); }, /** @@ -164,7 +170,7 @@ var KanbanController = BasicController.extend({ var data = self.model.get(db_id); self.renderer.updateColumn(db_id, data); }); - }); + }); }).fail(this.reload.bind(this)); }, /** @@ -305,7 +311,7 @@ var KanbanController = BasicController.extend({ context: _.extend({default_name: name}, context), title: _t("Create"), disable_multiple_selection: true, - on_saved: function(record) { + on_saved: function (record) { add_record([record.res_id]); }, }).open(); diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index f714fc519df..400d73c0b7d 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -98,9 +98,25 @@ var KanbanModel = BasicModel.extend({ 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) { @@ -148,7 +164,8 @@ var KanbanModel = BasicModel.extend({ // 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 resID = self.localData[recordID].res_id; + 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++) { @@ -170,6 +187,7 @@ var KanbanModel = BasicModel.extend({ 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]; }); }, @@ -182,7 +200,15 @@ var KanbanModel = BasicModel.extend({ if (options && options.groupBy && !options.groupBy.length) { options.groupBy = this.defaultGroupedBy; } - return this._super(id, options); + 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); }, //-------------------------------------------------------------------------- @@ -190,6 +216,25 @@ var KanbanModel = BasicModel.extend({ //-------------------------------------------------------------------------- /** + * @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). * @@ -207,6 +252,34 @@ var KanbanModel = BasicModel.extend({ }); }, /** + * @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 @@ -253,8 +326,37 @@ var KanbanModel = BasicModel.extend({ } return $.when(); }, -}); + /** + * Reloads all progressbar data if the given id is a record's one. 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 !== 'record') { + 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, {onlyGroups: true}).then(function () { + return data; + }); + }); + } + element = this.localData[element.parentID]; + } + return def; + }, +}); return KanbanModel; - }); diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index 5d7be3d3636..49752ca2678 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -83,6 +83,10 @@ function transformQwebTemplate(node, fields) { var KanbanRenderer = BasicRenderer.extend({ className: 'o_kanban_view', + custom_events: _.extend({}, BasicRenderer.prototype.custom_events || {}, { + 'set_progress_bar_state': '_onSetProgressBarState', + }), + /** * @override */ @@ -100,6 +104,9 @@ var KanbanRenderer = BasicRenderer.extend({ viewType: 'kanban', }); this.columnOptions = _.extend({}, params.column_options, { qweb: this.qweb }); + if (this.columnOptions.hasProgressBar) { + this.columnOptions.progressBarStates = {}; + } this._setState(state); }, @@ -336,6 +343,24 @@ var KanbanRenderer = BasicRenderer.extend({ }); this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable; }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Updates progressbar internal states (necessary for animations) with + * received data. + * + * @private + * @param {OdooEvent} ev + */ + _onSetProgressBarState: function (ev) { + if (!this.columnOptions.progressBarStates[ev.data.columnID]) { + this.columnOptions.progressBarStates[ev.data.columnID] = {}; + } + _.extend(this.columnOptions.progressBarStates[ev.data.columnID], ev.data.values); + }, }); return KanbanRenderer; diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js index 55a90e83242..3e808de72e3 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -6,6 +6,7 @@ var core = require('web.core'); var KanbanModel = require('web.KanbanModel'); var KanbanRenderer = require('web.KanbanRenderer'); var KanbanController = require('web.KanbanController'); +var utils = require('web.utils'); var _lt = core._lt; @@ -21,6 +22,10 @@ var KanbanView = BasicView.extend({ Renderer: KanbanRenderer, }, viewType: 'kanban', + + /** + * @constructor + */ init: function (viewInfo, params) { this._super.apply(this, arguments); @@ -29,8 +34,20 @@ var KanbanView = BasicView.extend({ this.loadParams.limit = this.loadParams.limit || 40; this.loadParams.openGroupByDefault = true; this.loadParams.type = 'list'; - this.loadParams.groupBy = arch.attrs.default_group_by ? [arch.attrs.default_group_by] : (params.groupBy || []); + var progressBar; + utils.traverse(arch, function (n) { + var isProgressBar = (n.tag === 'progressbar'); + if (isProgressBar) { + progressBar = _.clone(n.attrs); + progressBar.colors = JSON.parse(progressBar.colors); + progressBar.sum = progressBar.sum || false; + } + return !isProgressBar; + }); + if (progressBar) { + this.loadParams.progressBar = progressBar; + } var activeActions = this.controllerParams.activeActions; activeActions = _.extend(activeActions, { @@ -44,6 +61,7 @@ var KanbanView = BasicView.extend({ deletable: activeActions.group_delete, group_creatable: activeActions.group_create, quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(viewInfo), + hasProgressBar: !!progressBar, }; this.rendererParams.record_options = { editable: activeActions.edit, @@ -56,10 +74,19 @@ var KanbanView = BasicView.extend({ this.controllerParams.readOnlyMode = false; this.controllerParams.hasButtons = true; }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} viewInfo + */ _isQuickCreateEnabled: function (viewInfo) { var groupBy = this.loadParams.groupBy[0]; groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined; - if(groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], viewInfo.fields[groupBy].type)) { + if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], viewInfo.fields[groupBy].type)) { return false; } if (!this.controllerParams.activeActions.create) { @@ -70,11 +97,6 @@ var KanbanView = BasicView.extend({ } return true; } - - }); - return KanbanView; - }); - |
