[FIX] web,*: repair root class attr
*: web + adaptations in base, fleet, hr, hr_expense, hr_recruitment,
loyalty, lunch, mail, mass_mailing, note, project, stock, survey,
web_tour
**Foreword**
Since d19037e141 the rootnode class attribute for form/list views was
copied two times:
- on the o_view_controller div
- and on the root node of the view renderer.
Examples:
<form class="foo">...</form>
gives
<div class="o_view_controller o_form_view foo">
<div class="o_control_panel">...</div>
<div class="o_content">
<div class="foo o_form_editable ...">...</div>
</div>
</div>
and
<list class="foo">...</list>
gives
<div class="o_view_controller o_list_view foo">
<div class="o_control_panel">...</div>
<div class="o_content">
<div class="o_list_renderer foo ...">...</div>
</div>
</div>
**Issue**
This could lead to confusion and also unexpected styling issues.
See this PR #119815 to read a message JS Framework team has received.
See also another a fix that had to be made for x2m fields: 980244fa8
**Introduced Changes**
- in the form compiler, the root node attributes are no more copied to
the root div node of the compiled template the form renderer receives
- the root div node generated by the form compiler now has the
"o_form_renderer" class, which was removed during the recent form view
refactoring.
- the list renderer no more adds the root node class attribute to its
"o_list_renderer" div
- the X2ManyFieldDialog has been adapted too
- since View, X2ManyField & X2ManyFieldDialog both need to compute view
classnames derivated from the arch root node, an util has been
introduced to avoid duplicating code
- the whole codebase has been checked and adapted.
Related: odoo/enterprise#40418
Signed-off-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.o_fleet_form {
|
||||
.o_fleet_form_view .o_form_renderer {
|
||||
.oe_stat_button.text-warning, .oe_stat_button.text-danger {
|
||||
.o_stat_text {
|
||||
color: $o-main-text-color;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="name">fleet.vehicle.form</field>
|
||||
<field name="model">fleet.vehicle</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Vehicle" js_class="fleet_form" class="o_fleet_form">
|
||||
<form string="Vehicle" js_class="fleet_form">
|
||||
<field name="service_activity" invisible="1"/>
|
||||
<header>
|
||||
<button string="Apply New Driver"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
--KanbanRecord-width-small: 350px;
|
||||
}
|
||||
|
||||
.o_employee_form {
|
||||
.o_hr_employee_form_view .o_form_renderer {
|
||||
.o_employee_chat_btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<field name="name">hr.employee.public.form</field>
|
||||
<field name="model">hr.employee.public</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Employee" create="0" write="0" js_class="hr_employee_form" class="o_employee_form">
|
||||
<form string="Employee" create="0" write="0" js_class="hr_employee_form">
|
||||
<field name="image_128" invisible="1" />
|
||||
<header/>
|
||||
<sheet>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<field name="name">hr.employee.form</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Employee" js_class="hr_employee_form" class='o_employee_form'>
|
||||
<form string="Employee" js_class="hr_employee_form">
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="user_partner_id" invisible="1"/>
|
||||
|
||||
@@ -33,12 +33,12 @@ registry.category("web_tour.tours").add('hr_expense_tour' , {
|
||||
mobile: true,
|
||||
}, {
|
||||
trigger: '.o_field_widget[name="product_id"] .o_input_dropdown',
|
||||
extra_trigger: '.o_expense_form',
|
||||
extra_trigger: '.o_hr_expense_form_view_view',
|
||||
content: _t("Enter a name then choose a category and configure the amount of your expense."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_form_status_indicator_dirty .o_form_button_save',
|
||||
extra_trigger: '.o_expense_form',
|
||||
extra_trigger: '.o_hr_expense_form_view_view',
|
||||
content: Markup(_t("Ready? You can save it manually or discard modifications from here. You don't <em>need to save</em> - Odoo will save eveyrthing for you when you navigate.")),
|
||||
position: 'bottom',
|
||||
}, ...stepUtils.statusbarButtonsSteps(_t("Attach Receipt"), _t("Attach a receipt - usually an image or a PDF file.")),
|
||||
@@ -47,11 +47,11 @@ registry.category("web_tour.tours").add('hr_expense_tour' , {
|
||||
...stepUtils.goBackBreadcrumbsMobile(
|
||||
_t("Use the breadcrumbs to go back to the list of expenses."),
|
||||
undefined,
|
||||
".o_expense_form",
|
||||
".o_hr_expense_form_view_view",
|
||||
),
|
||||
{
|
||||
trigger: '.breadcrumb > li.breadcrumb-item:first',
|
||||
extra_triggger: ".o_expense_form",
|
||||
extra_triggger: ".o_hr_expense_form_view_view",
|
||||
content: _t("Let's go back to your expenses."),
|
||||
position: 'bottom',
|
||||
mobile: false,
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
<field name="name">hr.expense.view.form</field>
|
||||
<field name="model">hr.expense</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Expenses" class="o_expense_form" js_class="hr_expense_form_view">
|
||||
<form string="Expenses" js_class="hr_expense_form_view">
|
||||
<header>
|
||||
<button name="action_submit_expenses" string="Create Report" type="object" class="oe_highlight o_expense_submit" attrs="{'invisible': ['|', ('attachment_number', '<=', 0), ('sheet_id', '!=', False)]}" data-hotkey="v"/>
|
||||
<widget name="attach_document" string="Attach Receipt" action="attach_document" attrs="{'invisible': [('attachment_number', '<', 1)]}"/>
|
||||
@@ -655,7 +655,7 @@
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field eval="25" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Expense Reports" class="o_expense_sheet">
|
||||
<form string="Expense Reports">
|
||||
<field name="can_reset" invisible="1"/>
|
||||
<field name="can_approve" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
@@ -777,7 +777,7 @@
|
||||
}"
|
||||
attrs="{'readonly': [('is_editable', '=', False)]}"
|
||||
force_save="1">
|
||||
<tree editable="bottom" class="o_expense_line_list">
|
||||
<tree editable="bottom">
|
||||
<field name="date" optional="show"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
|
||||
@@ -100,7 +100,7 @@ registry.category("web_tour.tours").add('hr_recruitment_tour',{
|
||||
width: 225
|
||||
}, {
|
||||
trigger: ".o_form_button_save",
|
||||
extra_trigger: ".o_employee_form",
|
||||
extra_trigger: ".o_hr_employee_form_view",
|
||||
content: _t("Save it!"),
|
||||
position: "bottom",
|
||||
width: 80
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_applicant_form {
|
||||
.o_form_view.o_applicant_form .o_form_renderer {
|
||||
&.o_form_readonly {
|
||||
.o_field_empty:empty {
|
||||
min-height: unset;
|
||||
|
||||
@@ -100,9 +100,3 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_recruitment_list {
|
||||
.o_list_button {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<field name="name">hr.recruitment.source.tree</field>
|
||||
<field name="model">hr.recruitment.source</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Sources of Applicants" editable="top" class="o_recruitment_list" sample="1">
|
||||
<tree string="Sources of Applicants" editable="top" sample="1">
|
||||
<field name="has_domain" invisible="1"/>
|
||||
<field name="source_id" placeholder="e.g. LinkedIn" decoration-bf="1" attrs="{'readonly': [('id', '!=', False)]}"/>
|
||||
<field name="medium_id" optional="hidden"/>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.loyalty-program-list-view {
|
||||
.o_loyalty_program_list_view_view .o_list_renderer {
|
||||
.o_view_nocontent {
|
||||
top: 10% !important;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
<field name="name">loyalty.program.view.tree</field>
|
||||
<field name="model">loyalty.program</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree js_class="loyalty_program_list_view" class="loyalty-program-list-view">
|
||||
<tree js_class="loyalty_program_list_view">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="program_type"/>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<field name="name">lunch.order.tree</field>
|
||||
<field name="model">lunch.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Order lines Tree" create="false" edit="false" decoration-muted="state == 'cancelled'" class="o_lunch_list" expand="1">
|
||||
<tree string="Order lines Tree" create="false" edit="false" decoration-muted="state == 'cancelled'" expand="1">
|
||||
<header>
|
||||
<button name="action_confirm" type="object" string="Receive"/>
|
||||
</header>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_mail_composer_form {
|
||||
.o_mail_composer_form .o_form_renderer {
|
||||
.oe-bordered-editor[name=body] .o_readonly {
|
||||
border: 1px solid $o-gray-300;
|
||||
padding: 4px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.o_form_view .o_mass_mailing_mailing_form {
|
||||
.o_form_view.o_mass_mailing_mailing_form .o_form_renderer {
|
||||
input.o_field_char.o_field_widget {
|
||||
padding-right: 40px; // Avoid overlapping subject text on emoji widget
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_form_view .o_mass_mailing_mailing_form.o_mass_mailing_form_full_width {
|
||||
.o_form_view.o_mass_mailing_mailing_form.o_mass_mailing_form_full_width .o_form_renderer {
|
||||
.o_form_statusbar {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_form_view .o_mass_mailing_mailing_form {
|
||||
.o_form_view.o_mass_mailing_mailing_form .o_form_renderer {
|
||||
&.o_form_readonly .o_mass_mailing_subject {
|
||||
// Place the favorite button without breaking the emoji widget
|
||||
padding-right: 50px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.o_note_form_view {
|
||||
.o_note_form_view .o_form_renderer {
|
||||
.o_form_sheet_bg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -117,7 +117,7 @@ registry.category("web_tour.tours").add('project_tour', {
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: ".o_form_button_save",
|
||||
extra_trigger: '.o_form_project_tasks.o_form_dirty',
|
||||
extra_trigger: '.o_form_project_tasks .o_form_dirty',
|
||||
content: Markup(_t("You have unsaved changes - no worries! Odoo will automatically save it as you navigate.<br/> You can discard these changes from here or manually save your task.<br/>Let's save it manually.")),
|
||||
position: "bottom",
|
||||
}, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.o_form_project_tasks {
|
||||
.o_form_project_tasks .o_form_renderer {
|
||||
.o_widget_web_ribbon:not(.o_invisible_modifier) + div div[name="kanban_state"] > div {
|
||||
margin-right: 150px;
|
||||
padding-left: 1rem;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<field name="name">project.update.view.form</field>
|
||||
<field name="model">project.update</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project Update" class="o_form_project_update" js_class="form_description_expander">
|
||||
<form string="Project Update" js_class="form_description_expander">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
.o_inventory_adjustments {
|
||||
.o_inventory_report_list_view .o_list_renderer {
|
||||
|
||||
button[name='action_set_inventory_quantity'] {
|
||||
pointer-events: auto;
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
<field name="model">stock.quant</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" decoration-warning='is_outdated' editable="bottom" create="1" edit="1" import="1" js_class="inventory_report_list" class="o_inventory_adjustments" sample="1">
|
||||
<tree default_order="location_id, inventory_date, product_id, package_id, lot_id, owner_id" decoration-warning='is_outdated' editable="bottom" create="1" edit="1" import="1" js_class="inventory_report_list" sample="1">
|
||||
<header>
|
||||
<button name="action_inventory_at_date" string="Inventory at Date" type="object"
|
||||
invisible="((context.get('inventory_mode') and not context.get('inventory_report_mode')) or context.get('no_at_date'))"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.o_survey_question_view_form {
|
||||
.o_survey_question_view_form .o_form_renderer {
|
||||
|
||||
.o_preview_questions {
|
||||
border: 3px solid $o-gray-500;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="name">survey.invite.view.form</field>
|
||||
<field name="model">survey.invite</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Compose Email" class="o_mail_composer_form" style="padding-bottom: 0px;">
|
||||
<form string="Compose Email" class="o_mail_composer_form">
|
||||
<group col="1">
|
||||
<group col="2">
|
||||
<field name="author_id" invisible="1"/>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { createElement } from "@web/core/utils/xml";
|
||||
import { FormArchParser } from "@web/views/form/form_arch_parser";
|
||||
import { loadSubViews } from "@web/views/form/form_controller";
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
import { evalDomain, isNull } from "@web/views/utils";
|
||||
import { computeViewClassName, evalDomain, isNull } from "@web/views/utils";
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
import { useViewButtons } from "@web/views/view_button/view_button_hook";
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
@@ -491,6 +491,7 @@ export class X2ManyFieldDialog extends Component {
|
||||
this.archInfo = this.props.archInfo;
|
||||
this.record = this.props.record;
|
||||
this.title = this.props.title;
|
||||
this.contentClass = computeViewClassName("form", this.archInfo.xmlDoc);
|
||||
useSubEnv({ config: this.props.config });
|
||||
|
||||
useBus(this.record.model.bus, "update", () => this.render(true));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.X2ManyFieldDialog" owl="1">
|
||||
<Dialog title="title" withBodyPadding="false" modalRef="modalRef" contentClass="'o_form_view'">
|
||||
<Dialog title="title" withBodyPadding="false" modalRef="modalRef" contentClass="contentClass">
|
||||
<FormRenderer record="record" archInfo="archInfo"/>
|
||||
<t t-set-slot="footer">
|
||||
<t t-if="footerArchInfo">
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { evalDomain } from "@web/views/utils";
|
||||
import { computeViewClassName, evalDomain } from "@web/views/utils";
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
@@ -44,6 +44,7 @@ export class X2ManyField extends Component {
|
||||
);
|
||||
|
||||
this.archInfo = this.props.views?.[this.props.viewMode] || {};
|
||||
this.className = computeViewClassName(this.props.viewMode, this.archInfo.xmlDoc);
|
||||
|
||||
const { activeActions, creates } = this.archInfo;
|
||||
if (this.props.viewMode === "kanban") {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.X2ManyField" owl="1">
|
||||
<div t-att-class="{ [archInfo.className]: archInfo.className }" t-attf-class="o_field_x2many o_field_x2many_{{props.viewMode}}">
|
||||
<div t-att-class="className" t-attf-class="o_field_x2many o_field_x2many_{{props.viewMode}}">
|
||||
<div class="o_x2m_control_panel">
|
||||
<t t-if="displayControlPanelButtons">
|
||||
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
|
||||
|
||||
@@ -52,7 +52,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
this.compilers.push(
|
||||
...compilersRegistry.getAll(),
|
||||
{ selector: "div[name='button_box']", fn: this.compileButtonBox },
|
||||
{ selector: "form", fn: this.compileForm },
|
||||
{ selector: "form", fn: this.compileForm, doNotCopyAttributes: true },
|
||||
{ selector: "group", fn: this.compileGroup },
|
||||
{ selector: "header", fn: this.compileHeader },
|
||||
{ selector: "label", fn: this.compileLabel, doNotCopyAttributes: true },
|
||||
@@ -224,6 +224,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
const stateClasses =
|
||||
"{{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}";
|
||||
const form = createElement("div", {
|
||||
class: "o_form_renderer",
|
||||
"t-att-class": "__comp__.props.class",
|
||||
"t-attf-class": `{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} ${displayClasses} ${stateClasses}`,
|
||||
});
|
||||
@@ -231,7 +232,7 @@ export class FormCompiler extends ViewCompiler {
|
||||
for (const child of el.childNodes) {
|
||||
append(form, this.compileNode(child, params));
|
||||
}
|
||||
form.className = "o_form_nosheet";
|
||||
form.classList.add("o_form_nosheet");
|
||||
} else {
|
||||
let compiledList = [];
|
||||
for (const child of el.childNodes) {
|
||||
|
||||
@@ -931,7 +931,7 @@
|
||||
display: none; // Hide the handler on readonly fields
|
||||
}
|
||||
|
||||
.oe_form_configuration {
|
||||
&.oe_form_configuration .o_form_renderer {
|
||||
.o_inner_group .o_form_label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1086,6 +1086,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.o_onboarding_payment_provider_wizard .o_form_renderer {
|
||||
a[type="action"] {
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
.o_group.o_inner_group .o_wrap_label {
|
||||
min-width: 100px;
|
||||
@@ -1104,13 +1111,6 @@
|
||||
@include form-break-table;
|
||||
}
|
||||
}
|
||||
|
||||
.o_onboarding_payment_provider_wizard {
|
||||
a[type="action"] {
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -219,6 +219,7 @@ export class KanbanArchParser extends XMLParser {
|
||||
templateDocs,
|
||||
tooltipInfo,
|
||||
examples: xmlDoc.getAttribute("examples"),
|
||||
xmlDoc,
|
||||
__rawArch: arch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ export class ListArchParser extends XMLParser {
|
||||
|
||||
parse(arch, models, modelName) {
|
||||
const xmlDoc = this.parseXML(arch);
|
||||
const className = xmlDoc.getAttribute("class") || null;
|
||||
const fieldNodes = {};
|
||||
const widgetNodes = {};
|
||||
let widgetNextId = 0;
|
||||
@@ -263,7 +262,6 @@ export class ListArchParser extends XMLParser {
|
||||
}
|
||||
|
||||
return {
|
||||
className,
|
||||
creates,
|
||||
handleField,
|
||||
headerButtons,
|
||||
@@ -272,6 +270,7 @@ export class ListArchParser extends XMLParser {
|
||||
activeFields,
|
||||
columns,
|
||||
groupBy,
|
||||
xmlDoc,
|
||||
__rawArch: arch,
|
||||
...treeAttr,
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<t t-name="web.ListRenderer" owl="1">
|
||||
<div
|
||||
class="o_list_renderer o_renderer table-responsive"
|
||||
t-attf-class="{{ props.archInfo.className or '' }}"
|
||||
tabindex="-1"
|
||||
t-ref="root"
|
||||
>
|
||||
|
||||
@@ -73,6 +73,19 @@ export function archParseBoolean(str, trueIfEmpty = false) {
|
||||
return str ? !/^false|0$/i.test(str) : trueIfEmpty;
|
||||
}
|
||||
|
||||
export function computeViewClassName(viewType, rootNode, additionalClassList = []) {
|
||||
const subType = rootNode.getAttribute("js_class");
|
||||
const isValidSubType = subType && registry.category("views").contains(subType);
|
||||
const subTypeClass = isValidSubType ? `o_${subType}_view` : "";
|
||||
const classList = [
|
||||
`o_${viewType}_view`,
|
||||
subTypeClass,
|
||||
...(rootNode.getAttribute("class") || "").split(" "),
|
||||
...additionalClassList,
|
||||
];
|
||||
return [...new Set(classList)].filter((c) => c).join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: doc
|
||||
*
|
||||
@@ -156,8 +169,8 @@ export function getFormattedValue(record, fieldName, attrs) {
|
||||
field: record.fields[fieldName],
|
||||
};
|
||||
return record.data[fieldName] !== undefined
|
||||
? formatter(record.data[fieldName], formatOptions)
|
||||
: "";
|
||||
? formatter(record.data[fieldName], formatOptions)
|
||||
: "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ import { extractLayoutComponents } from "@web/search/layout";
|
||||
import { WithSearch } from "@web/search/with_search/with_search";
|
||||
import { OnboardingBanner } from "@web/views/onboarding_banner";
|
||||
import { useActionLinks } from "@web/views/view_hook";
|
||||
|
||||
import { computeViewClassName } from "./utils";
|
||||
import {
|
||||
Component,
|
||||
markRaw,
|
||||
@@ -268,18 +268,15 @@ export class View extends Component {
|
||||
let subType = rootNode.getAttribute("js_class");
|
||||
const bannerRoute = rootNode.getAttribute("banner_route");
|
||||
const sample = rootNode.getAttribute("sample");
|
||||
const classList = [
|
||||
const className = computeViewClassName(type, rootNode, [
|
||||
"o_view_controller",
|
||||
`o_${type}_view`,
|
||||
...(props.className || "").split(" "),
|
||||
...(rootNode.getAttribute("class") || "").split(" "),
|
||||
];
|
||||
]);
|
||||
|
||||
// determine ViewClass to instantiate (if not already done)
|
||||
if (subType) {
|
||||
if (viewRegistry.contains(subType)) {
|
||||
descr = viewRegistry.get(subType);
|
||||
classList.push(`o_${subType}_view`);
|
||||
} else {
|
||||
subType = null;
|
||||
}
|
||||
@@ -311,7 +308,7 @@ export class View extends Component {
|
||||
relatedModels,
|
||||
resModel,
|
||||
useSampleModel: false,
|
||||
className: [...new Set(classList)].filter((c) => c).join(" "),
|
||||
className,
|
||||
};
|
||||
if (viewDescription.custom_view_id) {
|
||||
// for dashboard
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.o_base_settings {
|
||||
.o_base_settings_view .o_form_renderer {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -27,14 +27,14 @@
|
||||
}
|
||||
|
||||
// Use a very specif selector to overwrite generic form-view rules
|
||||
.o_form_view.o_form_nosheet.o_base_settings {
|
||||
.o_form_view.o_base_settings_view .o_form_renderer.o_form_nosheet {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
// BASE SETTINGS LAYOUT
|
||||
.o_base_settings {
|
||||
.o_base_settings_view .o_form_renderer {
|
||||
--settings__tab-bg: #{map-get($grays, '900')};
|
||||
--settings__tab-bg--active: #{map-get($grays, '800')};
|
||||
--settings__tab-color: #{map-get($grays, '400')};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@include media-breakpoint-down(md) {
|
||||
|
||||
.o_base_settings {
|
||||
.o_base_settings_view {
|
||||
flex-flow: column nowrap;
|
||||
|
||||
> .o_control_panel {
|
||||
|
||||
@@ -38,7 +38,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
const arch = /*xml*/ `<form><div>lol</div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div 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' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<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>lol</div>
|
||||
</div>
|
||||
</t>`;
|
||||
@@ -52,7 +52,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
const arch = /*xml*/ `<form><field field_id="test" name="test"/><label for="test" string=""/></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div 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' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<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">
|
||||
<Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']"/>
|
||||
<FormLabel id="'test'" fieldName="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']" className="""" string="\`\`" />
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
const arch = /*xml*/ `<form><div class="someClass">lol<field field_id="display_name" name="display_name"/></div></form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div 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' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<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="someClass">
|
||||
lol
|
||||
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
|
||||
@@ -120,12 +120,12 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
</form>`;
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div 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' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<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">
|
||||
<OuterGroup>
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<InnerGroup class="scope && scope.className">
|
||||
<t t-set-slot="item_0" type="'item'" sequence="0" t-slot-scope="scope" isVisible="true" itemSpan="1">
|
||||
<div 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' : '' }} {{scope && scope.className || "" }}" class="o_form_nosheet">
|
||||
<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' : '' }} {{scope && scope.className || "" }}">
|
||||
<div><Field id="'test'" name="'test'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['test']"/></div>
|
||||
</div>
|
||||
</t>
|
||||
@@ -183,7 +183,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div 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' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
|
||||
<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>someDiv</div>
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ QUnit.module("Form Compiler", (hooks) => {
|
||||
|
||||
const expected = /*xml*/ `
|
||||
<t>
|
||||
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ __comp__.uiService.size < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
|
||||
<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 < 6 ? "flex-column" : "flex-nowrap h-100" }} {{ __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>someDiv</div>
|
||||
|
||||
@@ -294,6 +294,31 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.containsNone(target, "label.o_form_label_empty:contains(timmy)");
|
||||
});
|
||||
|
||||
QUnit.test("form rendering with class and style attributes", async (assert) => {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: /* xml */ `<form class="myCustomClass" style="border: 1px solid red;"/>`,
|
||||
resId: 2,
|
||||
});
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_view_controller[style*='border: 1px solid red;'], .o_view_controller [style*='border: 1px solid red;']",
|
||||
"style attribute should not be copied"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_view_controller.o_form_view.myCustomClass",
|
||||
"class attribute should be passed to the view controller"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".myCustomClass",
|
||||
"class attribute should ONLY be passed to the view controller"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("generic tags are case insensitive", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
|
||||
@@ -365,6 +365,36 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.containsOnce(target, ".o_kanban_record:contains(gnap)");
|
||||
});
|
||||
|
||||
QUnit.test("kanban rendering with class and style attributes", async (assert) => {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<kanban class="myCustomClass" style="border: 1px solid red;">
|
||||
<templates><t t-name="kanban-box">
|
||||
<field name="foo"/>
|
||||
</t></templates>
|
||||
</kanban>
|
||||
`,
|
||||
});
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_view_controller[style*='border: 1px solid red;'], .o_view_controller [style*='border: 1px solid red;']",
|
||||
"style attribute should not be copied"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_view_controller.o_kanban_view.myCustomClass",
|
||||
"class attribute should be passed to the view controller"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".myCustomClass",
|
||||
"class attribute should ONLY be passed to the view controller"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("generic tags are case insensitive", async function (assert) {
|
||||
await makeView({
|
||||
type: "kanban",
|
||||
|
||||
@@ -438,15 +438,32 @@ QUnit.module("Views", (hooks) => {
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("list with class", async function (assert) {
|
||||
QUnit.test("list with class and style attributes", async function (assert) {
|
||||
await makeView({
|
||||
type: "list",
|
||||
resModel: "foo",
|
||||
serverData,
|
||||
arch: '<tree class="myClass"><field name="foo"/></tree>',
|
||||
arch: /* xml */ `
|
||||
<tree class="myClass" style="border: 1px solid red;">
|
||||
<field name="foo"/>
|
||||
</tree>
|
||||
`,
|
||||
});
|
||||
|
||||
assert.hasClass(target.querySelector(".o_list_renderer"), "myClass");
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_view_controller[style*='border: 1px solid red;'], .o_view_controller [style*='border: 1px solid red;']",
|
||||
"style attribute should not be copied"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_view_controller.o_list_view.myClass",
|
||||
"class attribute should be passed to the view controller"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".myClass",
|
||||
"class attribute should ONLY be passed to the view controller"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('list with create="0"', async function (assert) {
|
||||
@@ -11484,7 +11501,7 @@ QUnit.module("Views", (hooks) => {
|
||||
assert.containsNone(target, ".o_dialog", "should not display an invalid field dialog");
|
||||
assert.strictEqual(target.querySelector(".o_data_row .o_data_cell").innerText, "10");
|
||||
|
||||
// edit again with an invalid value
|
||||
// edit again with an invalid value
|
||||
await click(rows[0].querySelector(".o_data_cell"));
|
||||
target.querySelector(".o_data_row .o_data_cell input").value = "oof2";
|
||||
await triggerEvents(target, ".o_data_row .o_data_cell input", ["input"]);
|
||||
|
||||
@@ -658,7 +658,7 @@ export const stepUtils = {
|
||||
{
|
||||
content: "wait for cancellation to complete",
|
||||
trigger:
|
||||
".o_list_view, .o_form_view > div > div > .o_form_readonly, .o_form_view > div > div > .o_form_saved",
|
||||
".o_view_controller.o_list_view, .o_form_view > div > div > .o_form_readonly, .o_form_view > div > div > .o_form_saved",
|
||||
run() {},
|
||||
auto: true,
|
||||
},
|
||||
|
||||
@@ -112,7 +112,7 @@ class TestResConfig(TransactionCase):
|
||||
arch = self.env['res.config.settings'].get_view(view.id)['arch']
|
||||
tree = etree.fromstring(arch)
|
||||
self.assertTrue(tree.xpath("""
|
||||
//form[@class="oe_form_configuration o_base_settings"]
|
||||
//form[@class="oe_form_configuration"]
|
||||
/app[@name="foo"]
|
||||
"""), 'The res.config.settings form view architecture is not what is expected by the web client.')
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="name">res.config.settings.view.form</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
|
||||
<form string="Settings" class="oe_form_configuration" js_class="base_settings">
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Reference in New Issue
Block a user