From 829003178e16134eda028c1caed32aeb15a187bb Mon Sep 17 00:00:00 2001 From: Romain Estievenart Date: Mon, 11 Feb 2019 10:32:28 +0000 Subject: [PATCH 1/4] [FIX] web: error on mobile when kanban groupedBy change Sometimes when the groupedBy view is changed the index of the current active column overflow the kanban tabs length. In this commit this effect is avoided by resetting the active kanban column to the first one when the groupedBy is changed. Task: 1893168 --- .../web/static/src/js/views/kanban/kanban_renderer_mobile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index b5aa4945364..b078bc6e3d2 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -201,7 +201,8 @@ KanbanRenderer.include({ var self = this; return this._super.apply(this, arguments).then(function () { if (self.state.groupedBy.length) { - return self._moveToGroup(self.activeColumnIndex); + // force first column for kanban view, because the groupedBy can be changed + return self._moveToGroup(0); } }); }, From 848ea507ec0b3aaf9d13ef1e969c3a4c706121e4 Mon Sep 17 00:00:00 2001 From: Romain Estievenart Date: Mon, 11 Feb 2019 11:03:15 +0000 Subject: [PATCH 2/4] [FIX] web: column data not loaded on mobile kanban view This commit apply those changes: - Avoid overflow widgets array by checking its boundaries when the user swipes on the kanban column. - Extract the success callback update layout to avoid duplicated code. Task: 1893168 --- .../js/views/kanban/kanban_renderer_mobile.js | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index b078bc6e3d2..6f733a2a1c1 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -114,10 +114,50 @@ KanbanRenderer.include({ this.$el.swipe({ excludedElements: ".o_kanban_mobile_tabs", swipeLeft: function () { - self._moveToGroup(self.activeColumnIndex + 1, self.ANIMATE); + var moveToIndex = self.activeColumnIndex + 1; + if (moveToIndex < self.widgets.length) { + self._moveToGroup(moveToIndex, self.ANIMATE); + } }, swipeRight: function () { - self._moveToGroup(self.activeColumnIndex - 1, self.ANIMATE); + var moveToIndex = self.activeColumnIndex - 1; + if (moveToIndex > -1) { + self._moveToGroup(moveToIndex, self.ANIMATE); + } + } + }); + }, + /** + * Update the kanban layout + * @private + * @param {boolean} [animate=false] set to true to animate + */ + _layoutUpdate : function (animate) { + var self = this; + var moveToIndex = this.activeColumnIndex; + // update the columns and tabs positions (optionally with an animation) + var updateFunc = animate ? 'animate' : 'css'; + this.$('.o_kanban_mobile_tab, .o_kanban_group').removeClass('o_current'); + _.each(self.widgets, function (column, index) { + var columnID = column.id || column.db_id; + var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]'); + var $tab = self.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]'); + if (index === moveToIndex - 1) { + $column[updateFunc]({left: '-100%'}); + $tab[updateFunc]({left: '0%'}); + } else if (index === moveToIndex + 1) { + $column[updateFunc]({left: '100%'}); + $tab[updateFunc]({left: '100%'}); + } else if (index === moveToIndex) { + $column[updateFunc]({left: '0%'}); + $tab[updateFunc]({left: '50%'}); + $tab.add($column).addClass('o_current'); + } else if (index < moveToIndex) { + $column.css({left: '-100%'}); + $tab[updateFunc]({left: '-100%'}); + } else if (index > moveToIndex) { + $column.css({left: '100%'}); + $tab[updateFunc]({left: '200%'}); } }); }, @@ -133,6 +173,7 @@ KanbanRenderer.include({ _moveToGroup: function (moveToIndex, animate) { var self = this; if (moveToIndex < 0 || moveToIndex >= this.widgets.length) { + this._layoutUpdate(animate); return $.when(); } var def = $.Deferred(); @@ -141,31 +182,7 @@ KanbanRenderer.include({ this.trigger_up('kanban_load_records', { columnID: column.db_id, onSuccess: function () { - // update the columns and tabs positions (optionally with an animation) - var updateFunc = animate ? 'animate' : 'css'; - self.$('.o_kanban_mobile_tab, .o_kanban_group').removeClass('o_current'); - _.each(self.widgets, function (column, index) { - var columnID = column.id || column.db_id; - var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]'); - var $tab = self.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]'); - if (index === moveToIndex - 1) { - $column[updateFunc]({left: '-100%'}); - $tab[updateFunc]({left: '0%'}); - } else if (index === moveToIndex + 1) { - $column[updateFunc]({left: '100%'}); - $tab[updateFunc]({left: '100%'}); - } else if (index === moveToIndex) { - $column[updateFunc]({left: '0%'}); - $tab[updateFunc]({left: '50%'}); - $tab.add($column).addClass('o_current'); - } else if (index < moveToIndex) { - $column.css({left: '-100%'}); - $tab[updateFunc]({left: '-100%'}); - } else if (index > moveToIndex) { - $column.css({left: '100%'}); - $tab[updateFunc]({left: '200%'}); - } - }); + self._layoutUpdate(animate); def.resolve(); }, }); From 1fca14bfa16858145647f64c4a3c7aeffad7bf06 Mon Sep 17 00:00:00 2001 From: Romain Estievenart Date: Mon, 11 Feb 2019 11:11:43 +0000 Subject: [PATCH 3/4] [REF] web: cleanup the layout update for the mobile kanban view The goal of this refactoring is mainly to improve the code readability of the layout update in the mobile kanban renderer by focusing on each parts separetaly: * mark current column as the active one. * compute the title tabs' position. * compute the content column's position. Task: 1893168 --- .../js/views/kanban/kanban_renderer_mobile.js | 115 +++++++++++++----- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index 6f733a2a1c1..dcd8b1c7cda 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -103,11 +103,89 @@ KanbanRenderer.include({ // Private //-------------------------------------------------------------------------- + /** + * Update the columns positions + * + * @private + * @param {boolean} [animate=false] set to true to animate + */ + _computeColumnPosition: function (animate) { + if (this.widgets.length) { + var self = this; + var moveToIndex = this.activeColumnIndex; + var updateFunc = animate ? 'animate' : 'css'; + _.each(this.widgets, function (column, index) { + var columnID = column.id || column.db_id; + var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]'); + if (index === moveToIndex - 1) { + $column[updateFunc]({left: '-100%'}); + } else if (index === moveToIndex + 1) { + $column[updateFunc]({left: '100%'}); + } else if (index === moveToIndex) { + $column[updateFunc]({left: '0%'}); + } else if (index < moveToIndex) { + $column.css({left: '-100%'}); + } else if (index > moveToIndex) { + $column.css({left: '100%'}); + } + }); + } + }, + + /** + * Define the o_current class to the current selected kanban (column & tab) + * + * @private + */ + _computeCurrentColumn: function () { + if (this.widgets.length) { + var column = this.widgets[this.activeColumnIndex]; + if (!column) { + return; + } + var columnID = column.id || column.db_id; + this.$('.o_kanban_mobile_tab.o_current, .o_kanban_group.o_current') + .removeClass('o_current'); + this.$('.o_kanban_group[data-id="' + columnID + '"], ' + + '.o_kanban_mobile_tab[data-id="' + columnID + '"]') + .addClass('o_current'); + } + }, + + /** + * Update the tabs positions + * + * @private + * @param {boolean} [animate=false] set to true to animate + */ + _computeTabPosition: function (animate) { + if (this.widgets.length) { + var self = this; + var moveToIndex = this.activeColumnIndex; + // update the columns and tabs positions (optionally with an animation) + var updateFunc = animate ? 'animate' : 'css'; + _.each(this.widgets, function (column, index) { + var columnID = column.id || column.db_id; + var $tab = self.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]'); + if (index === moveToIndex - 1) { + $tab[updateFunc]({left: '0%'}); + } else if (index === moveToIndex + 1) { + $tab[updateFunc]({left: '100%'}); + } else if (index === moveToIndex) { + $tab[updateFunc]({left: '50%'}); + } else if (index < moveToIndex) { + $tab[updateFunc]({left: '-100%'}); + } else if (index > moveToIndex) { + $tab[updateFunc]({left: '200%'}); + } + }); + } + }, + /** * Enables swipe event on the current column * * @private - * @param {KanbanColumn} column */ _enableSwipe: function () { var self = this; @@ -127,40 +205,19 @@ KanbanRenderer.include({ } }); }, + /** * Update the kanban layout + * * @private * @param {boolean} [animate=false] set to true to animate */ _layoutUpdate : function (animate) { - var self = this; - var moveToIndex = this.activeColumnIndex; - // update the columns and tabs positions (optionally with an animation) - var updateFunc = animate ? 'animate' : 'css'; - this.$('.o_kanban_mobile_tab, .o_kanban_group').removeClass('o_current'); - _.each(self.widgets, function (column, index) { - var columnID = column.id || column.db_id; - var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]'); - var $tab = self.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]'); - if (index === moveToIndex - 1) { - $column[updateFunc]({left: '-100%'}); - $tab[updateFunc]({left: '0%'}); - } else if (index === moveToIndex + 1) { - $column[updateFunc]({left: '100%'}); - $tab[updateFunc]({left: '100%'}); - } else if (index === moveToIndex) { - $column[updateFunc]({left: '0%'}); - $tab[updateFunc]({left: '50%'}); - $tab.add($column).addClass('o_current'); - } else if (index < moveToIndex) { - $column.css({left: '-100%'}); - $tab[updateFunc]({left: '-100%'}); - } else if (index > moveToIndex) { - $column.css({left: '100%'}); - $tab[updateFunc]({left: '200%'}); - } - }); + this._computeCurrentColumn(); + this._computeTabPosition(animate); + this._computeColumnPosition(animate); }, + /** * Moves to the given kanban column * @@ -188,6 +245,7 @@ KanbanRenderer.include({ }); return def; }, + /** * @override * @private @@ -210,6 +268,7 @@ KanbanRenderer.include({ })).prependTo(fragment); return result; }, + /** * @override * @private From 6ac1ce85793af9d64870a3558f75d72248936972 Mon Sep 17 00:00:00 2001 From: Romain Estievenart Date: Mon, 11 Feb 2019 11:22:49 +0000 Subject: [PATCH 4/4] [IMP] web: mobile kanban tabs UX The mobile kanban title tabs required to move from column to column 1-by-1 to go from the first to the last column. Focusing everytime to only the active column has ugly side effects when the titles are long (eg. text overlapping). This commit change the mobile kanban tabs UX by allowing to scroll through them and go directly from a column A to B. It also improve the readability independently of the title's length. This behavior is heavily inspired by Material Design. See: https://material.io/design/components/tabs.html Task: 1893168 --- .../js/views/kanban/kanban_renderer_mobile.js | 80 ++++++++++++++----- .../static/src/scss/kanban_view_mobile.scss | 8 +- .../static/tests/views/kanban_mobile_tests.js | 24 +++++- 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index dcd8b1c7cda..a9576e9836f 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -45,6 +45,7 @@ KanbanRenderer.include({ $column.scrollLeft(this._scrollPosition.left); $column.scrollTop(this._scrollPosition.top); } + this._computeTabPosition(); this._super.apply(this, arguments); }, /** @@ -135,7 +136,7 @@ KanbanRenderer.include({ /** * Define the o_current class to the current selected kanban (column & tab) * - * @private + * @private */ _computeCurrentColumn: function () { if (this.widgets.length) { @@ -156,29 +157,52 @@ KanbanRenderer.include({ * Update the tabs positions * * @private - * @param {boolean} [animate=false] set to true to animate */ - _computeTabPosition: function (animate) { + _computeTabPosition: function () { + this._computeTabJustification(); + this._computeTabScrollPosition(); + }, + + /** + * Update the tabs positions + * + * @private + */ + _computeTabScrollPosition: function () { + if (this.widgets.length) { + var lastItemIndex = this.widgets.length - 1; + var moveToIndex = this.activeColumnIndex; + var scrollToLeft = 0; + for (var i = 0; i < moveToIndex; i++) { + var columnWidth = this._getTabWidth(this.widgets[i]); + // apply + if (moveToIndex !== lastItemIndex && i === moveToIndex - 1) { + var partialWidth = 0.75; + scrollToLeft += columnWidth * partialWidth; + } else { + scrollToLeft += columnWidth; + } + } + // Apply the scroll x on the tabs + this.$('.o_kanban_mobile_tabs').scrollLeft(scrollToLeft); + } + }, + + /** + * Compute the justify content of the kanban tab headers + * + * @private + */ + _computeTabJustification: function () { if (this.widgets.length) { var self = this; - var moveToIndex = this.activeColumnIndex; - // update the columns and tabs positions (optionally with an animation) - var updateFunc = animate ? 'animate' : 'css'; - _.each(this.widgets, function (column, index) { - var columnID = column.id || column.db_id; - var $tab = self.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]'); - if (index === moveToIndex - 1) { - $tab[updateFunc]({left: '0%'}); - } else if (index === moveToIndex + 1) { - $tab[updateFunc]({left: '100%'}); - } else if (index === moveToIndex) { - $tab[updateFunc]({left: '50%'}); - } else if (index < moveToIndex) { - $tab[updateFunc]({left: '-100%'}); - } else if (index > moveToIndex) { - $tab[updateFunc]({left: '200%'}); - } - }); + // Use to compute the sum of the width of all tab + var widthChilds = this.widgets.reduce(function (total, column) { + return total + self._getTabWidth(column); + }, 0); + // Apply a space around between child if the parent length is higher then the sum of the child width + var $tabs = this.$('.o_kanban_mobile_tabs'); + $tabs.toggleClass('justify-content-around', $tabs.outerWidth() >= widthChilds); } }, @@ -206,6 +230,18 @@ KanbanRenderer.include({ }); }, + /** + * Retrieve the outerWidth of a given widget column + * + * @param {KanbanColumn} column + * @returns {integer} outerWidth of the found column + * @private + */ + _getTabWidth : function (column) { + var columnID = column.id || column.db_id; + return this.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]').outerWidth(); + }, + /** * Update the kanban layout * @@ -214,7 +250,7 @@ KanbanRenderer.include({ */ _layoutUpdate : function (animate) { this._computeCurrentColumn(); - this._computeTabPosition(animate); + this._computeTabPosition(); this._computeColumnPosition(animate); }, diff --git a/addons/web/static/src/scss/kanban_view_mobile.scss b/addons/web/static/src/scss/kanban_view_mobile.scss index 53dc200f398..e464fd88b8a 100644 --- a/addons/web/static/src/scss/kanban_view_mobile.scss +++ b/addons/web/static/src/scss/kanban_view_mobile.scss @@ -5,15 +5,16 @@ overflow: hidden; .o_kanban_mobile_tabs { + display:flex; + width: 100%; height: $o-kanban-mobile-tabs-height; position: relative; + overflow-x: auto; background-color: #5E5E5E; .o_kanban_mobile_tab { - @include o-position-absolute($left: 100%); - transform: translateX(-50%); height: $o-kanban-mobile-tabs-height; - padding: 10px; + padding: 10px 20px; font-size: 14px; color: white; @@ -23,6 +24,7 @@ } .o_column_title { white-space: nowrap; + text-transform: uppercase; } } } diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js index a564ff7f8ed..cc8354432e3 100644 --- a/addons/web/static/tests/views/kanban_mobile_tests.js +++ b/addons/web/static/tests/views/kanban_mobile_tests.js @@ -56,7 +56,7 @@ QUnit.module('Views', { QUnit.module('KanbanView Mobile'); QUnit.test('mobile grouped rendering', function (assert) { - assert.expect(11); + assert.expect(13); var kanban = createView({ View: KanbanView, @@ -110,10 +110,16 @@ QUnit.module('Views', { }).get(); assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id"); + // kanban tabs with tab with lower width then available with have justify-content-around class + assert.containsN(kanban, '.o_kanban_mobile_tabs.justify-content-around', 1, + "should have justify-content-around class"); + assert.hasClass(kanban.$('.o_kanban_mobile_tabs'), 'justify-content-around', + "the mobile tabs have the class 'justify-content-around'"); + kanban.destroy(); }); QUnit.test('mobile grouped with undefined column', function (assert) { - assert.expect(3); + assert.expect(5); var kanban = createView({ View: KanbanView, @@ -141,10 +147,16 @@ QUnit.module('Views', { }).get(); assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id"); + // kanban tabs with tab with lower width then available with have justify-content-around class + assert.containsN(kanban, '.o_kanban_mobile_tabs.justify-content-around', 1, + "should have justify-content-around class"); + assert.hasClass(kanban.$('.o_kanban_mobile_tabs'), 'justify-content-around', + "the mobile tabs have the class 'justify-content-around'"); + kanban.destroy(); }); QUnit.test('mobile grouped on many2one rendering', function (assert) { - assert.expect(3); + assert.expect(5); var kanban = createView({ View: KanbanView, @@ -172,6 +184,12 @@ QUnit.module('Views', { }).get(); assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id"); + // kanban tabs with tab with lower width then available with have justify-content-around class + assert.containsN(kanban, '.o_kanban_mobile_tabs.justify-content-around', 1, + "should have justify-content-around class"); + assert.hasClass(kanban.$('.o_kanban_mobile_tabs'), 'justify-content-around', + "the mobile tabs have the class 'justify-content-around'"); + kanban.destroy(); });