[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:
committed by
Bruno Boi
co-authored by
Bruno Boi
parent
156d41dbda
commit
3f15989e15
@@ -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"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user