[MOV] web: transfer settings touch customization to community

This commit adapts codebase following the move and integration of the
touch device detection and customization (through the o_touch_device
class) in community.

X-original-commit: f3adc2b0f70feb80292f6c755d53690e49647dbb
Part-of: odoo/odoo#101185
Co-authored-by: Bruno Boi <boi@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2022-09-26 22:48:44 +02:00
committed by Bruno Boi
co-authored by Bruno Boi
parent 156d41dbda
commit 3f15989e15
7 changed files with 451 additions and 173 deletions
-1
View File
@@ -230,7 +230,6 @@ This module provides the core of the Odoo Web Client.
'web/static/src/legacy/scss/attachment_preview.scss',
'web/static/src/legacy/scss/base_document_layout.scss',
'web/static/src/legacy/scss/special_fields.scss',
'web/static/src/legacy/scss/base_settings.scss',
'web/static/src/legacy/scss/fields_extra.scss',
'web/static/src/legacy/scss/form_view_extra.scss',
'web/static/src/legacy/scss/list_view_extra.scss',
@@ -1,167 +0,0 @@
// MIXINS
@mixin o-base-settings-horizontal-padding($padding-base: $input-btn-padding-y-sm) {
padding: $padding-base $o-horizontal-padding;
@include media-breakpoint-up(xl) {
padding-left: $o-horizontal-padding*2;;
}
}
// Use a very specif selector to overwrite generic form-view rules
.o_form_view.o_form_nosheet.o_base_settings {
display: flex;
flex-flow: column nowrap;
padding: 0px;
}
// BASE SETTINGS LAYOUT
.o_base_settings {
height: 100%;
.o_control_panel {
flex: 0 0 auto;
.o_panel {
display: flex;
flex-flow: row wrap;
width: 100%;
.o_settings_title, .o_setting_search {
flex: 1 0 auto;
width: map-get($o-extra-grid-breakpoints, vsm) / 2;
}
.o_setting_search {
position: relative;
.searchInput {
border: 0px;
max-width: 400px;
border-bottom: 1px solid $border-color;
box-shadow: none;
}
.searchIcon {
@include o-position-absolute($right: 0);
color: map-get($grays, '700');
}
&:focus-within {
.searchInput {
border-bottom-color: $primary;
}
}
}
}
.o_form_statusbar {
padding: 0;
margin: 0;
border: 0;
}
}
.o_setting_container {
display: flex;
flex: 1 1 auto;
overflow: auto;
.settings_tab {
display: flex;
flex: 0 0 auto;
flex-flow: column nowrap;
background: map-get($grays, '900');
overflow: auto;
.selected {
background-color: map-get($grays, '800');
box-shadow: inset 2px 0 0 $o-brand-primary;
}
.tab {
display: flex;
padding: 0 $o-horizontal-padding*2 0 $o-horizontal-padding;
height: 40px;
color: map-get($grays, '400');
font-size: 13px;
line-height: 40px;
cursor: pointer;
white-space: nowrap;
.icon {
width: 23px;
min-width: 23px;
margin-right: 10px;
}
&:hover, &.selected {
color: white;
}
}
}
.settings {
position: relative;
flex: 1 1 100%;
background-color: $o-view-background-color;
overflow: auto;
> .app_settings_block {
h2 {
margin: 0 0 !important;
@include o-base-settings-horizontal-padding(.4rem);
background-color: map-get($grays, '200');
font-size: 15px;
&.o_invisible_modifier + .o_settings_container {
display: none;
}
}
h3 {
margin: 0 0 !important;
@include o-base-settings-horizontal-padding(.4rem);
font-weight: 400;
font-size: 13px;
}
.o_settings_container {
max-width: map-get($grid-breakpoints, lg); // Provide a maximum container size to ensure readability
@include o-base-settings-horizontal-padding(0);
margin-bottom: 24px;
}
}
.settingSearchHeader {
display: flex;
align-items: center;
margin-bottom: 10px;
@include o-base-settings-horizontal-padding(.8rem);
background-color: map-get($grays, '200');
.icon {
width: 1.4em;
height: 1.4em;
margin-right: 10px;
}
& + .app_settings_header {
margin-top: -10px;
}
}
.app_settings_header {
@include o-base-settings-horizontal-padding(0);
}
.highlighter {
background: yellow;
}
}
.d-block {
display: block!important;
}
}
}
@@ -1,4 +1,5 @@
/** @odoo-module **/
import { ActionSwiper } from "@web/core/action_swiper/action_swiper";
const { Component, useState } = owl;
@@ -14,9 +15,35 @@ export class SettingsPage extends Component {
}
}
getCurrentIndex() {
return this.props.modules.findIndex((object) => {
return object.key === this.state.selectedTab;
});
}
hasRightSwipe() {
return (
this.env.isSmall && this.state.search.value.length === 0 && this.getCurrentIndex() !== 0
);
}
hasLeftSwipe() {
return (
this.env.isSmall &&
this.state.search.value.length === 0 &&
this.getCurrentIndex() !== this.props.modules.length - 1
);
}
onRightSwipe() {
this.state.selectedTab = this.props.modules[this.getCurrentIndex() - 1].key;
}
onLeftSwipe() {
this.state.selectedTab = this.props.modules[this.getCurrentIndex() + 1].key;
}
onSettingTabClick(key) {
this.state.selectedTab = key;
this.env.searchState.value = "";
}
}
SettingsPage.template = "web.SettingsPage";
SettingsPage.components = { ActionSwiper };
@@ -1,16 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.SettingsPage" owl="1">
<div class="settings_tab">
<div class="settings_tab" t-if="!env.isSmall or state.search.value.length === 0">
<t t-foreach="props.modules" t-as="module" t-key="module.key">
<div class="tab" t-if="!module.isVisible" t-att-class="(state.selectedTab === module.key and state.search.value.length === 0) ? 'selected': ''" t-att-data-key="module.key" role="tab" t-on-click="() => this.onSettingTabClick(module.key)">
<div class="icon d-none d-md-block" t-attf-style="background : url('{{module.imgurl}}') no-repeat center;background-size:contain;"/> <span class="app_name"><t t-esc="module.string"/></span>
</div>
</t>
</div>
<div class="settings">
<t t-slot="NoContentHelper" t-if="props.slots['NoContentHelper'].isVisible"/>
<t t-slot="default" selectedTab="state.selectedTab"/>
</div>
<ActionSwiper
onRightSwipe = " hasRightSwipe() ? {
action: onRightSwipe.bind(this),
} : undefined"
onLeftSwipe = " hasLeftSwipe() ? {
action: onLeftSwipe.bind(this),
} : undefined"
animationOnMove="false"
animationType="'forwards'"
swipeDistanceRatio="6">
<div class="settings">
<t t-slot="NoContentHelper" t-if="props.slots['NoContentHelper'].isVisible"/>
<t t-slot="default" selectedTab="state.selectedTab"/>
</div>
</ActionSwiper>
</t>
</templates>
@@ -58,3 +58,171 @@
top: 0px;
right: 40px;
}
// MIXINS
@mixin o-base-settings-horizontal-padding($padding-base: $input-btn-padding-y-sm) {
padding: $padding-base $o-horizontal-padding;
@include media-breakpoint-up(xl) {
padding-left: $o-horizontal-padding*2;;
}
}
// Use a very specif selector to overwrite generic form-view rules
.o_form_view.o_form_nosheet.o_base_settings {
display: flex;
flex-flow: column nowrap;
padding: 0px;
}
// BASE SETTINGS LAYOUT
.o_base_settings {
height: 100%;
.o_control_panel {
flex: 0 0 auto;
.o_panel {
display: flex;
flex-flow: row wrap;
width: 100%;
.o_settings_title, .o_setting_search {
flex: 1 0 auto;
width: map-get($o-extra-grid-breakpoints, vsm) / 2;
}
.o_setting_search {
position: relative;
.searchInput {
border: 0px;
max-width: 400px;
border-bottom: 1px solid $border-color;
box-shadow: none;
}
.searchIcon {
@include o-position-absolute($right: 0);
color: map-get($grays, '700');
}
&:focus-within {
.searchInput {
border-bottom-color: $primary;
}
}
}
}
.o_form_statusbar {
padding: 0;
margin: 0;
border: 0;
}
}
.o_setting_container {
display: flex;
flex: 1 1 auto;
overflow: auto;
.settings_tab {
display: flex;
flex: 0 0 auto;
flex-flow: column nowrap;
background: map-get($grays, '900');
overflow: auto;
.selected {
background-color: map-get($grays, '800');
box-shadow: inset 2px 0 0 $o-brand-primary;
}
.tab {
display: flex;
padding: 0 $o-horizontal-padding*2 0 $o-horizontal-padding;
height: 40px;
color: map-get($grays, '400');
font-size: 13px;
line-height: 40px;
cursor: pointer;
white-space: nowrap;
.icon {
width: 23px;
min-width: 23px;
margin-right: 10px;
}
&:hover, &.selected {
color: white;
}
}
}
.settings {
position: relative;
flex: 1 1 100%;
background-color: $o-view-background-color;
overflow: auto;
> .app_settings_block {
h2 {
margin: 0 0 !important;
@include o-base-settings-horizontal-padding(.4rem);
background-color: map-get($grays, '200');
font-size: 15px;
&.o_invisible_modifier + .o_settings_container {
display: none;
}
}
h3 {
margin: 0 0 !important;
@include o-base-settings-horizontal-padding(.4rem);
font-weight: 400;
font-size: 13px;
}
.o_settings_container {
max-width: map-get($grid-breakpoints, lg); // Provide a maximum container size to ensure readability
@include o-base-settings-horizontal-padding(0);
margin-bottom: 24px;
}
}
.settingSearchHeader {
display: flex;
align-items: center;
margin-bottom: 10px;
@include o-base-settings-horizontal-padding(.8rem);
background-color: map-get($grays, '200');
.icon {
width: 1.4em;
height: 1.4em;
margin-right: 10px;
}
& + .app_settings_header {
margin-top: -10px;
}
}
.app_settings_header {
@include o-base-settings-horizontal-padding(0);
}
.highlighter {
background: yellow;
}
}
.d-block {
display: block!important;
}
}
}
@@ -0,0 +1,68 @@
@include media-breakpoint-down(md) {
.o_base_settings {
flex-flow: column nowrap;
> .o_control_panel {
@include o-webclient-padding($top: 10px, $bottom: 10px);
}
.o_setting_container {
flex-flow: column nowrap;
.settings_tab {
flex: 0 0 $o-base-settings-mobile-tabs-height;
flex-direction: row;
justify-content: space-between;
position: relative;
overflow: hidden;
overflow-x: auto;
padding: 0;
background: #5E5E5E;
.tab {
display: block;
width: auto;
height: $o-base-settings-mobile-tabs-height;
padding: $o-base-settings-mobile-tabs-height*0.25 $o-base-settings-mobile-tabs-height*0.4;
text-align: center;
font-size: 14px;
font-weight: 500;
line-height: inherit;
transition: 0.2s all ease 0s;
transform: translate3d(0, 0, 0);
.app_name {
display: block;
white-space: nowrap;
text-transform: uppercase;
}
&:after {
content: '';
background: $o-brand-primary;
opacity: 0;
@include o-position-absolute(auto, 0, 0, 0);
width: 100%;
height: 3px;
transition: 0.2s all ease 0s;
}
&.current {
color: white;
font-weight: bold;
// Reset default style for 'selected' tabs
box-shadow: none;
background: none;
&:after {
opacity: 1;
}
}
}
}
}
}
}
@@ -0,0 +1,172 @@
/** @odoo-module **/
import { getFixture, mockTimeout, nextTick } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { swipeLeft, swipeRight } from "@web/../tests/mobile/helpers";
import { registry } from "@web/core/registry";
const { EventBus } = owl;
let serverData, target;
const serviceRegistry = registry.category("services");
QUnit.module("Mobile SettingsFormView", (hooks) => {
hooks.beforeEach(() => {
serverData = {
models: {
project: {
fields: {
foo: { string: "Foo", type: "boolean" },
bar: { string: "Bar", type: "boolean" },
},
},
},
};
target = getFixture();
setupViewRegistries();
});
QUnit.module("BaseSettings Mobile");
QUnit.test("swipe settings in mobile [REQUIRE TOUCHEVENT]", async function (assert) {
const { execRegisteredTimeouts } = mockTimeout();
serviceRegistry.add("ui", {
start(env) {
Object.defineProperty(env, "isSmall", {
value: true,
});
return {
bus: new EventBus(),
size: 0,
isSmall: true,
};
},
});
await makeView({
type: "form",
resModel: "project",
serverData,
arch: `
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
<div class="o_setting_container">
<div class="settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="bar"/>
</div>
<div class="o_setting_right_pane">
<label for="bar"/>
<div class="text-muted">this is bar</div>
</div>
</div>
</div>
</div>
<div class="app_settings_block" string="Project" data-key="project">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<label for="foo"/>
<div class="text-muted">this is foo</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>`,
});
await swipeLeft(target, ".settings");
execRegisteredTimeouts();
await nextTick();
assert.hasAttrValue(
target.querySelector(".selected"),
"data-key",
"project",
"current setting should be project"
);
await swipeRight(target, ".settings");
execRegisteredTimeouts();
await nextTick();
assert.hasAttrValue(
target.querySelector(".selected"),
"data-key",
"crm",
"current setting should be crm"
);
});
QUnit.test(
"swipe settings on larger screen sizes has no effect [REQUIRE TOUCHEVENT]",
async function (assert) {
const { execRegisteredTimeouts } = mockTimeout();
serviceRegistry.add("ui", {
start(env) {
Object.defineProperty(env, "isSmall", {
value: false,
});
return {
bus: new EventBus(),
size: 9,
isSmall: false,
};
},
});
await makeView({
type: "form",
resModel: "project",
serverData,
arch: `
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
<div class="o_setting_container">
<div class="settings">
<div class="app_settings_block" string="CRM" data-key="crm">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="bar"/>
</div>
<div class="o_setting_right_pane">
<label for="bar"/>
<div class="text-muted">this is bar</div>
</div>
</div>
</div>
</div>
<div class="app_settings_block" string="Project" data-key="project">
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="foo"/>
</div>
<div class="o_setting_right_pane">
<label for="foo"/>
<div class="text-muted">this is foo</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>`,
});
await swipeLeft(target, ".settings");
execRegisteredTimeouts();
await nextTick();
assert.hasAttrValue(
target.querySelector(".selected"),
"data-key",
"crm",
"current setting should still be crm"
);
}
);
});