[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:
Bruno Boi
2023-04-27 16:10:30 +02:00
parent 232575f684
commit f22961ad07
44 changed files with 159 additions and 81 deletions
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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', '&lt;=', 0), ('sheet_id', '!=', False)]}" data-hotkey="v"/>
<widget name="attach_document" string="Attach Receipt" action="attach_document" attrs="{'invisible': [('attachment_number', '&lt;', 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"/>
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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"
>
+15 -2
View File
@@ -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)
: "";
}
/**
+4 -7
View File
@@ -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="&quot;&quot;" 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 &amp;&amp; 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 &amp;&amp; scope.className || &quot;&quot; }}" 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 &amp;&amp; scope.className || &quot;&quot; }}">
<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 &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_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>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,
},
+1 -1
View File
@@ -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>