[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:
Odoo's Mergebot
2022-06-22 11:05:21 +02:00
committed by GitHub
9 changed files with 140 additions and 62 deletions
@@ -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 {
+6 -4
View File
@@ -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 {