diff --git a/addons/web/static/src/js/views/kanban/kanban_column_quick_create.js b/addons/web/static/src/js/views/kanban/kanban_column_quick_create.js index b2a38d4e53e..3fe3a556782 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column_quick_create.js +++ b/addons/web/static/src/js/views/kanban/kanban_column_quick_create.js @@ -6,6 +6,7 @@ odoo.define('web.kanban_column_quick_create', function (require) { * create kanban columns directly from the Kanban view. */ +var config = require('web.config'); var core = require('web.core'); var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); @@ -34,6 +35,7 @@ var ColumnQuickCreate = Widget.extend({ this._super.apply(this, arguments); this.examples = options.examples; this.folded = true; + this.isMobile = config.device.isMobile; }, /** * @override @@ -101,9 +103,11 @@ var ColumnQuickCreate = Widget.extend({ */ _cancel: function () { if (!this.folded) { - this.folded = true; this.$input.val(''); - this._update(); + if (!this.isMobile) { + this.folded = true; + this._update(); + } } }, /** 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 66d473095be..95012eeebfc 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -238,6 +238,7 @@ var KanbanController = BasicController.extend({ }).then(function () { self._updateButtons(); self.renderer.quickCreateToggleFold(); + self.renderer.trigger_up("quick_create_column_created"); }); }); }, 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 377131c16ba..43de802741a 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 @@ -19,8 +19,12 @@ if (!config.device.isMobile) { } KanbanRenderer.include({ + custom_events: _.extend({}, KanbanRenderer.prototype.custom_events || {}, { + quick_create_column_created: '_onColumnAdded', + }), events: _.extend({}, KanbanRenderer.prototype.events, { 'click .o_kanban_mobile_tab': '_onMobileTabClicked', + 'click .o_kanban_mobile_add_column': '_onMobileQuickCreateClicked', }), ANIMATE: true, // allows to disable animations for the tests /** @@ -73,12 +77,18 @@ KanbanRenderer.include({ /** * Displays the quick create record in the active column + * override to open quick create record in current active column * + * @override * @returns {Promise} */ addQuickCreate: function () { + if(this._canCreateColumn() && !this.quickCreate.folded) { + this._onMobileQuickCreateClicked(); + } return this.widgets[this.activeColumnIndex].addQuickCreate(); }, + /** * Overrides to restore the left property and the scrollTop on the updated * column, and to enable the swipe handlers @@ -86,27 +96,28 @@ KanbanRenderer.include({ * @override */ updateColumn: function (localID) { - var self = this; var index = _.findIndex(this.widgets, {db_id: localID}); var $column = this.widgets[index].$el; - var left = $column.css('left'); - var right = $column.css('right'); var scrollTop = $column.scrollTop(); - return this._super.apply(this, arguments).then(function () { - $column = self.widgets[index].$el; - if (_t.database.parameters.direction === 'rtl') { - $column.css({right: right}); - } else { - $column.css({left: left}); - } - $column.scrollTop(scrollTop); // required when clicking on 'Load More' - }); + return this._super.apply(this, arguments) + .then(() => this._layoutUpdate(false)) + // required when clicking on 'Load More' + .then(() => $column.scrollTop(scrollTop)); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + /** + * Check if we use the quick create on mobile + * @returns {boolean} + * @private + */ + _canCreateColumn: function() { + return this.quickCreateEnabled && this.quickCreate && this.widgets.length; + }, + /** * Update the columns positions * @@ -115,45 +126,28 @@ KanbanRenderer.include({ */ _computeColumnPosition: function (animate) { if (this.widgets.length) { - var self = this; - var moveToIndex = this.activeColumnIndex; - var updateFunc = animate ? 'animate' : 'css'; - var rtl = _t.database.parameters.direction === 'rtl'; - _.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) { - if (rtl) { - $column[updateFunc]({right: '-100%'}); - } else { - $column[updateFunc]({left: '-100%'}); - } - } else if (index === moveToIndex + 1) { - if (rtl) { - $column[updateFunc]({right: '100%'}); - } else { - $column[updateFunc]({left: '100%'}); - } - } else if (index === moveToIndex) { - if (rtl) { - $column[updateFunc]({right: '0%'}); - } else { - $column[updateFunc]({left: '0%'}); - } - } else if (index < moveToIndex) { - if (rtl) { - $column.css({right: '-100%'}); - } else { - $column.css({left: '-100%'}); - } - } else if (index > moveToIndex) { - if (rtl) { - $column.css({right: '100%'}); - } else { - $column.css({left: '100%'}); - } - } - }); + // check rtl to compute correct css value + const rtl = _t.database.parameters.direction === 'rtl'; + + // display all o_kanban_group + this.$('.o_kanban_group').show(); + + const $columnAfter = this._toNode(this.widgets.filter((widget, index) => index > this.activeColumnIndex)); + const promiseAfter = this._updateColumnCss($columnAfter, rtl ? {right: '100%'} : {left: '100%'}, animate); + + const $columnBefore = this._toNode(this.widgets.filter((widget, index) => index < this.activeColumnIndex)); + const promiseBefore = this._updateColumnCss($columnBefore, rtl ? {right: '-100%'} : {left: '-100%'}, animate); + + const $columnCurrent = this._toNode(this.widgets.filter((widget, index) => index === this.activeColumnIndex)); + const promiseCurrent = this._updateColumnCss($columnCurrent, rtl ? {right: '0%'} : {left: '0%'}, animate); + + promiseAfter + .then(promiseBefore) + .then(promiseCurrent) + .then(() => { + $columnAfter.hide(); + $columnBefore.hide(); + }); } }, @@ -227,7 +221,7 @@ KanbanRenderer.include({ }, 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); + $tabs.toggleClass('justify-content-between', $tabs.outerWidth() >= widthChilds); } }, @@ -308,14 +302,18 @@ KanbanRenderer.include({ this._enableSwipe(); return Promise.resolve(); }, - + /** + * override to avoid display of example background + */ + _renderExampleBackground: function () {}, /** * @override * @private */ _renderGrouped: function (fragment) { var self = this; - this._super.apply(this, arguments); + var newFragment = document.createDocumentFragment(); + this._super.apply(this, [newFragment]); this.defs.push(Promise.all(this.defs).then(function () { var data = []; _.each(self.state.data, function (group) { @@ -327,8 +325,13 @@ KanbanRenderer.include({ } }); + var kanbanColumnContainer = document.createElement('div'); + kanbanColumnContainer.classList.add('o_kanban_columns_content'); + kanbanColumnContainer.appendChild(newFragment); + fragment.appendChild(kanbanColumnContainer); $(qweb.render('KanbanView.MobileTabs', { data: data, + quickCreateEnabled: self._canCreateColumn() })).prependTo(fragment); })); }, @@ -343,19 +346,76 @@ KanbanRenderer.include({ if (self.state.groupedBy.length) { // force first column for kanban view, because the groupedBy can be changed return self._moveToGroup(0); + } else { + if(self._canCreateColumn()) { + self._onMobileQuickCreateClicked(); + } + return Promise.resolve(); } }); }, + /** + * Retrieve the Jquery node (.o_kanban_group) for a list of a given widgets + * + * @private + * @param widgets + * @returns {jQuery} the matching .o_kanban_group widgets + */ + _toNode: function (widgets) { + const selectorCss = widgets + .map(widget => '.o_kanban_group[data-id="' + (widget.id || widget.db_id) + '"]') + .join(', '); + return this.$(selectorCss); + }, + + /** + * Update the given column to the updated positions + * + * @private + * @param $column The jquery column + * @param cssProperties Use to update column + * @param {boolean} [animate=false] set to true to animate + * @return {Promise} + */ + _updateColumnCss: function ($column, cssProperties, animate) { + if (animate) { + return new Promise(resolve => $column.animate(cssProperties, 'fast', resolve)); + } else { + $column.css(cssProperties); + return Promise.resolve(); + } + }, + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- + /** + * @private + */ + _onColumnAdded: function () { + this._computeTabPosition(); + if(this._canCreateColumn() && !this.quickCreate.folded) { + this.quickCreate.toggleFold(); + } + }, + + /** + * @private + */ + _onMobileQuickCreateClicked: function() { + this.$('.o_kanban_group').toggle(); + this.quickCreate.toggleFold(); + }, /** * @private * @param {MouseEvent} event */ _onMobileTabClicked: function (event) { + if(this._canCreateColumn() && !this.quickCreate.folded) { + this.quickCreate.toggleFold(); + } this._moveToGroup($(event.currentTarget).index(), true); }, }); 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 cc0de646d2d..3a3dd2a1cd6 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -61,7 +61,7 @@ var KanbanView = BasicView.extend({ editable: activeActions.group_edit, deletable: activeActions.group_delete, archivable: archAttrs.archivable ? !!JSON.parse(archAttrs.archivable) : true, - group_creatable: activeActions.group_create && !config.device.isMobile, + group_creatable: activeActions.group_create, quickCreateView: archAttrs.quick_create_view || null, recordsDraggable: archAttrs.records_draggable ? !!JSON.parse(archAttrs.records_draggable) : true, hasProgressBar: !!progressBar, diff --git a/addons/web/static/src/scss/kanban_view_mobile.scss b/addons/web/static/src/scss/kanban_view_mobile.scss index 9c938766833..3deb81a38cd 100644 --- a/addons/web/static/src/scss/kanban_view_mobile.scss +++ b/addons/web/static/src/scss/kanban_view_mobile.scss @@ -4,31 +4,51 @@ position: relative; overflow-x: hidden; - .o_kanban_mobile_tabs { - display:flex; + .o_kanban_mobile_tabs_container { + position: sticky; + display: flex; + justify-content: space-between; width: 100%; - height: $o-kanban-mobile-tabs-height; - position: relative; - overflow-x: auto; + top: 0; + z-index: 1; background-color: #5E5E5E; - .o_kanban_mobile_tab { + .o_kanban_mobile_add_column { height: $o-kanban-mobile-tabs-height; - padding: 10px 20px; - font-size: 14px; + padding: 10px; + border-left: grey 1px solid; color: white; + font-size: 14px; + } - &.o_current { - font-weight: bold; - border-bottom: 3px solid $o-brand-primary; - } - .o_column_title { - white-space: nowrap; - text-transform: uppercase; + .o_kanban_mobile_tabs { + position: relative; + display: flex; + width: 100%; + height: $o-kanban-mobile-tabs-height; + overflow-x: auto; + + .o_kanban_mobile_tab { + height: $o-kanban-mobile-tabs-height; + padding: 10px 20px; + font-size: 14px; + color: white; + + &.o_current { + font-weight: bold; + border-bottom: 3px solid $o-brand-primary; + } + + .o_column_title { + white-space: nowrap; + text-transform: uppercase; + } } } } - + .o_kanban_columns_content { + position: relative; + } // [class] to get same specificity as elsewhere (kanban_view.less) &[class] .o_kanban_group:not(.o_column_folded) { @include o-position-absolute($top: $o-kanban-mobile-tabs-height, $left: 0, $bottom: 0); @@ -58,4 +78,13 @@ } } } + .o_kanban_view .o_column_quick_create { + .o_quick_create_folded { + display: none !important; + } + .o_quick_create_unfolded{ + width: 100%; + } + } } + diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml index 77d2d319d70..9d998677dc4 100644 --- a/addons/web/static/src/xml/kanban.xml +++ b/addons/web/static/src/xml/kanban.xml @@ -56,8 +56,8 @@ - Esc to discard - + Esc to discard + @@ -319,12 +319,17 @@ -
- -
- +
+
+ +
+ +
+
+
+
- +
diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js index ecc986eb0e4..dbd4ac95af7 100644 --- a/addons/web/static/tests/views/kanban_mobile_tests.js +++ b/addons/web/static/tests/views/kanban_mobile_tests.js @@ -112,11 +112,11 @@ 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 tabs with tab with lower width then available with have justify-content-between class + assert.containsN(kanban, '.o_kanban_mobile_tabs.justify-content-between', 1, + "should have justify-content-between class"); + assert.hasClass(kanban.$('.o_kanban_mobile_tabs'), 'justify-content-between', + "the mobile tabs have the class 'justify-content-between'"); kanban.destroy(); }); @@ -167,7 +167,7 @@ QUnit.module('Views', { // first column should be undefined with framework unique identifier assert.containsN(kanban, '.o_kanban_group', 3, "should have 3 columns"); - assert.containsOnce(kanban, '.o_kanban_mobile_tabs + .o_kanban_group[data-id^="partner_"]', + assert.containsOnce(kanban, '.o_kanban_columns_content .o_kanban_group:first-child[data-id^="partner_"]', "Undefined column should be first and have unique framework identifier as data-id"); // kanban column should match kanban mobile tabs @@ -179,11 +179,11 @@ 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 tabs with tab with lower width then available with have justify-content-between class + assert.containsN(kanban, '.o_kanban_mobile_tabs.justify-content-between', 1, + "should have justify-content-between class"); + assert.hasClass(kanban.$('.o_kanban_mobile_tabs'), 'justify-content-between', + "the mobile tabs have the class 'justify-content-between'"); kanban.destroy(); }); @@ -217,13 +217,88 @@ 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 tabs with tab with lower width then available with have justify-content-between class + assert.containsN(kanban, '.o_kanban_mobile_tabs.justify-content-between', 1, + "should have justify-content-between class"); + assert.hasClass(kanban.$('.o_kanban_mobile_tabs'), 'justify-content-between', + "the mobile tabs have the class 'justify-content-between'"); kanban.destroy(); }); + + QUnit.test('mobile quick create column view rendering', async function (assert) { + assert.expect(12); + + var kanban = await createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
', + domain: [['product_id', '!=', false]], + groupBy: ['product_id'], + }); + + // basic rendering tests + assert.containsN(kanban, '.o_kanban_group', 2, "should have 2 columns"); + assert.hasClass(kanban.$('.o_kanban_view > .o_kanban_mobile_tabs_container > .o_kanban_mobile_tabs > div:last'), 'o_kanban_mobile_add_column', + "should have column quick create tab and should be displayed as last tab"); + assert.hasClass(kanban.$('.o_kanban_mobile_tab:first'), 'o_current', + "should have first tab as active tab with class 'o_current'"); + assert.hasClass(kanban.$('.o_kanban_group:first'), 'o_current', + "should have first group as active group with class 'o_current'"); + assert.hasClass(kanban.$('.o_kanban_group:first'), 'o_current', + "should have first column as active column with left 0"); + + // quick create record in first column(tab) + await testUtils.dom.click(kanban.$('.o-kanban-button-new')); + assert.hasClass(kanban.$('.o_kanban_group[data-id="3"].o_current > div:nth(1)'), 'o_kanban_quick_create', + "should open record quick create when clicking on create button in first column"); + + // quick create record not allowed in quick create column tab + // clicking quick create record should move to first column and create record in first column + await testUtils.dom.click(kanban.$('.o_kanban_mobile_tab:last')); + await testUtils.dom.click(kanban.$('.o-kanban-button-new')); + assert.hasClass(kanban.$('.o_kanban_group[data-id="5"].o_current > div:nth(1)'), 'o_kanban_quick_create', + "should open record quick create when clicking on create button in new column quick create"); + + // new quick create column + await testUtils.dom.click(kanban.$('.o_kanban_mobile_add_column')); + assert.isVisible(kanban.$('.o_quick_create_unfolded'), "kanban quick create should be unfolded by default"); + assert.isVisible(kanban.$('.o_column_quick_create input'), "the quick create column input should be visible"); + assert.containsNone(kanban, '.o_kanban_examples', "Should not have See Examples link in mobile"); + assert.containsNone(kanban, '.o_discard_msg', "Should not have Esc to Discard in mobile kanban"); + kanban.$('.o_column_quick_create input').val('msh'); + await testUtils.dom.click(kanban.$('.o_column_quick_create button.o_kanban_add')); + + assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(msh)').length, 1, + "the last column(tab) should be the newly created one"); + kanban.destroy(); + }); + + QUnit.test('mobile no quick create column when grouping on non m2o field', async function (assert) { + assert.expect(2); + + var kanban = await createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '
' + + '
' + + '
' + + '
', + groupBy: ['int_field'], + }); + + assert.containsNone(kanban, '.o_kanban_mobile_add_column', "should not have the add column button"); + assert.containsNone(kanban.$('.o_column_quick_create'), + "should not have column quick create tab as we grouped records on integer field"); + kanban.destroy(); + }); }); });