[IMP] web,crm: revamp mobile kanban implementation
This PR main purpose is to revamp the grouped Kanban implementation on mobile to provide a more responsive, smoother and closer to the desktop functionalities experience for the user. In a nutshell: - columns are displayed individually ; an horizontal scroll allows to switch to the other ones, "snapping" to the column (aka. carroussel-like). - each column takes 90% of the viewport's width to give a hint of its siblings. - each column scrolls (vertically) individually to avoid being lost when switching from one column to another. - folded columns are "virtually unfolded": they takes the same space as the other ones, but content is loaded on-demand. - some configuration modals are fullscreen for ease of use. Also, in this PR, the WebClient's layout has been standardized between Community and Enterprise to ease the maintenance and avoid redundancy. Part of the SCSS revamp task-2704984 task-2883057 closes odoo/odoo#94134 Related: odoo/enterprise#28652 Signed-off-by: Adrien Dieudonné (adr) <adr@odoo.com>
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
<t t-name="KanbanView.ForecastColumnQuickCreate">
|
||||
<div class="o_column_quick_create">
|
||||
<div class="o_quick_create_folded">
|
||||
<span class="o_kanban_add_column"><i class="fa fa-plus" role="img" t-att-aria-label="widget.addColumnLabel" t-att-title="widget.addColumnLabel"/></span>
|
||||
<span class="o_kanban_title"><t t-esc="widget.addColumnLabel"/></span>
|
||||
<button class="o_kanban_add_column btn btn-outline-secondary w-100">
|
||||
<i class="fa fa-plus" role="img" t-att-aria-label="widget.addColumnLabel" t-att-title="widget.addColumnLabel"/>
|
||||
<t t-out="widget.addColumnLabel"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -2,6 +2,7 @@ odoo.define('web.KanbanColumn', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var session = require('web.session');
|
||||
var Dialog = require('web.Dialog');
|
||||
var KanbanRecord = require('web.KanbanRecord');
|
||||
@@ -66,7 +67,7 @@ var KanbanColumn = Widget.extend({
|
||||
this.offset = 0;
|
||||
this.loadMoreCount = data.loadMoreCount;
|
||||
this.loadMoreOffset = data.loadMoreOffset;
|
||||
this.canBeFolded = this.folded;
|
||||
this.isMobile = config.device.isMobile;
|
||||
|
||||
if (options.hasProgressBar) {
|
||||
this.barOptions = {
|
||||
@@ -107,7 +108,7 @@ var KanbanColumn = Widget.extend({
|
||||
defs.push(this._addRecord(this.data_records[i]));
|
||||
}
|
||||
|
||||
if (this.recordsDraggable) {
|
||||
if (this.recordsDraggable && !config.device.isMobile) {
|
||||
this.$el.sortable({
|
||||
connectWith: '.o_kanban_group',
|
||||
containment: this.draggable ? false : 'parent',
|
||||
@@ -147,21 +148,23 @@ var KanbanColumn = Widget.extend({
|
||||
}
|
||||
});
|
||||
}
|
||||
this.$el.click(function (event) {
|
||||
if (self.folded) {
|
||||
self._onToggleFold(event);
|
||||
}
|
||||
});
|
||||
if (!config.device.isMobile) {
|
||||
this.$el.click(function (event) {
|
||||
if (self.folded) {
|
||||
self._onToggleFold(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.barOptions) {
|
||||
this.$el.addClass('o_kanban_has_progressbar');
|
||||
this.progressBar = this._getKanbanColumnProgressBar();
|
||||
defs.push(this.progressBar.appendTo(this.$header));
|
||||
}
|
||||
|
||||
var title = this.folded ? this.title + ' (' + this.data.count + ')' : this.title;
|
||||
var title = !config.device.isMobile && this.folded ? this.title + ' (' + this.data.count + ')' : this.title;
|
||||
this.$header.find('.o_column_title').text(title);
|
||||
|
||||
this.$el.toggleClass('o_column_folded', this.canBeFolded);
|
||||
this.$el.toggleClass('o_column_folded', !config.device.isMobile && this.folded);
|
||||
if (this.tooltipInfo) {
|
||||
this.$header.find('.o_kanban_header_title').tooltip({}).attr('data-original-title', this.tooltipInfo);
|
||||
}
|
||||
@@ -365,7 +368,11 @@ var KanbanColumn = Widget.extend({
|
||||
*/
|
||||
_onLoadMore: function (event) {
|
||||
event.preventDefault();
|
||||
this.trigger_up('kanban_load_column_records', { loadMoreOffset: this.loadMoreOffset });
|
||||
if (this.folded) {
|
||||
this.trigger_up('column_toggle_fold');
|
||||
} else {
|
||||
this.trigger_up('kanban_load_column_records', { loadMoreOffset: this.loadMoreOffset });
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -7,6 +7,7 @@ odoo.define('web.kanban_column_quick_create', function (require) {
|
||||
*/
|
||||
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
@@ -35,7 +36,7 @@ var ColumnQuickCreate = Widget.extend({
|
||||
this.applyExamplesText = options.applyExamplesText || _t("Use This For My Kanban");
|
||||
this.examples = options.examples;
|
||||
this.folded = true;
|
||||
this.isMobile = false;
|
||||
this.isMobile = config.device.isMobile;
|
||||
this.isFirstColumn = options.isFirstColumn;
|
||||
},
|
||||
/**
|
||||
@@ -204,6 +205,7 @@ var ColumnQuickCreate = Widget.extend({
|
||||
text: _t('Close'),
|
||||
}],
|
||||
size: "large",
|
||||
fullscreen: config.device.isMobile,
|
||||
title: _t("Kanban Examples"),
|
||||
}).open();
|
||||
dialog.on('closed', this, function () {
|
||||
|
||||
@@ -9,6 +9,7 @@ odoo.define('web.KanbanController', function (require) {
|
||||
|
||||
var BasicController = require('web.BasicController');
|
||||
var Context = require('web.Context');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Domain = require('web.Domain');
|
||||
@@ -50,6 +51,14 @@ var KanbanController = BasicController.extend({
|
||||
this.hasButtons = params.hasButtons;
|
||||
this.quickCreateEnabled = params.quickCreateEnabled;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async start() {
|
||||
await this._super(...arguments);
|
||||
const { groupedBy } = this.model.get(this.handle);
|
||||
this.el.classList.toggle("o_action_delegate_scroll", config.device.isMobile && groupedBy.length);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
@@ -72,6 +81,14 @@ var KanbanController = BasicController.extend({
|
||||
this.$buttons.appendTo($node);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async update() {
|
||||
await this._super(...arguments);
|
||||
const { groupedBy } = this.model.get(this.handle);
|
||||
this.el.classList.toggle("o_action_delegate_scroll", config.device.isMobile && groupedBy.length);
|
||||
},
|
||||
/**
|
||||
* In grouped mode, set 'Create' button as btn-secondary if there is no column
|
||||
* (except if we can't create new columns)
|
||||
|
||||
@@ -428,6 +428,13 @@
|
||||
padding: 0;
|
||||
background-color: $gray-100;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
min-height: initial;
|
||||
height: 100%;
|
||||
overflow: scroll hidden !important;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.o_kanban_record, .o_kanban_quick_create {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
@@ -441,7 +448,7 @@
|
||||
|
||||
// Kanban Grouped Layout - Column default
|
||||
.o_kanban_group {
|
||||
flex: 0 0 auto;
|
||||
flex: 0 0 $o-kanban-default-record-width + 2*$o-kanban-group-padding;
|
||||
padding: 0 $o-kanban-group-padding $o-kanban-group-padding $o-kanban-group-padding;
|
||||
background-color: inherit;
|
||||
|
||||
@@ -471,22 +478,16 @@
|
||||
.o_kanban_config {
|
||||
visibility: hidden;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
i {
|
||||
@include o-kanban-icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_load_more {
|
||||
padding: $o-kanban-record-margin 0;
|
||||
box-shadow: inset 0 10px 13px -13px black;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:not(.o_column_folded) {
|
||||
width: $o-kanban-default-record-width + 2*$o-kanban-group-padding;
|
||||
}
|
||||
|
||||
&.o_kanban_dragged {
|
||||
background-color: $o-main-text-color;
|
||||
|
||||
@@ -495,45 +496,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-sortable .o_kanban_header_title .o_column_title {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
// Kanban Grouped Layout - Column Folded
|
||||
.o_kanban_group.o_column_folded {
|
||||
@include o-kanban-slim-col;
|
||||
background-color: $gray-200;
|
||||
// don't visually fold on smaller screens (aka. mobile)
|
||||
@include media-breakpoint-up(md) {
|
||||
@include o-kanban-slim-col;
|
||||
background-color: $gray-200;
|
||||
|
||||
& + .o_kanban_group.o_column_folded {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.o_kanban_header_title {
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
|
||||
.o_column_title {
|
||||
@include o-kanban-v-title;
|
||||
& + .o_kanban_group.o_column_folded {
|
||||
margin-left: 1px;
|
||||
}
|
||||
.o_column_unfold {
|
||||
@include o-kanban-icon(1);
|
||||
}
|
||||
}
|
||||
|
||||
> .o_kanban_record, .o_kanban_quick_add, .o_kanban_config, .o_kanban_load_more {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
&:hover, &.o_kanban_hover {
|
||||
.o_kanban_header_title {
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
|
||||
.o_column_title {
|
||||
@include o-kanban-v-title;
|
||||
}
|
||||
|
||||
.o_column_unfold {
|
||||
@include o-kanban-icon(1);
|
||||
}
|
||||
}
|
||||
|
||||
> .o_kanban_record, .o_kanban_quick_add, .o_kanban_config, .o_kanban_load_more {
|
||||
display: none !important;
|
||||
}
|
||||
&:hover, &.o_kanban_hover {
|
||||
.o_kanban_header_title {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kanban Grouped Layout - "Create new column" column
|
||||
.o_column_quick_create {
|
||||
flex-basis: $o-kanban-small-record-width + 2*$o-kanban-group-padding;
|
||||
|
||||
.o_quick_create_folded {
|
||||
cursor: pointer;
|
||||
padding: 12px 16px;
|
||||
@@ -541,21 +547,15 @@
|
||||
font-weight: $font-weight-bold;
|
||||
@include o-hover-opacity(.7);
|
||||
@include o-hover-text-color($text-muted, $body-color);
|
||||
|
||||
.o_kanban_add_column {
|
||||
margin-right: $o-kanban-inside-hgutter;
|
||||
display: inline-block;
|
||||
padding: 10px 14px;
|
||||
background-color: rgba($text-muted, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.o_quick_create_unfolded {
|
||||
margin: $border-width ($o-kanban-inside-hgutter * .5) 0;
|
||||
padding: $o-kanban-inside-vgutter $o-kanban-inside-hgutter;
|
||||
width: $o-kanban-small-record-width;
|
||||
height: 100%;
|
||||
background-color: $o-view-background-color;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.o_kanban_header {
|
||||
height: 50px;
|
||||
@@ -579,6 +579,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.o_kanban_group, .o_column_quick_create {
|
||||
flex: 1 0 90%; // don't take full width to give a hint of next/previous column
|
||||
scroll-snap-align: center;
|
||||
overflow-y: scroll;
|
||||
|
||||
.o_kanban_header {
|
||||
position: sticky; // keep it visible
|
||||
top: 0;
|
||||
z-index: 10; // same as the sticky ControlPanel
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.o_kanban_header_title {
|
||||
margin-top: 0 !important; // avoid sticky glitch when scrolling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kanban UN-grouped Layout
|
||||
&.o_kanban_ungrouped {
|
||||
min-height: 100%;
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
.modal-body {
|
||||
&.o_act_window {
|
||||
padding: 0;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.o_action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_modal_header {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<span class="o_kanban_config dropdown">
|
||||
<a class="dropdown-toggle o-no-caret" data-toggle="dropdown" href="#"><i class="fa fa-gear" role="img" aria-label="Settings" title="Settings"/></a>
|
||||
<div class="dropdown-menu o_cursor_default" role="menu">
|
||||
<a role="menuitem" class="dropdown-item o_kanban_toggle_fold" href="#">Fold</a>
|
||||
<a t-if="!widget.isMobile" role="menuitem" class="dropdown-item o_kanban_toggle_fold" href="#">Fold</a>
|
||||
<t t-if="widget.grouped_by_m2o">
|
||||
<a t-if="widget.editable and widget.id" role="menuitem" class="dropdown-item o_column_edit" href="#">Edit Stage</a>
|
||||
<a t-if="widget.deletable and widget.id" role="menuitem" class="dropdown-item o_column_delete" href="#">Delete</a>
|
||||
@@ -39,14 +39,16 @@
|
||||
</t>
|
||||
|
||||
<t t-name="KanbanView.LoadMore">
|
||||
<a href="#">Load more... (<t t-esc="widget.loadMoreCount"/> remaining)</a>
|
||||
<button class="btn btn-outline-primary w-100 mt-4">Load more... (<t t-out="widget.loadMoreCount"/> remaining)</button>
|
||||
</t>
|
||||
|
||||
<t t-name="KanbanView.ColumnQuickCreate">
|
||||
<div class="o_column_quick_create">
|
||||
<div class="o_quick_create_folded">
|
||||
<span class="o_kanban_add_column"><i class="fa fa-plus" role="img" aria-label="Add column" title="Add column"/></span>
|
||||
<span class="o_kanban_title">Add a Column</span>
|
||||
<button class="o_kanban_add_column btn btn-outline-secondary w-100">
|
||||
<i class="fa fa-plus" role="img" aria-label="Add column" title="Add column"/>
|
||||
Add a Column
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_quick_create_unfolded">
|
||||
<div class="o_kanban_header">
|
||||
|
||||
@@ -18,6 +18,10 @@ $o-searchpanel-filter-default-color: #D59244;
|
||||
overflow: auto; // make the renderer and search panel scroll individually
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ html {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
> .o_action_manager {
|
||||
direction: ltr; //Define direction attribute here so when rtlcss preprocessor run, it converts it to rtl
|
||||
@@ -19,10 +20,9 @@ html {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
overflow: auto;
|
||||
|
||||
-ms-overflow-style: none; // IE and Edge
|
||||
scrollbar-width: none; // Firefox
|
||||
|
||||
@@ -40,6 +40,18 @@ html {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
// Made the o_action scroll instead of its o_content.
|
||||
// Except when the view wants to handle the scroll itself.
|
||||
&:not(.o_action_delegate_scroll) {
|
||||
overflow: auto;
|
||||
|
||||
> .o_content {
|
||||
overflow: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +79,11 @@ html {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Fix scrolling in modal on focus input due to a bug between Chrome (Android) and Bootstrap
|
||||
body.modal-open {
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
Reference in New Issue
Block a user