[REF] web,mail: formView design

- ButtonBox is moved to the ControlPanel's actions section (center).
- StatusBarField, ButtonBox and PercentPieField design.
- Chatter is moved below the ControlPanel (aka "in the view")

task-2818586

Part-of: odoo/odoo#116641
Co-authored-by: Pierre Paridans <app@odoo.com>
Co-authored-by: Pierre Rousseau <pro@odoo.com>
Co-authored-by: luvi <luvi@odoo.com>
This commit is contained in:
stefanorigano (SRI)
2023-05-12 22:59:14 +02:00
committed by Pierre Paridans
co-authored by Pierre Paridans Pierre Rousseau luvi
parent c964329add
commit 824024f8aa
33 changed files with 401 additions and 558 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<t t-name="mail.Thread" owl="1">
<t t-if="env.inChatter" t-call="mail.Thread.jumpPresent"/> <!-- chatter has its own scrollable, this ensures proper sticky showing -->
<div class="o-mail-Thread flex-grow-1 bg-view" t-att-class="{ 'overflow-auto': props.hasScrollAdjust, 'pb-4': !state.showJumpPresent }" t-ref="messages">
<div class="o-mail-Thread flex-grow-1" t-att-class="{ 'overflow-auto': props.hasScrollAdjust, 'pb-4': !state.showJumpPresent }" t-ref="messages">
<t t-if="!props.thread.isEmpty or props.thread.loadOlder or props.thread.hasLoadingFailed" name="content">
<div class="d-flex flex-column position-relative" t-att-class="{'justify-content-end': !env.inChatter and props.thread.type !== 'mailbox'}" style="min-height:100%">
<span class="position-absolute w-100 invisible" t-att-class="props.order === 'asc' ? 'bottom-0' : 'top-0'" t-ref="present-treshold" t-att-style="`height: Min(${PRESENT_THRESHOLD}px, 100%)`"/>
@@ -5,7 +5,6 @@ import { registry } from "@web/core/registry";
import { SIZES } from "@web/core/ui/ui_service";
import { patch } from "@web/core/utils/patch";
import { append, createElement, setAttributes } from "@web/core/utils/xml";
import { ViewCompiler, getModifier } from "@web/views/view_compiler";
import { FormCompiler } from "@web/views/form/form_compiler";
function compileChatter(node, params) {
@@ -37,8 +36,9 @@ function compileChatter(node, params) {
break;
}
}
const chatterContainerXml = createElement("Chatter");
const chatterContainerXml = createElement("t");
setAttributes(chatterContainerXml, {
"t-component": "__comp__.mailComponents.Chatter",
hasActivities,
hasFollowers,
hasMessageList,
@@ -46,9 +46,9 @@ function compileChatter(node, params) {
hasParentReloadOnFollowersUpdate,
hasParentReloadOnMessagePosted,
isAttachmentBoxVisibleInitially,
threadId: params.threadId,
threadModel: params.threadModel,
webRecord: params.webRecord,
threadId: "__comp__.props.record.model.root.resId or undefined",
threadModel: "__comp__.props.record.model.root.resModel",
webRecord: "__comp__.props.record.model.root",
saveRecord: "() => __comp__.saveButtonClicked and __comp__.saveButtonClicked()",
});
const chatterContainerHookXml = createElement("div");
@@ -60,92 +60,24 @@ function compileChatter(node, params) {
function compileAttachmentPreview(node, params) {
const webClientViewAttachmentViewContainerHookXml = createElement("div");
webClientViewAttachmentViewContainerHookXml.classList.add("o_attachment_preview");
const webClientViewAttachmentViewContainerXml = createElement("AttachmentView");
const webClientViewAttachmentViewContainerXml = createElement("t");
setAttributes(webClientViewAttachmentViewContainerXml, {
threadId: params.threadId,
threadModel: params.threadModel,
"t-component": "__comp__.mailComponents.AttachmentView",
threadId: "__comp__.props.record.model.root.resId or undefined",
threadModel: "__comp__.props.record.model.root.resModel",
});
append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml);
return webClientViewAttachmentViewContainerHookXml;
}
export class MailFormCompiler extends ViewCompiler {
setup() {
this.compilers.push({ selector: "t", fn: this.compileT });
this.compilers.push({ selector: "div.oe_chatter", fn: this.compileChatter });
this.compilers.push({
selector: "div.o_attachment_preview",
fn: this.compileAttachmentPreview,
});
}
compile(node, params) {
const res = super.compile(node, params).children[0];
const chatterContainerHookXml = res.querySelector(".o-mail-Form-chatter");
if (chatterContainerHookXml) {
setAttributes(chatterContainerHookXml, {
"t-if": `!__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
"t-attf-class": "o-aside",
});
const chatterContainerXml = chatterContainerHookXml.querySelector("Chatter");
setAttributes(chatterContainerXml, {
hasMessageListScrollAdjust: "true",
isInFormSheetBg: "false",
});
}
const attachmentViewHookXml = res.querySelector(".o_attachment_preview");
if (attachmentViewHookXml) {
setAttributes(attachmentViewHookXml, {
"t-if": `__comp__.hasAttachmentViewer()`,
});
}
return res;
}
compileT(node, params) {
const compiledRoot = createElement("t");
for (const child of node.childNodes) {
const invisible = getModifier(child, "invisible");
let compiledChild = this.compileNode(child, params, false);
compiledChild = this.applyInvisible(invisible, compiledChild, {
...params,
recordExpr: "__comp__.model.root",
});
append(compiledRoot, compiledChild);
}
return compiledRoot;
}
compileChatter(node) {
return compileChatter(node, {
chatter: "__comp__.chatter",
threadId: "__comp__.model.root.resId or undefined",
threadModel: "__comp__.model.root.resModel",
webRecord: "__comp__.model.root",
});
}
compileAttachmentPreview(node) {
return compileAttachmentPreview(node, {
threadId: "__comp__.model.root.resId or undefined",
threadModel: "__comp__.model.root.resModel",
});
}
}
registry.category("form_compilers").add("chatter_compiler", {
selector: "div.oe_chatter",
fn: (node) =>
compileChatter(node, {
threadId: "__comp__.props.record.resId or undefined",
threadModel: "__comp__.props.record.resModel",
webRecord: "__comp__.props.record",
}),
fn: compileChatter,
});
registry.category("form_compilers").add("attachment_preview_compiler", {
selector: "div.o_attachment_preview",
fn: () => createElement("t"),
fn: compileAttachmentPreview,
});
patch(FormCompiler.prototype, "mail", {
@@ -156,7 +88,9 @@ patch(FormCompiler.prototype, "mail", {
if (!chatterContainerHookXml) {
return res; // no chatter, keep the result as it is
}
const chatterContainerXml = chatterContainerHookXml.querySelector("Chatter");
const chatterContainerXml = chatterContainerHookXml.querySelector(
"t[t-component='__comp__.mailComponents.Chatter']"
);
setAttributes(chatterContainerXml, {
hasMessageListScrollAdjust: "false",
isInFormSheetBg: "false",
@@ -170,24 +104,46 @@ patch(FormCompiler.prototype, "mail", {
if (!parentXml) {
return res; // miss-config: a sheet-bg is required for the rest
}
if (params.hasAttachmentViewerInArch) {
const webClientViewAttachmentViewHookXml = res.querySelector(".o_attachment_preview");
if (webClientViewAttachmentViewHookXml) {
// in sheet bg (attachment viewer present)
setAttributes(webClientViewAttachmentViewHookXml, {
"t-if": `__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
});
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
sheetBgChatterContainerHookXml.classList.add("o-isInFormSheetBg");
setAttributes(sheetBgChatterContainerHookXml, {
"t-if": `__comp__.props.hasAttachmentViewer`,
"t-if": `__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}`,
});
append(formSheetBgXml, sheetBgChatterContainerHookXml);
const sheetBgChatterContainerXml =
sheetBgChatterContainerHookXml.querySelector("Chatter");
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(
"t[t-component='__comp__.mailComponents.Chatter']"
);
setAttributes(sheetBgChatterContainerXml, {
isInFormSheetBg: "true",
hasMessageListScrollAdjust: "false",
});
}
// after sheet bg (standard position, either aside or below)
if (webClientViewAttachmentViewHookXml) {
setAttributes(chatterContainerHookXml, {
"t-if": `!(__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL})`,
"t-attf-class": `{{ __comp__.uiService.size >= ${SIZES.XXL} and !(__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL}) ? "o-aside" : "" }}`,
});
setAttributes(chatterContainerXml, {
isInFormSheetBg: "__comp__.hasAttachmentViewer()",
hasMessageListScrollAdjust: `__comp__.uiService.size >= ${SIZES.XXL} and !(__comp__.hasAttachmentViewer() and __comp__.uiService.size >= ${SIZES.XXL})`,
});
} else {
setAttributes(chatterContainerXml, {
isInFormSheetBg: "false",
hasMessageListScrollAdjust: `__comp__.uiService.size >= ${SIZES.XXL}`,
});
setAttributes(chatterContainerHookXml, {
"t-attf-class": `{{ __comp__.uiService.size >= ${SIZES.XXL} ? "o-aside" : "mt-4 mt-md-0" }}`,
});
}
// after sheet bg (standard position, below form)
setAttributes(chatterContainerHookXml, {
"t-if": `!__comp__.props.hasAttachmentViewer and __comp__.uiService.size < ${SIZES.XXL}`,
});
append(parentXml, chatterContainerHookXml);
return res;
},
@@ -1,88 +0,0 @@
/** @odoo-module */
import { AttachmentView } from "@mail/attachments/attachment_view";
import { Chatter } from "@mail/web/chatter";
import { browser } from "@web/core/browser/browser";
import { useService } from "@web/core/utils/hooks";
import { createElement } from "@web/core/utils/xml";
import { SIZES } from "@web/core/ui/ui_service";
import { patch } from "@web/core/utils/patch";
import { useDebounced } from "@web/core/utils/timing";
import { FormController } from "@web/views/form/form_controller";
import { useViewCompiler } from "@web/views/view_compiler";
import { evalDomain } from "@web/views/utils";
import { MailFormCompiler } from "./form_compiler";
import { onMounted, onWillUnmount, useState } from "@odoo/owl";
patch(FormController.prototype, "mail", {
setup() {
this._super();
this.messagingState = useState({
/** @type {import("@mail/core/thread_model").Thread} */
thread: undefined,
});
if (this.env.services["mail.thread"]) {
this.threadService = useService("mail.thread");
}
this.uiService = useService("ui");
this.hasAttachmentViewerInArch = false;
const { archInfo } = this.props;
const template = createElement("t");
const xmlDocAttachmentPreview = archInfo.xmlDoc.querySelector("div.o_attachment_preview");
if (xmlDocAttachmentPreview && xmlDocAttachmentPreview.parentNode.nodeName === "form") {
// TODO hasAttachmentViewer should also depend on the groups= and/or invisible modifier on o_attachment_preview (see invoice form)
template.appendChild(xmlDocAttachmentPreview);
this.hasAttachmentViewerInArch = true;
archInfo.arch = archInfo.xmlDoc.outerHTML;
}
const xmlDocChatter = archInfo.xmlDoc.querySelector("div.oe_chatter");
if (xmlDocChatter && xmlDocChatter.parentNode.nodeName === "form") {
template.appendChild(xmlDocChatter.cloneNode(true));
}
const mailTemplates = useViewCompiler(
MailFormCompiler,
archInfo.arch,
{ Mail: template },
{}
);
this.mailTemplate = mailTemplates.Mail;
this.onResize = useDebounced(this.render, 200);
onMounted(() => browser.addEventListener("resize", this.onResize));
onWillUnmount(() => browser.removeEventListener("resize", this.onResize));
},
/**
* @returns {boolean}
*/
hasAttachmentViewer() {
if (
!this.threadService ||
this.uiService.size < SIZES.XXL ||
!this.hasAttachmentViewerInArch ||
!this.model.root.resId
) {
return false;
}
this.messagingState.thread = this.threadService.insert({
id: this.model.root.resId,
model: this.model.root.resModel,
type: "chatter",
});
return this.messagingState.thread.attachmentsInWebClientView.length > 0;
},
evalDomainFromRecord(record, expr) {
return evalDomain(expr, record.evalContext);
},
});
Object.assign(FormController.components, {
AttachmentView,
Chatter,
});
@@ -2,14 +2,7 @@
<templates xml:space="preserve">
<t t-inherit="web.FormView" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
<t t-if="mailTemplate">
<t t-call="{{ mailTemplate }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }"/>
</t>
</xpath>
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
<attribute name="hasAttachmentViewerInArch">hasAttachmentViewerInArch</attribute>
<attribute name="hasAttachmentViewer">hasAttachmentViewer()</attribute>
<attribute name="saveButtonClicked">() => this.saveButtonClicked()</attribute>
</xpath>
</t>
@@ -2,29 +2,51 @@
import { Chatter } from "@mail/web/chatter";
import { AttachmentView } from "@mail/attachments/attachment_view";
import { patch } from "@web/core/utils/patch";
import { FormRenderer } from "@web/views/form/form_renderer";
import { browser } from "@web/core/browser/browser";
import { useService } from "@web/core/utils/hooks";
import { SIZES } from "@web/core/ui/ui_service";
import { useDebounced } from "@web/core/utils/timing";
import { onMounted, onWillUnmount, useState } from "@odoo/owl";
patch(FormRenderer.prototype, "mail", {
get compileParams() {
return {
...this._super(),
hasAttachmentViewerInArch: this.props.hasAttachmentViewerInArch,
saveButtonClicked: this.props.saveButtonClicked,
setup() {
this._super();
this.mailComponents = {
AttachmentView,
Chatter,
};
this.messagingState = useState({
/** @type {import("@mail/core/thread_model").Thread} */
thread: undefined,
});
if (this.env.services["mail.thread"]) {
this.threadService = useService("mail.thread");
}
this.uiService = useService("ui");
this.onResize = useDebounced(this.render, 200);
onMounted(() => browser.addEventListener("resize", this.onResize));
onWillUnmount(() => browser.removeEventListener("resize", this.onResize));
},
/**
* @returns {boolean}
*/
hasAttachmentViewer() {
if (!this.threadService || this.uiService.size < SIZES.XXL || !this.props.record.resId) {
return false;
}
this.messagingState.thread = this.threadService.insert({
id: this.props.record.resId,
model: this.props.record.resModel,
type: "chatter",
});
return this.messagingState.thread.attachmentsInWebClientView.length > 0;
},
});
patch(FormRenderer.props, "mail", {
hasAttachmentViewerInArch: { type: Boolean, optional: true },
// Template props : added by the FormCompiler
hasAttachmentViewer: { type: Boolean, optional: true },
chatter: { type: Object, optional: true },
saveButtonClicked: { type: Function, optional: true },
});
Object.assign(FormRenderer.components, {
AttachmentView,
Chatter,
});
@@ -4,20 +4,14 @@
.o-mail-Form-chatter {
--ChatterTopbar-padding-v: #{map-get($spacers, 2)};
@include media-breakpoint-down(md) {
--ChatterTopbar-padding-left: #{$o-horizontal-padding};
--ChatterTopbar-padding-right: #{$o-horizontal-padding};
}
--ChatterTopbar-padding-left: #{$o-horizontal-padding};
--ChatterTopbar-padding-right: #{$o-horizontal-padding};
&.o-aside {
--Chatter-max-width: #{$o-form-view-sheet-max-width};
--ChatterTopbar-padding-left: #{$o-horizontal-padding};
--ChatterTopbar-padding-right: #{$o-horizontal-padding};
flex-shrink: 0;
flex-grow: 1;
border-left: $border-width solid;
padding: map-get($spacers, 0);
max-width: var(--Chatter-max-width);
width: $o-mail-Chatter-minWidth;
@@ -33,13 +27,10 @@
&.o-isInFormSheetBg {
background-color: $white;
width: auto;
border-top: 1px solid $border-color;
}
&:not(.o-aside):not(.o-full-width) .o-mail-Chatter {
@include make-container-max-widths();
margin-left: auto;
margin-right: auto;
}
@@ -49,11 +40,6 @@
// Style
// ------------------------------------------------------------------
.o-mail-Form-chatter.o-aside {
border-left-color: $border-color;
}
.o-mail-Form-chatter {
display: flex;
margin: 0 auto;
-8
View File
@@ -2,14 +2,6 @@
display: none;
}
.o-mail-Chatter-topbar {
padding-inline-start: 0;
}
.o-mail-Form-chatter.o-aside .o-mail-Chatter-topbar {
padding-inline-start: $o-horizontal-padding;
}
.o-chatter-disabled .o-mail-Message-actions {
display: none;
}
+20 -11
View File
@@ -2,27 +2,27 @@
<templates xml:space="preserve">
<t t-name="mail.Chatter" owl="1">
<div class="o-mail-Chatter bg-view h-100 flex-grow-1 d-flex flex-column" t-att-class="{ 'overflow-auto': props.hasMessageListScrollAdjust, 'o-chatter-disabled': props.threadId === false }" t-ref="root">
<div class="o-mail-Chatter-topbar d-flex flex-shrink-0 flex-grow-0">
<button t-if="props.hasMessageList" class="o-mail-Chatter-sendMessage btn text-nowrap me-2" t-att-class="{ 'btn-odoo': state.thread.composer.type !== 'note', 'btn-light': state.thread.composer.type === 'note', 'o-active': state.thread.composer.type === 'message', 'my-2': !props.compactHeight }" t-att-disabled="!state.thread.hasWriteAccess and !(state.thread.hasReadAccess and state.thread.canPostOnReadonly) and props.threadId" data-hotkey="m" t-on-click="() => this.toggleComposer('message')">
<div class="o-mail-Chatter w-100 h-100 flex-grow-1 d-flex flex-column" t-att-class="{ 'overflow-auto': props.hasMessageListScrollAdjust, 'o-chatter-disabled': props.threadId === false }" t-ref="root">
<div class="o-mail-Chatter-topbar d-flex flex-shrink-0 flex-grow-0 px-3 overflow-x-auto">
<button t-if="props.hasMessageList" class="o-mail-Chatter-sendMessage btn text-nowrap me-1" t-att-class="{ 'btn-primary': state.thread.composer.type !== 'note', 'btn-secondary': state.thread.composer.type === 'note', 'o-active': state.thread.composer.type === 'message', 'my-2': !props.compactHeight }" t-att-disabled="!state.thread.hasWriteAccess and !(state.thread.hasReadAccess and state.thread.canPostOnReadonly) and props.threadId" data-hotkey="m" t-on-click="() => this.toggleComposer('message')">
Send message
</button>
<button t-if="props.hasMessageList" class="o-mail-Chatter-logNote btn text-nowrap me-2" t-att-class="{ 'btn-odoo o-active': state.thread.composer.type === 'note', 'btn-light': state.thread.composer.type !== 'note', 'my-2': !props.compactHeight }" data-hotkey="shift+m" t-on-click="() => this.toggleComposer('note')">
<button t-if="props.hasMessageList" class="o-mail-Chatter-logNote btn text-nowrap me-2" t-att-class="{ 'btn-primary o-active': state.thread.composer.type === 'note', 'btn-secondary': state.thread.composer.type !== 'note', 'my-2': !props.compactHeight }" data-hotkey="shift+m" t-on-click="() => this.toggleComposer('note')">
Log note
</button>
<div class="border-bottom flex-grow-1 d-flex" t-att-class="{ 'border-start ps-2 pe-4': props.hasActivities }">
<button t-if="props.hasActivities" class="btn btn-light text-nowrap" t-att-class="{ 'my-2': !props.compactHeight }" data-hotkey="shift+a" t-on-click="scheduleActivity">
<div class="flex-grow-1 d-flex">
<button t-if="props.hasActivities" class="btn btn-secondary text-nowrap" t-att-class="{ 'my-2': !props.compactHeight }" data-hotkey="shift+a" t-on-click="scheduleActivity">
<i class="fa fa-clock-o me-1"/>
<span>Activities</span>
</button>
<span class="o-mail-Chatter-topbarGrow flex-grow-1 border-start pe-2" t-att-class="{ 'ms-2': props.hasActivities }"/>
<span class="o-mail-Chatter-topbarGrow flex-grow-1 pe-2"/>
<FileUploader t-if="attachments.length === 0" fileUploadClass="'o-mail-Chatter-fileUploader'" multiUpload="true" onUploaded.bind="onUploaded" onClick="(ev) => this.onClickAttachFile(ev)">
<t t-set-slot="toggler">
<t t-call="mail.Chatter.attachFiles"/>
</t>
</FileUploader>
<t t-else="" t-call="mail.Chatter.attachFiles"/>
<Dropdown t-if="props.hasFollowers" position="'bottom-end'" disabled="isDisabled" class="'o-mail-Followers d-flex'" menuClass="'o-mail-Followers-dropdown flex-column p-0'" menuDisplay="'d-flex'" title="followerButtonLabel" togglerClass="'o-mail-Followers-button btn btn-light px-2 ' + (props.compactHeight ? '' : 'my-2')">
<Dropdown t-if="props.hasFollowers" position="'bottom-end'" disabled="isDisabled" class="'o-mail-Followers d-flex me-1'" menuClass="'o-mail-Followers-dropdown flex-column p-0'" menuDisplay="'d-flex'" title="followerButtonLabel" togglerClass="'o-mail-Followers-button btn btn-link text-action px-1 ' + (props.compactHeight ? '' : 'my-2')">
<t t-set-slot="toggler">
<i class="fa fa-user-o" role="img"/>
<span class="o-mail-Followers-counter ps-1" t-esc="state.thread.followers.length"/>
@@ -58,7 +58,7 @@
<button t-if="props.hasFollowers and state.thread.followerOfSelf" class="btn px-0" t-att-class="{ 'text-success': !unfollowHover.isHover, 'text-warning': unfollowHover.isHover, 'my-2': !props.compactHeight }" t-att-disabled="!props.threadId" t-on-click="onClickUnfollow" t-ref="unfollow">
<div class="position-relative">
<!-- Hidden element used to set the button maximum size -->
<span class="invisible">
<span class="d-flex invisible text-nowrap">
<i class="me-1 fa fa-fw fa-eye-slash"/><t t-out="followingText.length > unfollowText.length ? followingText : unfollowText"/>
</span>
<span class="position-absolute end-0 top-0" t-att-class="{ 'text-warning': unfollowHover.isHover }">
@@ -66,7 +66,16 @@
</span>
</div>
</button>
<button t-if="!state.thread.followerOfSelf" class="o-mail-Chatter-follow btn btn-link text-600" t-on-click="onClickFollow">Follow</button>
<button t-if="!state.thread.followerOfSelf" class="o-mail-Chatter-follow btn btn-link px-0 text-600" t-on-click="onClickFollow">
<div class="position-relative">
<span class="d-flex invisible text-nowrap">
<i class="me-1 fa fa-fw fa-eye-slash"/><t t-out="followingText.length > unfollowText.length ? followingText : unfollowText"/>
</span>
<span class="position-absolute end-0 top-0">
Follow
</span>
</div>
</button>
</div>
<button t-if="props.close" class="o-mail-Chatter-close btn btn-dark flex-shrink-0 rounded-bottom-3" aria-label="Close" t-on-click="props.close">
<i class="oi oi-large oi-close"/>
@@ -136,7 +145,7 @@
</t>
<t t-name="mail.Chatter.attachFiles" owl="1">
<button class="btn btn-light" aria-label="Attach files" t-att-class="{ 'my-2': !props.compactHeight }" t-on-click="onClickAddAttachments">
<button class="btn btn-link text-action px-1" aria-label="Attach files" t-att-class="{ 'my-2': !props.compactHeight }" t-on-click="onClickAddAttachments">
<i class="fa fa-paperclip fa-lg me-1"/>
<span t-if="attachments.length > 0" t-esc="attachments.length"/>
<i t-if="!state.thread.areAttachmentsLoaded and state.thread.isLoadingAttachments" class="fa fa-circle-o-notch fa-spin" aria-label="Attachment counter loading..."/>
-4
View File
@@ -12,11 +12,7 @@
margin: 0;
}
.o_form_sheet {
margin: 0;
border: none;
flex: 1;
box-shadow: none;
padding: 0;
max-width: 100%;
.oe_pad {
flex: 1;
@@ -402,46 +402,6 @@
// Percent pie
&.o_field_percent_pie {
align-items: center;
> .o_pie {
position: relative;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
border-radius: 100%;
overflow: hidden;
background-color: $o-brand-primary;
&:after { // Outside pie border to go over border-radius irregularities
content: " ";
@include o-position-absolute(0, 0);
width: 100%;
height: 100%;
border-radius: 100%;
}
.o_mask {
@include o-position-absolute(0, 0);
transform-origin: 0;
width: 50%;
height: 100%;
background-color: $o-view-background-color;
&.o_full {
background-color: inherit;
}
}
.o_pie_value {
display: flex;
justify-content: center;
align-items: center;
border-radius: 100%;
font-weight: 700;
color: $o-brand-odoo;
}
}
}
// Ace editor
@@ -26,7 +26,6 @@ $o-colors-complete: join(
$o-sheet-cancel-hpadding: $o-horizontal-padding !default;
$o-sheet-cancel-bpadding: $o-horizontal-padding !default;
$o-statusbar-arrow-width: $o-statusbar-height / 3 !default;
$o-statusbar-disabled-bg: lighten($o-brand-lightsecondary, 7%) !default;
$o-datepicker-week-bg-color: lighten($o-datepicker-week-color, 30%) !default;
@@ -217,7 +217,6 @@
.o_cp_buttons {
display: flex;
margin-right: auto;
margin-left: $o-kanban-record-margin;
}
.o_cp_pager {
display: flex;
@@ -12,15 +12,6 @@ export class PercentPieField extends Component {
...standardFieldProps,
string: { type: String, optional: true },
};
get transform() {
const rotateDeg = (360 * this.props.record.data[this.props.name]) / 100;
return {
left: rotateDeg < 180 ? 180 : rotateDeg,
right: rotateDeg < 180 ? rotateDeg : 0,
value: rotateDeg,
};
}
}
export const percentPieField = {
@@ -1,19 +1,10 @@
.o_field_percent_pie .o_pie {
$pie-dimension: $o-statbutton-height - 2 * $o-statbutton-vpadding;
$pie-ring-width: 4px;
.o_field_percent_pie {
.o_pie {
--PercentPieField-color-active: #{$o-action};
--PercentPieField-color-static: #{rgba(#000, .15)};
width: $pie-dimension;
height: $pie-dimension;
&:after { // Outside pie border to go over border-radius irregularities
border: 1px solid $o-brand-secondary;
}
.o_pie_value {
@include o-position-absolute($pie-ring-width, $pie-ring-width);
width: $pie-dimension - 2 * $pie-ring-width;
height: $pie-dimension - 2 * $pie-ring-width;
border: 1px solid $o-brand-secondary;
background-color: $o-brand-lightsecondary;
height: var(--PercentPieField-size, #{$font-size-lg});
aspect-ratio: 1;
margin-right: var(--PercentPieField-gap, #{map-get($spacers, 2) });
}
}
@@ -2,14 +2,17 @@
<templates xml:space="preserve">
<t t-name="web.PercentPieField" owl="1">
<div class="o_pie">
<div class="o_mask" t-attf-class="{{transform.value >= 360 ? 'o_full' : ''}}" t-attf-style="transform: rotate({{transform.left}}deg)"/>
<div class="o_mask" t-attf-class="{{transform.value >= 180 ? 'o_full' : ''}}" t-attf-style="transform: rotate({{transform.right}}deg)"/>
<div class="o_pie_value">
<span t-esc="props.record.data[props.name] + '%'"/>
</div>
<figure class="o_pie mb-0 rounded-circle"
t-attf-style="background: conic-gradient(
var(--PercentPieField-color-active) 0% {{props.record.data[props.name]}}%,
var(--PercentPieField-color-static) 0% 100%
)"
/>
<div class="o_pie_info">
<span class="o_pie_value" t-esc="props.record.data[props.name] + '%'"/>
<span class="o_pie_text" t-esc="props.string"/>
</div>
<span t-esc="props.string"/>
</t>
</templates>
@@ -130,11 +130,10 @@ export class StatusBarField extends Component {
}
getDropdownItemClassNames(item) {
const classNames = [
"btn",
item.isSelected ? "btn-primary" : "btn-secondary",
"o_arrow_button",
];
const classNames = [];
if (item.isSelected) {
classNames.push("active");
}
if (item.isSelected || this.props.isDisabled) {
classNames.push("disabled");
}
@@ -10,37 +10,42 @@
align-items: stretch;
> .o_arrow_button {
min-height: $o-statusbar-height;
position: relative;
padding-left: $o-statusbar-arrow-width*2;
border-width: 0;
padding-left: $o-statusbar-arrow-width * 1.5;
border-radius: 0;
font-size: $font-size-sm;
&:first-child {
@include border-end-radius($border-radius);
padding-right: $o-horizontal-padding; // Compensate container padding
overflow-x: hidden; // to prevent horizontal scroll due to last arrow
}
&:last-child {
padding-left: $o-horizontal-padding - 1;
border-left: $border-width solid $border-color;
@include border-start-radius($border-radius);
padding-left: $o-horizontal-padding;
}
&:not(:first-child):before, &:not(:first-child):after {
content: " ";
display: block;
@include o-position-absolute(0, -$o-statusbar-arrow-width + 1);
&:not(:first-child) {
&:before, &:after {
@include o-position-absolute(-$border-width, -$o-statusbar-arrow-width);
display: block;
border-top: floor($o-statusbar-height/2) solid transparent;
border-bottom: ceil($o-statusbar-height/2) solid transparent;
border-right: none;
border-left: $o-statusbar-arrow-width solid $o-view-background-color;
-moz-transform: scale(0.9999); // Smooth the triangle on firefox
}
border-top: floor($o-statusbar-height/2) solid transparent;
border-bottom: ceil($o-statusbar-height/2) solid transparent;
border-right: none;
border-left: $o-statusbar-arrow-width solid map-get($-btn-secondary-design, background);
-moz-transform: scale(0.9999); // Smooth the triangle on firefox
content: " ";
}
&:not(:first-child):before {
right: -$o-statusbar-arrow-width;
border-left-color: $border-color!important;
&:before {
right: calc(-#{$o-statusbar-arrow-width} - #{$border-width});
border-left-color: $o-view-background-color;
}
&:hover:after {
border-left-color: map-get($-btn-secondary-design, hover-background);
}
}
&.disabled {
@@ -54,12 +59,19 @@
}
}
&.o_arrow_button_current.disabled {
background-color: $o-brand-odoo;
color: color-contrast($o-brand-odoo);
&.o_arrow_button_current.disabled, &:active:not(:first-child) {
background-color: map-get($-btn-secondary-design, active-background);
border-color: map-get($-btn-secondary-design, active-border);
color: map-get($-btn-secondary-design, active-color);
&:after, &:before {
border-left-color: $o-brand-odoo;
&:after {
border-left-color: map-get($-btn-secondary-design, active-background);
}
&, & + .btn {
&:before {
border-left-color: map-get($-btn-secondary-design, active-border);
}
}
}
}
@@ -0,0 +1 @@
$o-statusbar-arrow-width: 1em;
@@ -6,7 +6,7 @@
<t t-set="items" t-value="computeItems()" />
<t t-if="items.folded.length">
<button type="button" class="btn dropdown-toggle text-uppercase" t-att-class="{ 'o_arrow_button': !env.isSmall, 'btn-primary': env.isSmall }">
<button type="button" class="btn btn-secondary dropdown-toggle" t-att-class="{ 'o_arrow_button': !env.isSmall, 'btn-primary': env.isSmall }">
<Dropdown toggler="'parent'">
<t t-foreach="items.folded" t-as="item" t-key="item.id">
<DropdownItem class="getDropdownItemClassNames(item)" onSelected="() => this.selectItem(item)">
@@ -25,7 +25,7 @@
<t t-if="item.isSelected">
<button
type="button"
class="btn o_arrow_button_current o_arrow_button disabled text-uppercase"
class="btn btn-secondary o_arrow_button_current o_arrow_button disabled"
disabled="disabled"
role="radio"
aria-label="Current state"
@@ -39,7 +39,7 @@
<t t-elif="props.isDisabled">
<button
type="button"
class="btn o_arrow_button disabled text-uppercase"
class="btn btn-secondary o_arrow_button disabled"
disabled="disabled"
role="radio"
aria-label="Not active state"
@@ -52,7 +52,7 @@
<t t-else="">
<button
type="button"
class="btn o_arrow_button text-uppercase"
class="btn btn-secondary o_arrow_button"
role="radio"
aria-label="Not active state, click to change it"
aria-checked="false"
@@ -3,7 +3,7 @@
<t t-name="web.X2ManyField" owl="1">
<div t-att-class="className">
<div class="o_x2m_control_panel">
<div class="o_x2m_control_panel d-empty-none mb-2">
<t t-if="displayControlPanelButtons">
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
<t t-foreach="creates" t-as="create" t-key="create_index">
@@ -28,8 +28,8 @@
</t>
</div>
</t>
<div class="o_cp_pager" role="search">
<Pager t-if="props.record.data[props.name].count > props.record.data[props.name].limit" t-props="pagerProps"/>
<div t-if="props.record.data[props.name].count > props.record.data[props.name].limit" class="o_cp_pager" role="search">
<Pager t-props="pagerProps"/>
</div>
</div>
<ListRenderer t-if="props.viewMode === 'list'" t-props="rendererProps" />
@@ -8,7 +8,7 @@ import { Component } from "@odoo/owl";
export class ButtonBox extends Component {
setup() {
const ui = useService("ui");
this.getMaxButtons = () => [2, 2, 2, 4][ui.size] || 7;
this.getMaxButtons = () => [3, 3, 3, 7, 3, 4, 7][ui.size] || 7;
}
getButtons() {
@@ -1,77 +1,74 @@
.o-form-buttonbox {
--o-input-border-color: #{map-get($grays, '600')};
margin-bottom: $o-sheet-vpadding;
margin-top: -$o-sheet-vpadding;
@include o-form-sheet-negative-margin;
// Use box-shadow instead of border-bottom because some button boxes are
// empty in some cases and we do not want to see a floating border in
// that cases.
box-shadow: inset 0 -1px 0 $border-color;
// Compute the expected oe_stat_button's height according to the default btn size.
$-stat-button-inner-size: ($btn-padding-y * 2) + ($btn-font-size * $btn-line-height);
$-stat-button-height: $-stat-button-inner-size + o-to-rem($btn-border-width * 2);
.btn.oe_stat_button, > .o_field_widget {
flex: 0 0 auto;
width: percentage(1/3); // Adapt the number of visible buttons for each screen width
@include media-breakpoint-up(md) {
width: percentage(1/5);
}
@include media-breakpoint-up(lg) {
width: percentage(1/7);
}
@include media-breakpoint-up(xl) {
width: percentage(1/8);
}
$-stat-button-icon-size: 1.5em;
$-stat-button-icon-margin-end: $btn-padding-x * 0.5;
%-stat-button-container {
display: flex;
flex-direction: column;
max-width: 100%;
text-align: left;
font-weight: $font-weight-normal;
line-height: 1;
}
&.o_full > *:first-child {
border-left: none !important;
box-shadow: -1px 0 0 $border-color;
%-stat-button-text {
@include o-text-overflow(block);
order: 1;
font-size: $o-font-size-base-smaller;
line-height: $line-height-sm;
}
.btn.oe_stat_button {
height: $o-statbutton-height;
> .o_button_icon {
display: inline-block;
vertical-align: middle;
line-height: $o-statbutton-height;
width: 30%;
%-stat-button-value {
order: 2;
font-weight: $font-weight-bold;
color: $o-brand-primary;
font-size: $o-font-size-base-smaller;
}
%-stat-button-figure-offset {
margin-left: $btn-padding-x * -0.33;
}
.oe_stat_button {
display: flex;
flex-direction: row;
align-items: center;
flex-grow: 1;
padding-top: 0;
padding-bottom: 0;
height: $-stat-button-height;
.o_button_icon {
font-size: $-stat-button-icon-size;
color: $primary;
&:before {
font-size: 22px;
vertical-align: middle;
margin-right: $-stat-button-icon-margin-end;
}
}
> .o_field_percent_pie {
margin-left: $o-statbutton-spacing; // To create the button padding left (firefox bug)
}
// Some buttons only display text without using StatInfo template
> span {
@include o-text-overflow(block);
white-space: normal; // text on several lines if needed
}
> .o_stat_info, > span, > .o_field_statinfo { // contains the value and text
display: inline-block;
vertical-align: middle;
font-weight: $font-weight-normal;
.o_stat_info, > span, .o_field_statinfo {
@extend %-stat-button-container;
max-width: 70%;
padding-right: $o-statbutton-spacing;
line-height: 1.3;
> .o_stat_value, > .o_stat_text {
@include o-text-overflow(block);
line-height: 1.2;
.o_stat_text {
@extend %-stat-button-text;
}
.o_stat_value {
font-weight: $font-weight-bold;
color: map-get($o-theme-text-colors, 'odoo');
@extend %-stat-button-value;
}
.o_stat_text .o_field_empty {
.o_field_empty {
display: none;
}
}
@@ -82,6 +79,10 @@
}
}
&:not(:first-child) .o_button_icon {
@extend %-stat-button-figure-offset;
}
&:not(:hover) .o_stat_info > .o_hover {
display: none !important;
}
@@ -89,11 +90,21 @@
display: none !important
}
}
.oe_stat_button {
border-left: 1px solid $border-color;
> * + * {
margin-left: -$border-width;
}
&.o_full > .oe_stat_button:first-child {
border-left: none;
.oe_stat_button, > div > button {
border-radius: 0;
}
.oe_stat_button:first-child, div:first-child > button {
@include border-start-radius($border-radius);
}
.oe_stat_button:last-child, div:last-child > button {
@include border-end-radius($border-radius);
}
// "More" button and dropdown
@@ -101,25 +112,43 @@
> .o_button_more {
width: 100%;
height: 100%;
&:after {
margin-left: 5px;
@include o-caret-down;
}
}
&.show > .o_button_more {
&:after {
@include o-caret-up;
}
}
.o_dropdown_more {
width: 150px;
> .dropdown-item {
.oe_stat_button {
border: none;
width: 100%;
}
.oe_stat_button {
height: auto;
width: 100%;
border: none;
padding-top: map-get($spacers, 2);
padding-bottom: map-get($spacers, 2);
}
}
}
// Piechart
--PercentPieField-size: #{$-stat-button-icon-size};
--PercentPieField-gap: #{$-stat-button-icon-margin-end};
.oe_stat_button:not(:first-child) .o_pie {
@extend %-stat-button-figure-offset;
}
.o_pie_info {
@extend %-stat-button-container;
}
.o_pie_text {
@extend %-stat-button-text;
}
.o_pie_value {
@extend %-stat-button-value;
}
}
@@ -5,10 +5,10 @@
<t t-set="allButtons" t-value="getButtons()" />
<t t-set="visibleButtons" t-value="allButtons['visible']" />
<t t-set="additionalButtons" t-value="allButtons['additional']" />
<div class="o-form-buttonbox oe_button_box position-relative text-end" t-attf-class="{{ visibleButtons.length >= getMaxButtons() ? 'o_full' : 'o_not_full'}} {{this.props.class}}">
<div class="o-form-buttonbox position-relative d-flex w-md-auto" t-attf-class="{{ visibleButtons.length >= getMaxButtons() ? 'o_full w-100' : 'o_not_full'}} {{this.props.class}}">
<t t-slot="{{ button_value }}" t-foreach="visibleButtons" t-as="button" t-key="button_value"/>
<t t-if="additionalButtons.length" >
<Dropdown togglerClass="'btn btn-light o_button_more border-0 border-start border-bottom'" menuClass="'o_dropdown_more position-absolute start-0 top-100 w-100 p-0 border-0'" class="'oe_stat_button btn position-relative p-0 border-0'">
<Dropdown position="'bottom-end'" togglerClass="'o_button_more btn btn-outline-secondary'" menuClass="'o_dropdown_more position-absolute top-100 w-100 p-0 border-0'" class="'oe_stat_button btn position-relative p-0 border-0'">
<t t-set-slot="toggler"><span>More</span></t>
<DropdownItem t-foreach="additionalButtons" t-as="button" t-key="button_value" class="'d-flex flex-column p-0'">
<t t-slot="{{ button_value }}" />
@@ -1,2 +1,2 @@
$o-form-view-sheet-max-width: 1140px !default;
$o-form-view-sheet-max-width: 1534px !default;
$o-form-picture-size: 90px !default;
@@ -63,9 +63,11 @@ export class FormCompiler extends ViewCompiler {
);
}
compile() {
compile(key, params = {}) {
const compiled = super.compile(...arguments);
compiled.children[0].setAttribute("t-ref", "compiled_view_root");
if (!params.isSubView) {
compiled.children[0].setAttribute("t-ref", "compiled_view_root");
}
return compiled;
}
@@ -151,17 +153,10 @@ export class FormCompiler extends ViewCompiler {
if (child.tagName === "button" || child.children.tagName === "button") {
child.classList.add(
"oe_stat_button",
"btn-light",
"flex-shrink-0",
"mb-0",
"py-0",
"border-0",
"border-start",
"border-bottom",
"rounded-0",
"text-start",
"text-nowrap",
"text-capitalize"
"btn",
"btn-outline-secondary",
"flex-grow-1",
"flex-lg-grow-0"
);
}
if (child.tagName === "field") {
@@ -230,7 +225,11 @@ export class FormCompiler extends ViewCompiler {
});
if (!sheetNode) {
for (const child of el.childNodes) {
append(form, this.compileNode(child, params));
// ButtonBox are already compiled for the control panel and should not
// be recompiled for the renderer of the view
if (child.attributes?.name?.value !== "button_box") {
append(form, this.compileNode(child, params));
}
}
form.classList.add("o_form_nosheet");
} else {
@@ -407,7 +406,7 @@ export class FormCompiler extends ViewCompiler {
compileHeader(el, params) {
const statusBar = createElement("div");
statusBar.className =
"o_form_statusbar position-relative d-flex justify-content-between border-bottom";
"o_form_statusbar position-relative d-flex justify-content-between mb-2 px-3 pb-2 pb-md-0 px-md-0";
const buttons = [];
const others = [];
for (const child of el.childNodes) {
@@ -644,12 +643,12 @@ export class FormCompiler extends ViewCompiler {
sheetBG.className = "o_form_sheet_bg";
const sheetFG = createElement("div");
sheetFG.className = "o_form_sheet position-relative clearfix";
sheetFG.className = "o_form_sheet position-relative";
append(sheetBG, sheetFG);
for (const child of el.childNodes) {
const compiled = this.compileNode(child, params);
if (!compiled) {
if (!compiled || compiled.nodeName === "ButtonBox") {
continue;
}
if (getTag(child, true) === "field") {
@@ -18,8 +18,14 @@ import { isX2Many } from "@web/views/utils";
import { useViewButtons } from "@web/views/view_button/view_button_hook";
import { useSetupView } from "@web/views/view_hook";
import { FormStatusIndicator } from "./form_status_indicator/form_status_indicator";
import { ButtonBox } from "./button_box/button_box";
import { ViewButton } from "@web/views/view_button/view_button";
import { Field } from "@web/views/fields/field";
import { Component, onRendered, useEffect, useRef, useState } from "@odoo/owl";
import { useViewCompiler } from "../view_compiler";
import { FormCompiler } from "./form_compiler";
import { evalDomain } from "../utils";
const viewRegistry = registry.category("views");
@@ -180,6 +186,17 @@ export class FormController extends Component {
this.archInfo.arch = this.archInfo.xmlDoc.outerHTML;
}
const xmlDocButtonBox = this.archInfo.xmlDoc.querySelector("div[name='button_box']");
if (xmlDocButtonBox) {
const buttonBoxTemplates = useViewCompiler(
this.props.Compiler || FormCompiler,
xmlDocButtonBox.outerHTML,
{ ButtonBox: xmlDocButtonBox },
{ isSubView: true }
);
this.buttonBoxTemplate = buttonBoxTemplates.ButtonBox;
}
const rootRef = useRef("root");
useViewButtons(this.model, rootRef, {
beforeExecuteAction: this.beforeExecuteActionButton.bind(this),
@@ -471,10 +488,21 @@ export class FormController extends Component {
result["o_field_highlight"] = size < SIZES.SM || hasTouch();
return result;
}
evalDomainFromRecord(record, expr) {
return evalDomain(expr, record.evalContext);
}
}
FormController.template = `web.FormView`;
FormController.components = { ActionMenus, FormStatusIndicator, Layout };
FormController.components = {
ActionMenus,
FormStatusIndicator,
Layout,
ButtonBox,
ViewButton,
Field,
};
FormController.props = {
...standardViewProps,
discardRecord: { type: Function, optional: true },
@@ -16,16 +16,12 @@
}
// Compensate margins
@mixin o-form-sheet-negative-margin {
margin-left: -$o-horizontal-padding;
margin-right: -$o-horizontal-padding;
@include media-breakpoint-up(lg, $o-extra-grid-breakpoints) {
margin-left: -$o-horizontal-padding*2;
margin-right: -$o-horizontal-padding*2;
}
margin-left: calc(var(--formView-sheet-padding-x) * -1);
margin-right: calc(var(--formView-sheet-padding-x) * -1);
}
@mixin o-form-nosheet-negative-margin {
margin-left: -$o-horizontal-padding;
margin-right: -$o-horizontal-padding;
margin-left: calc(var(--formView-sheet-padding-x) * -1);
margin-right: calc(var(--formView-sheet-padding-x) * -1);
}
// XXS form view specific rules
@@ -66,7 +62,18 @@
}
.o_form_view {
background-color: $o-view-background-color;
// Component custom properties
--formView-sheetBg-padding-top: #{map-get($spacers, 2)};
--formView-sheetBg-padding-x: #{map-get($spacers, 3)};
--formView-sheet-padding-y: #{map-get($spacers, 3)};
--formView-sheet-padding-x: #{map-get($spacers, 3)};
--formView-sheet-padding-y-xxl: #{map-get($spacers, 4)};
--formView-sheet-padding-x-xxl: #{map-get($spacers, 4)};
--formView-sheet-border-color: #{$border-color};
--formView-sheet-border-width: #{$border-width};
--formView-sheet-border-radius: #{$border-radius};
--formView-sheet-background: #{$o-view-background-color};
@include media-breakpoint-up(md) {
display: flex;
@@ -242,9 +249,6 @@
}
.o_notebook {
--notebook-margin-x: -#{$o-horizontal-padding};
--notebook-padding-x: #{$o-horizontal-padding};
> .tab-content > .tab-pane > :first-child:not(.o_group) {
// These elements will appear attached to the tabs
&.o_field_x2many.o_field_x2many_list {
@@ -254,45 +258,49 @@
}
}
// Sheet
// Sheet BG
.o_form_sheet_bg {
position: relative;
flex: 1 0 auto;
background-color: $o-webclient-background-color;
border-bottom: 1px solid $border-color;
padding-top: var(--formView-sheetBg-padding-top);
padding-left: var(--formView-sheetBg-padding-left, var(--formView-sheetBg-padding-x));
padding-right: var(--formView-sheetBg-padding-right, var(--formView-sheetBg-padding-x));
width: 100%;
max-width: $o-form-view-sheet-max-width;
margin-right: auto; // Always align to le left
> .o_form_sheet {
@include make-container();
@include make-container-max-widths();
@include media-breakpoint-down(md) {
--formView-sheetBg-padding-x: var(--formView-sheetBg-padding-x-sm, 0);
}
}
background-color: $o-view-background-color;
box-shadow: 0 5px 20px -15px rgba(#000, 0.4);
margin: $o-sheet-vpadding*0.2 auto;
@include media-breakpoint-up(md) {
margin: $o-sheet-vpadding*0.5 auto;
}
padding: $o-sheet-vpadding;
@include o-form-sheet-inner-right-padding;
@include o-form-sheet-inner-left-padding;
// Sheet
.o_form_sheet {
padding: var(--formView-sheet-padding-y) var(--formView-sheet-padding-x);
border-style: solid;
border-color: var(--formView-sheet-border-color);
border-width: var(--formView-sheet-border-width);
border-radius: var(--formView-sheet-border-radius);
background-color: var(--formView-sheet-background);
@include media-breakpoint-up(sm) {
border: 1px solid $border-color;
}
@include media-breakpoint-down(md) {
--formView-sheet-padding-y: #{map-get($spacers, 4)};
--formView-sheet-padding-x: #{map-get($spacers, 3)};
--formView-sheet-border-width: 0 0 1px 0;
--formView-sheet-border-radius: 0;
}
// Selection
> .o_selection {
float: right;
}
@include media-breakpoint-up(xxl, $o-extra-grid-breakpoints) {
--formView-sheet-padding-y: var(--formView-sheet-padding-y-xxl);
--formView-sheet-padding-x: var(--formView-sheet-padding-x-xxl);
}
// Selection
> .o_selection {
float: right;
}
}
// Statusbar
.o_form_statusbar {
@include media-breakpoint-up(md) {
padding-left: $o-horizontal-padding;
}
background-color: $o-view-background-color;
> .o_statusbar_status {
display: flex;
align-items: center;
@@ -313,46 +321,8 @@
margin-bottom: 0px;
}
> .o_statusbar_buttons {
> .btn, .o_widget > .btn {
$o-statusbar-buttons-vmargin: 4px;
min-height: $o-statusbar-height - 2 * $o-statusbar-buttons-vmargin;
margin: $o-statusbar-buttons-vmargin 3px $o-statusbar-buttons-vmargin 0;
padding-top: 2px;
padding-bottom: 2px;
> .o_button_icon {
margin-right: $o-statbutton-spacing;
}
}
}
// Touch device mode
> .o_statusbar_buttons > .btn-group, > .o_statusbar_status {
> .dropdown-toggle {
&:after {
@include o-caret-down;
margin-left: $o-form-spacing-unit;
}
}
> .dropdown-menu {
min-width: 0;
.dropdown-item.btn {
min-width: 100%;
margin-bottom: 3px;
opacity: 1;
}
}
}
// Match the status bar button styles with rest of the buttons in touch device
@include media-breakpoint-down(md) {
> .o_statusbar_buttons > .btn {
margin: 0;
padding: $o-touch-btn-padding;
}
border-bottom: $border-width solid $border-color;
}
}
@@ -412,6 +382,8 @@
// Groups
.o_group {
justify-content: space-between;
.o_form_label {
font-weight: $font-weight-normal;
}
@@ -454,6 +426,7 @@
--columns: 1;
gap: map-get($spacers, 2) $o-horizontal-padding;
margin-bottom: map-get($spacers, 2);
max-width: 700px;
span, .o_field_boolean, .oe_avatar, .o_form_uri {
&.o_field_widget {
@@ -507,19 +480,22 @@
}
}
// Contextual adaptation
.o_kanban_renderer {
--Kanban-background: transparent;
--Kanban-gap: #{map-get($spacers, 2)};
--KanbanRecord-margin-v: 0;
--KanbanRecord-margin-h: 0;
}
// Notebooks
.o_notebook {
--notebook-margin-x: calc(var(--formView-sheet-padding-x) * -1);
--notebook-padding-x: var(--formView-sheet-padding-x);
clear: both; // For the notebook not to have alongside floating elements
margin-top: $o-form-spacing-unit * 2;
--notebook-margin-x: -#{$o-horizontal-padding};
--notebook-padding-x: #{$o-horizontal-padding};
@include media-breakpoint-up(lg, $o-extra-grid-breakpoints) {
--notebook-margin-x: -#{$o-horizontal-padding * 2};
--notebook-padding-x: #{$o-horizontal-padding * 2};
}
> .tab-content {
border-bottom: 1px solid $border-color;
@@ -955,16 +931,6 @@
}
}
.o_form_sheet {
padding: 16px;
}
.o_form_sheet_bg > .o_form_sheet {
border-top: 0;
border-bottom: 0;
margin: 0 auto;
}
.o_form_label:not(.o_invisible_modifier) {
padding-bottom: 4px;
}
@@ -1049,7 +1015,15 @@
}
.o_form_sheet_bg {
padding: 0;
/**
* Reset all paddings and margins to 0.
* Make the view fill all available room.
*/
--formView-sheetBg-padding-top: 0;
--formView-sheetBg-padding-x: 0;
--formView-sheet-padding: 0;
--formView-sheet-border-radius: 0;
width: 100%;
> .o_form_statusbar, > .alert {
@@ -1122,24 +1096,14 @@
flex-flow: row nowrap !important;
.o_form_view_container {
width: $o-form-sheet-min-width + 2 * $o-horizontal-padding;
width: 1px; // List view needs a width value to recompute the size correctly
}
.o_form_sheet_bg {
flex: 1 1 auto; // Side chatter is disabled if this has to shrink but this was added for safety
padding: 0 $o-horizontal-padding;
overflow: auto;
border-bottom: none;
> .o_form_statusbar, > .alert {
margin-left: -$o-horizontal-padding;
margin-right: -$o-horizontal-padding;
}
> .o_form_sheet {
width: 100%;
max-width: map-get($container-max-widths, xxl);
}
display: flex;
flex-direction: column;
flex: 1 1 $o-form-sheet-min-width; // Side chatter is disabled if this has to shrink but this was added for safety
}
}
@@ -44,8 +44,8 @@
</t>
<t t-name="web.FormView.Buttons" owl="1">
<div class="o_cp_buttons" role="toolbar" aria-label="Main actions" t-ref="cpButtons">
<div t-if="model.root.isInEdition" class="o_form_buttons_edit">
<div class="o_cp_buttons d-flex align-items-baseline" role="toolbar" aria-label="Main actions" t-ref="cpButtons">
<div t-if="model.root.isInEdition" class="o_form_buttons_edit d-flex gap-1">
<button type="button" class="btn btn-primary o_form_button_save" data-hotkey="s" t-on-click.stop="() => this.saveButtonClicked({closable: true})">
Save
</button>
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.StatusBarButtons" owl="1">
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap">
<div class="o_statusbar_buttons d-flex align-items-center align-content-around flex-wrap gap-1">
<t t-set="slots" t-value="visibleSlotNames" />
<t t-if="env.isSmall and slots.length > 1">
<Dropdown togglerClass="'btn btn-primary'" showCaret="true">
@@ -11,6 +11,7 @@
// Default KanbanView values
--Kanban-background: #{$o-kanban-background};
--Kanban-gap: unset;
--Kanban-padding: 0;
--KanbanGroup-background: var(--Kanban-background);
@@ -40,6 +41,7 @@
border-top: $border-width solid var(--Kanban-background);
background: var(--Kanban-background);
padding: var(--Kanban-padding);
gap: var(--Kanban-gap);
@include media-breakpoint-down(md) {
--KanbanRecord-padding-h: #{$o-kanban-inside-hgutter * 2};
@@ -25,7 +25,7 @@ function iconFromString(iconString) {
const icon = {};
if (iconString.startsWith("fa-")) {
icon.tag = "i";
icon.class = `fa fa-fw o_button_icon ${iconString}`;
icon.class = `o_button_icon fa ${iconString}`;
} else {
icon.tag = "img";
icon.src = iconString;
@@ -18,7 +18,7 @@
t-att-tabindex="props.tabindex"
t-on-click.stop="onClick"
>
<t t-if="icon" t-tag="icon.tag" t-att-class="icon.class" t-att-src="icon.src"/>
<t t-if="icon" t-tag="icon.tag" t-att-class="icon.class + (props.string ? ' me-1' : '')" t-att-src="icon.src"/>
<t t-slot="contents"><span t-if="props.string" t-esc="props.string"/></t>
</t>
</t>
@@ -184,7 +184,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<t>
<div class="o_form_renderer o_form_nosheet" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
<div class="o_form_statusbar position-relative d-flex justify-content-between pb-3 px-3"><StatusBarButtons/></div>
<div>someDiv</div>
</div>
</t>`;
@@ -206,10 +206,10 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<t>
<div class="o_form_renderer" t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ __comp__.uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="o_form_sheet_bg">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
<div class="o_form_sheet_bg pt-2">
<div class="o_form_statusbar position-relative d-flex justify-content-between pb-3 px-3"><StatusBarButtons/></div>
<div>someDiv</div>
<div class="o_form_sheet position-relative clearfix">
<div class="o_form_sheet position-relative clearfix px-3">
<div>inside sheet</div>
</div>
</div>
@@ -267,7 +267,7 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
<div class="o_form_statusbar position-relative d-flex justify-content-between pb-3 px-3">
<StatusBarButtons>
<t t-set-slot="button_0" isVisible="true">
<div>someDiv</div>
@@ -285,7 +285,7 @@ QUnit.module("Form Compiler", (hooks) => {
</form>`;
const expected = /*xml*/ `
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
<div class="o_form_statusbar position-relative d-flex justify-content-between pb-3 px-3">
<StatusBarButtons/>
</div>`;