[IMP] web, mail: change layout of form view
This commit changes the layout of the form view to move the control panel next to the chatter instead of being on top. The chatter now takes all the place on the right side of the view. task id: 2822553 X-original-commit: 7f36dd05b3238c3d27aab6e5af12233df27067e4 Part-of: odoo/odoo#101118
This commit is contained in:
@@ -193,6 +193,7 @@ For more specific needs, you may also assign custom-defined actions
|
||||
'mail/static/src/components/*/*.js',
|
||||
'mail/static/src/components/*/*.scss',
|
||||
'mail/static/src/components/*/*.xml',
|
||||
'mail/static/src/views/*/*.xml',
|
||||
('include', 'mail.assets_messaging'),
|
||||
'mail/static/src/services/*.js',
|
||||
'mail/static/src/views/**/*.js',
|
||||
|
||||
@@ -5,9 +5,9 @@ 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) {
|
||||
let hasActivities = false;
|
||||
let hasFollowers = false;
|
||||
@@ -44,9 +44,9 @@ function compileChatter(node, params) {
|
||||
"hasParentReloadOnFollowersUpdate": hasParentReloadOnFollowersUpdate,
|
||||
"hasParentReloadOnMessagePosted": hasParentReloadOnMessagePosted,
|
||||
"isAttachmentBoxVisibleInitially": isAttachmentBoxVisibleInitially,
|
||||
"threadId": "props.record.resId or undefined",
|
||||
"threadModel": "props.record.resModel",
|
||||
"webRecord": "props.record",
|
||||
"threadId": params.threadId,
|
||||
"threadModel": params.threadModel,
|
||||
"webRecord": params.webRecord,
|
||||
});
|
||||
const chatterContainerHookXml = createElement("div");
|
||||
chatterContainerHookXml.classList.add("o_FormRenderer_chatterContainer");
|
||||
@@ -59,27 +59,94 @@ function compileAttachmentPreview(node, params) {
|
||||
webClientViewAttachmentViewContainerHookXml.classList.add('o_attachment_preview');
|
||||
const webClientViewAttachmentViewContainerXml = createElement("WebClientViewAttachmentViewContainer");
|
||||
setAttributes(webClientViewAttachmentViewContainerXml, {
|
||||
"threadId": "props.record.resId or undefined",
|
||||
"threadModel": "props.record.resModel",
|
||||
"threadId": params.threadId,
|
||||
"threadModel": params.threadModel,
|
||||
});
|
||||
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_FormRenderer_chatterContainer");
|
||||
if (chatterContainerHookXml) {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `!hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
|
||||
"t-attf-class": `{{ uiService.size >= ${SIZES.XXL} ? "o-aside" : "" }}`,
|
||||
});
|
||||
}
|
||||
const attachmentViewHookXml = res.querySelector(".o_attachment_preview");
|
||||
if (attachmentViewHookXml) {
|
||||
setAttributes(attachmentViewHookXml, {
|
||||
"t-if": `hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
|
||||
});
|
||||
}
|
||||
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: "model.root",
|
||||
});
|
||||
append(compiledRoot, compiledChild);
|
||||
}
|
||||
return compiledRoot;
|
||||
}
|
||||
|
||||
compileChatter(node) {
|
||||
return compileChatter(node, {
|
||||
threadId: "model.root.resId or undefined",
|
||||
threadModel: "model.root.resModel",
|
||||
webRecord: "model.root",
|
||||
});
|
||||
}
|
||||
|
||||
compileAttachmentPreview(node) {
|
||||
return compileAttachmentPreview(node, {
|
||||
threadId: "model.root.resId or undefined",
|
||||
threadModel: "model.root.resModel",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("form_compilers").add("chatter_compiler", {
|
||||
selector: "div.oe_chatter",
|
||||
fn: compileChatter,
|
||||
fn: (node) =>
|
||||
compileChatter(node, {
|
||||
threadId: "props.record.resId or undefined",
|
||||
threadModel: "props.record.resModel",
|
||||
webRecord: "props.record",
|
||||
}),
|
||||
});
|
||||
|
||||
registry.category("form_compilers").add("attachment_preview_compiler", {
|
||||
selector: "div.o_attachment_preview",
|
||||
fn: compileAttachmentPreview,
|
||||
fn: (node) =>
|
||||
compileAttachmentPreview(node, {
|
||||
threadId: "props.record.resId or undefined",
|
||||
threadModel: "props.record.resModel",
|
||||
}),
|
||||
});
|
||||
|
||||
patch(FormCompiler.prototype, 'mail', {
|
||||
compile() {
|
||||
compile(node, params) {
|
||||
// TODO no chatter if in dialog?
|
||||
const res = this._super(...arguments);
|
||||
const res = this._super(node, params);
|
||||
const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer');
|
||||
if (!chatterContainerHookXml) {
|
||||
return res; // no chatter, keep the result as it is
|
||||
@@ -97,17 +164,12 @@ patch(FormCompiler.prototype, 'mail', {
|
||||
if (!parentXml) {
|
||||
return res; // miss-config: a sheet-bg is required for the rest
|
||||
}
|
||||
const webClientViewAttachmentViewHookXml = res.querySelector('.o_attachment_preview');
|
||||
// TODO hasAttachmentViewer should also depend on the groups= and/or invisible modifier on o_attachment_preview (see invoice form)
|
||||
if (webClientViewAttachmentViewHookXml) {
|
||||
if (params.hasAttachmentViewer) {
|
||||
// in sheet bg (attachment viewer present)
|
||||
setAttributes(webClientViewAttachmentViewHookXml, {
|
||||
't-if': `hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
|
||||
});
|
||||
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
|
||||
const sheetBgChatterContainerHookXml = chatterContainerHookXml;
|
||||
sheetBgChatterContainerHookXml.classList.add('o-isInFormSheetBg');
|
||||
setAttributes(sheetBgChatterContainerHookXml, {
|
||||
't-if': `hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
|
||||
't-if': `uiService.size < ${SIZES.XXL} or hasAttachmentViewer()`,
|
||||
});
|
||||
append(formSheetBgXml, sheetBgChatterContainerHookXml);
|
||||
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector('ChatterContainer');
|
||||
@@ -116,29 +178,19 @@ patch(FormCompiler.prototype, 'mail', {
|
||||
"hasExternalBorder": "true",
|
||||
"hasMessageListScrollAdjust": "false",
|
||||
});
|
||||
}
|
||||
// after sheet bg (standard position, either aside or below)
|
||||
if (webClientViewAttachmentViewHookXml) {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-if': `!(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL})`,
|
||||
't-attf-class': `{{ uiService.size >= ${SIZES.XXL} and !(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}) ? "o-aside" : "" }}`,
|
||||
});
|
||||
setAttributes(chatterContainerXml, {
|
||||
"isInFormSheetBg": "hasAttachmentViewer()",
|
||||
"hasExternalBorder": `!(uiService.size >= ${SIZES.XXL} and !(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}))`,
|
||||
"hasMessageListScrollAdjust": `uiService.size >= ${SIZES.XXL} and !(hasAttachmentViewer() and uiService.size >= ${SIZES.XXL})`,
|
||||
});
|
||||
} else {
|
||||
// after sheet bg (standard position, either aside or below)
|
||||
setAttributes(chatterContainerXml, {
|
||||
"isInFormSheetBg": "false",
|
||||
"hasExternalBorder": `uiService.size < ${SIZES.XXL}`,
|
||||
"hasMessageListScrollAdjust": `uiService.size >= ${SIZES.XXL}`,
|
||||
});
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-if': `uiService.size < ${SIZES.XXL}`,
|
||||
't-attf-class': `{{ uiService.size >= ${SIZES.XXL} ? "o-aside" : "" }}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
}
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useModels } from "@mail/component_hooks/use_models";
|
||||
import { ChatterContainer } from "@mail/components/chatter_container/chatter_container";
|
||||
import { WebClientViewAttachmentViewContainer } from "@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
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";
|
||||
|
||||
const { onMounted, onWillUnmount } = owl;
|
||||
|
||||
patch(FormController.prototype, "mail", {
|
||||
setup() {
|
||||
this._super();
|
||||
this.uiService = useService("ui");
|
||||
this.hasAttachmentViewerInArch = false;
|
||||
|
||||
if (this.env.services.messaging) {
|
||||
useModels();
|
||||
}
|
||||
|
||||
const { archInfo } = this.props;
|
||||
const { arch, xmlDoc } = archInfo;
|
||||
|
||||
const template = document.createElement("t");
|
||||
const xmlDocAttachmentPreview = 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;
|
||||
}
|
||||
|
||||
const xmlDocChatter = xmlDoc.querySelector("div.oe_chatter");
|
||||
if (xmlDocChatter && xmlDocChatter.parentNode.nodeName === "form") {
|
||||
template.appendChild(xmlDocChatter.cloneNode(true));
|
||||
}
|
||||
|
||||
const mailTemplates = useViewCompiler(MailFormCompiler, 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 {Messaging|undefined}
|
||||
*/
|
||||
getMessaging() {
|
||||
return this.env.services.messaging && this.env.services.messaging.modelManager.messaging;
|
||||
},
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAttachmentViewer() {
|
||||
if (!this.getMessaging() || !this.model.root.resId) {
|
||||
return false;
|
||||
}
|
||||
const thread = this.getMessaging().models['Thread'].insert({
|
||||
id: this.model.root.resId,
|
||||
model: this.model.root.resModel,
|
||||
});
|
||||
return thread.attachmentsInWebClientView.length > 0;
|
||||
},
|
||||
evalDomainFromRecord(record, expr) {
|
||||
return evalDomain(expr, record.evalContext);
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(FormController.components, {
|
||||
ChatterContainer,
|
||||
WebClientViewAttachmentViewContainer,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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>
|
||||
</xpath>
|
||||
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="hasAttachmentViewer">hasAttachmentViewerInArch</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -15,6 +15,13 @@ patch(FormRenderer.prototype, 'mail', {
|
||||
}
|
||||
},
|
||||
|
||||
get compileParams() {
|
||||
return {
|
||||
...this._super(),
|
||||
hasAttachmentViewer: this.props.hasAttachmentViewer,
|
||||
};
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Mail Methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
.o_FormRenderer_chatterContainer {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
margin: 0 auto;
|
||||
max-width: $o-form-view-sheet-max-width;
|
||||
padding: map-get($spacers, 3) map-get($spacers, 3) map-get($spacers, 5);
|
||||
|
||||
@@ -224,6 +224,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_form_view_container {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
// No sheet
|
||||
&.o_form_nosheet, .o_form_nosheet {
|
||||
@include o-webclient-padding($top: $o-sheet-vpadding, $bottom: $o-sheet-vpadding);
|
||||
@@ -1150,6 +1157,7 @@
|
||||
}
|
||||
|
||||
.o_form_view.o_xxl_form_view {
|
||||
flex-flow: row nowrap !important;
|
||||
|
||||
.o_form_sheet_bg {
|
||||
flex: 1 1 auto; // Side chatter is disabled if this has to shrink but this was added for safety
|
||||
|
||||
@@ -3,43 +3,58 @@
|
||||
|
||||
<t t-name="web.FormView" owl="1">
|
||||
<div t-att-class="className" t-ref="root">
|
||||
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="display">
|
||||
<t t-set-slot="layout-buttons">
|
||||
<t t-if="footerArchInfo and env.inDialog">
|
||||
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo"/>
|
||||
<div class="o_form_view_container">
|
||||
<Layout className="model.useSampleModel ? 'o_view_sample_data' : ''" display="display">
|
||||
<t t-set-slot="layout-buttons">
|
||||
<t t-if="footerArchInfo and env.inDialog">
|
||||
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="{{ props.buttonTemplate }}"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="{{ props.buttonTemplate }}"/>
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-if="props.info.actionMenus and !model.root.isVirtual">
|
||||
<ActionMenus
|
||||
activeIds="[model.root.resId]"
|
||||
context="props.context"
|
||||
items="getActionMenuItems()"
|
||||
isDomainSelected="model.root.isDomainSelected"
|
||||
resModel="model.root.resModel"
|
||||
domain="props.domain"
|
||||
onActionExecuted="() => model.load()"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-if="!env.isSmall and props.info.actionMenus and !model.root.isVirtual">
|
||||
<ActionMenus
|
||||
activeIds="[model.root.resId]"
|
||||
context="props.context"
|
||||
items="getActionMenuItems()"
|
||||
isDomainSelected="model.root.isDomainSelected"
|
||||
resModel="model.root.resModel"
|
||||
domain="props.domain"
|
||||
onActionExecuted="() => model.load()"
|
||||
/>
|
||||
<t t-set-slot="control-panel-bottom-left">
|
||||
<t t-if="!env.isSmall and props.info.actionMenus and !model.root.isVirtual">
|
||||
<ActionMenus
|
||||
activeIds="[model.root.resId]"
|
||||
context="props.context"
|
||||
items="getActionMenuItems()"
|
||||
isDomainSelected="model.root.isDomainSelected"
|
||||
resModel="model.root.resModel"
|
||||
domain="props.domain"
|
||||
onActionExecuted="() => model.load()"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-set-slot="control-panel-bottom-right">
|
||||
<t t-if="env.isSmall and props.info.actionMenus and !model.root.isVirtual">
|
||||
<ActionMenus
|
||||
activeIds="[model.root.resId]"
|
||||
context="props.context"
|
||||
items="getActionMenuItems()"
|
||||
isDomainSelected="model.root.isDomainSelected"
|
||||
resModel="model.root.resModel"
|
||||
domain="props.domain"
|
||||
onActionExecuted="() => model.load()"
|
||||
/>
|
||||
<t t-set-slot="control-panel-bottom-right">
|
||||
<t t-if="env.isSmall and props.info.actionMenus and !model.root.isVirtual">
|
||||
<ActionMenus
|
||||
activeIds="[model.root.resId]"
|
||||
context="props.context"
|
||||
items="getActionMenuItems()"
|
||||
isDomainSelected="model.root.isDomainSelected"
|
||||
resModel="model.root.resModel"
|
||||
domain="props.domain"
|
||||
onActionExecuted="() => model.load()"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" translateAlert="translateAlert"/>
|
||||
</Layout>
|
||||
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="archInfo" translateAlert="translateAlert"/>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -65,6 +65,10 @@ export class FormRenderer extends Component {
|
||||
evalDomainFromRecord(record, expr) {
|
||||
return evalDomain(expr, record.evalContext);
|
||||
}
|
||||
|
||||
get compileParams() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
FormRenderer.template = xml`<t t-call="{{ templates.FormRenderer }}" />`;
|
||||
|
||||
@@ -254,7 +254,8 @@ export class ViewCompiler {
|
||||
return;
|
||||
}
|
||||
if (!params.enableInvisible) {
|
||||
let isVisileExpr = `!evalDomainFromRecord(props.record,${JSON.stringify(invisible)})`;
|
||||
const recordExpr = params.recordExpr || "props.record";
|
||||
let isVisileExpr = `!evalDomainFromRecord(${recordExpr},${JSON.stringify(invisible)})`;
|
||||
if (compiled.hasAttribute("t-if")) {
|
||||
const formerTif = compiled.getAttribute("t-if");
|
||||
isVisileExpr = `( ${formerTif} ) and ${isVisileExpr}`;
|
||||
|
||||
@@ -20,13 +20,17 @@ export class SettingsFormRenderer extends FormRenderer {
|
||||
if (!labels[this.props.archInfo.arch]) {
|
||||
labels[this.props.archInfo.arch] = [];
|
||||
}
|
||||
this.compileParams = {
|
||||
super.setup();
|
||||
this.searchState = useState(this.env.searchState);
|
||||
}
|
||||
|
||||
get compileParams() {
|
||||
return {
|
||||
...super.compileParams,
|
||||
labels: labels[this.props.archInfo.arch],
|
||||
getFieldExpr: this.getFieldExpr,
|
||||
record: this.props.record,
|
||||
};
|
||||
super.setup();
|
||||
this.searchState = useState(this.env.searchState);
|
||||
}
|
||||
|
||||
getFieldExpr(fieldName, fieldWidget) {
|
||||
|
||||
@@ -34,7 +34,7 @@ html {
|
||||
> .o_control_panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
> .o_content {
|
||||
.o_content {
|
||||
flex: 1 1 auto;
|
||||
position: relative; // Allow to redistribute the 100% height to its child
|
||||
overflow: auto;
|
||||
@@ -47,7 +47,7 @@ html {
|
||||
&:not(.o_action_delegate_scroll) {
|
||||
overflow: auto;
|
||||
|
||||
> .o_content {
|
||||
.o_content {
|
||||
overflow: initial;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user