[IMP] web,crm: revamp grouped kanban mobile implementation

This commit revamps the grouped-kanban implementation for smaller
screens (aka. mobile) by making it more "responsive" and avoiding
mobile-specific variation. It makes the implementation simpler and
closer to what the user expects from the desktop version.

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.

Part of the SCSS revamp task-2704984

task-2883057

Part-of: odoo/odoo#94134
This commit is contained in:
Pierre Paridans
2022-06-22 11:05:20 +02:00
parent fbf7232edc
commit 2dd37982e6
8 changed files with 119 additions and 61 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">
@@ -43,7 +43,8 @@ html {
@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 {