19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -1,91 +0,0 @@
odoo.define('resource.section_backend', function (require) {
// The goal of this file is to contain JS hacks related to allowing
// section on resource calendar.
"use strict";
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
var fieldRegistry = require('web.field_registry');
var ListRenderer = require('web.ListRenderer');
var SectionListRenderer = ListRenderer.extend({
/**
* We want section to take the whole line (except handle and trash)
* to look better and to hide the unnecessary fields.
*
* @override
*/
_renderBodyCell: function (record, node, index, options) {
var $cell = this._super.apply(this, arguments);
var isSection = record.data.display_type === 'line_section';
if (isSection) {
if (node.attrs.widget === "handle") {
return $cell;
} else if (node.attrs.name === "display_name") {
var nbrColumns = this._getNumberOfCols();
if (this.handleField) {
nbrColumns--;
}
if (this.addTrashIcon) {
nbrColumns--;
}
$cell.attr('colspan', nbrColumns);
} else {
return $cell.addClass('o_hidden');
}
}
return $cell;
},
/**
* We add the o_is_{display_type} class to allow custom behaviour both in JS and CSS.
*
* @override
*/
_renderRow: function (record, index) {
var $row = this._super.apply(this, arguments);
if (record.data.display_type) {
$row.addClass('o_is_' + record.data.display_type);
}
return $row;
},
/**
* We want to add .o_section_list_view on the table to have stronger CSS.
*
* @override
* @private
*/
_renderView: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.$('.o_list_table').addClass('o_section_list_view');
// Discard the possibility to remove the sections
self.$('.o_is_line_section .o_list_record_remove').remove()
});
},
});
// We create a custom widget because this is the cleanest way to do it:
// to be sure this custom code will only impact selected fields having the widget
// and not applied to any other existing ListRenderer.
var SectionFieldOne2Many = FieldOne2Many.extend({
/**
* We want to use our custom renderer for the list.
*
* @override
*/
_getRenderer: function () {
if (this.view.arch.tag === 'tree') {
return SectionListRenderer;
}
return this._super.apply(this, arguments);
},
});
fieldRegistry.add('section_one2many', SectionFieldOne2Many);
});

View file

@ -1,8 +1,5 @@
/** @odoo-module */
import { ListRenderer } from "@web/views/list/list_renderer";
const { useEffect } = owl;
import { useEffect } from "@odoo/owl";
export class SectionListRenderer extends ListRenderer {
setup() {

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="resource.SectionListRenderer.RecordRow" t-inherit="web.ListRenderer.RecordRow" owl="1">
<xpath expr="//t[@t-if='displayOptionalFields or hasX2ManyAction']" position="attributes">
<t t-name="resource.SectionListRenderer.RecordRow" t-inherit="web.ListRenderer.RecordRow">
<xpath expr="//t[@t-if='displayOptionalFields or props.list.isGrouped or hasX2ManyAction']" position="attributes">
<attribute name="t-if">(displayOptionalFields or hasX2ManyAction) and !isSection(record)</attribute>
</xpath>
</t>

View file

@ -1,18 +1,20 @@
/** @odoo-module */
import { SectionListRenderer } from "./section_list_renderer";
import { registry } from "@web/core/registry";
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
class SectionOneToManyField extends X2ManyField {}
SectionOneToManyField.components = {
...X2ManyField.components,
ListRenderer: SectionListRenderer,
};
SectionOneToManyField.defaultProps = {
...X2ManyField.defaultProps,
editable: "bottom",
};
class SectionOneToManyField extends X2ManyField {
static components = {
...X2ManyField.components,
ListRenderer: SectionListRenderer,
};
static defaultProps = {
...X2ManyField.defaultProps,
editable: "bottom",
};
}
SectionOneToManyField.additionalClasses = ['o_field_one2many'];
registry.category("fields").add("section_one2many", SectionOneToManyField);
registry.category("fields").add("section_one2many", {
...x2ManyField,
component: SectionOneToManyField,
additionalClasses: [...x2ManyField.additionalClasses || [], "o_field_one2many"],
});

View file

@ -0,0 +1,39 @@
import { useState } from "@odoo/owl";
import { FormController } from "@web/views/form/form_controller";
export class FormControllerWithHTMLExpander extends FormController {
static template = "resource.FormViewWithHtmlExpander";
setup() {
super.setup();
this.htmlExpanderState = useState({ reload: true });
const oldOnNotebookPageChange = this.onNotebookPageChange;
this.onNotebookPageChange = (notebookId, page) => {
oldOnNotebookPageChange(notebookId, page);
if (page && !this.htmlExpanderState.reload) {
this.htmlExpanderState.reload = true;
}
};
}
get modelParams() {
const modelParams = super.modelParams;
const onRootLoaded = modelParams.hooks.onRootLoaded;
modelParams.hooks.onRootLoaded = async () => {
if (onRootLoaded) {
onRootLoaded();
}
this.htmlExpanderState.reload = true;
};
return modelParams;
}
notifyHTMLFieldExpanded() {
this.htmlExpanderState.reload = false;
}
async onRecordSaved(record, changes) {
super.onRecordSaved(record, changes);
this.htmlExpanderState.reload = true;
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="resource.FormViewWithHtmlExpander" t-inherit="web.FormView">
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
<attribute name="reloadHtmlFieldHeight">htmlExpanderState.reload</attribute>
<attribute name="notifyHtmlExpander.bind">notifyHTMLFieldExpanded</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,64 @@
import { useService } from "@web/core/utils/hooks";
import { FormRenderer } from "@web/views/form/form_renderer";
import { useRef, useEffect } from "@odoo/owl";
export class FormRendererWithHtmlExpander extends FormRenderer {
static props = {
...FormRenderer.props,
reloadHtmlFieldHeight: { type: Boolean, optional: true },
notifyHtmlExpander: { type: Function, optional: true },
};
static defaultProps = {
...FormRenderer.defaultProps,
reloadHtmlFieldHeight: true,
notifyHtmlExpander: () => {},
};
setup() {
super.setup();
if (!this.uiService) {
// Should be defined in FormRenderer
this.uiService = useService("ui");
}
const ref = useRef("compiled_view_root");
useEffect(
(el, size) => {
if (el && this._canExpandHTMLField(size)) {
const descriptionField = el.querySelector(this.htmlFieldQuerySelector);
if (descriptionField) {
const containerEL = descriptionField.closest(
this.getHTMLFieldContainerQuerySelector
);
const editor = descriptionField.querySelector(".note-editable");
const elementToResize = editor || descriptionField;
const { top, bottom } = elementToResize.getBoundingClientRect();
const { bottom: containerBottom } = containerEL.getBoundingClientRect();
const { paddingTop, paddingBottom } = window.getComputedStyle(containerEL);
const nonEditableHeight =
containerBottom -
bottom +
parseInt(paddingTop) +
parseInt(paddingBottom);
const minHeight =
document.documentElement.clientHeight - top - nonEditableHeight;
elementToResize.style.minHeight = `${minHeight}px`;
}
}
this.props.notifyHtmlExpander();
},
() => [ref.el, this.uiService.size, this.props.reloadHtmlFieldHeight]
);
}
get htmlFieldQuerySelector() {
return ".o_field_html[name=description]";
}
get getHTMLFieldContainerQuerySelector() {
return ".o_form_sheet";
}
_canExpandHTMLField(size) {
return size === 6;
}
}

View file

@ -0,0 +1,12 @@
import { registry } from "@web/core/registry";
import { formView } from "@web/views/form/form_view";
import { FormRendererWithHtmlExpander } from "./form_renderer_with_html_expander";
import { FormControllerWithHTMLExpander } from "./form_controller_with_html_expander";
export const formViewWithHtmlExpander = {
...formView,
Controller: FormControllerWithHTMLExpander,
Renderer: FormRendererWithHtmlExpander,
};
registry.category("views").add("form_description_expander", formViewWithHtmlExpander);

View file

@ -0,0 +1,66 @@
import { expect, test } from "@odoo/hoot";
import { defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class Partner extends models.Model {
lines = fields.One2many({ relation: "lines_sections" });
_records = [
{
id: 1,
lines: [1, 2],
},
];
}
class LinesSections extends models.Model {
_name = "lines_sections";
display_type = fields.Char();
title = fields.Char();
int = fields.Integer();
_records = [
{
id: 1,
display_type: "line_section",
title: "firstSectionTitle",
int: 4,
},
{
id: 2,
display_type: false,
title: "recordTitle",
int: 5,
},
];
}
defineModels([Partner, LinesSections]);
test("basic rendering", async () => {
await mountView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="lines" widget="section_one2many">
<list>
<field name="display_type" column_invisible="1" />
<field name="title" />
<field name="int" />
</list>
</field>
</form>
`,
});
expect(".o_field_x2many .o_list_renderer table.o_section_list_view").toHaveCount(1);
expect(".o_data_row").toHaveCount(2);
expect(".o_data_row:first").toHaveClass("o_is_line_section fw-bold");
expect(".o_data_row:eq(1)").not.toHaveClass("o_is_line_section fw-bold");
expect(".o_data_row:first").toHaveText("firstSectionTitle");
expect(".o_data_row:eq(1)").toHaveText("recordTitle 5");
expect(".o_data_row:first td[name=title]").toHaveAttribute("colspan", "3");
expect(".o_data_row:eq(1) td[name=title]").not.toHaveAttribute("colspan");
expect(".o_list_record_remove").toHaveCount(1);
expect(".o_is_line_section .o_list_record_remove").toHaveCount(0);
});

View file

@ -1,81 +0,0 @@
/** @odoo-module */
import { getFixture } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
QUnit.module("SectionOneToManyField", (hooks) => {
let serverData;
let target;
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: { lines: { type: "one2many", relation: "lines_sections" } },
records: [
{
id: 1,
lines: [1, 2],
},
],
},
lines_sections: {
fields: {
display_type: { type: "char" },
title: { type: "char", string: "Title" },
int: { type: "number", string: "integer" },
},
records: [
{
id: 1,
display_type: "line_section",
title: "firstSectionTitle",
int: 4,
},
{
id: 2,
display_type: false,
title: "recordTitle",
int: 5,
},
],
},
},
};
setupViewRegistries();
});
QUnit.test("basic rendering", async (assert) => {
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="lines" widget="section_one2many">
<tree>
<field name="display_type" invisible="1" />
<field name="title" />
<field name="int" />
</tree>
</field>
</form>
`,
});
assert.containsOnce(target, ".o_field_x2many .o_list_renderer table.o_section_list_view");
assert.containsN(target, ".o_data_row", 2);
const rows = target.querySelectorAll(".o_data_row");
assert.hasClass(rows[0], "o_is_line_section fw-bold");
assert.doesNotHaveClass(rows[1], "o_is_line_section fw-bold");
assert.strictEqual(rows[0].textContent, "firstSectionTitle");
assert.strictEqual(rows[1].textContent, "recordTitle5");
assert.strictEqual(rows[0].querySelector("td[name=title]").getAttribute("colspan"), "3");
assert.strictEqual(rows[1].querySelector("td[name=title]").getAttribute("colspan"), null);
assert.containsOnce(target, ".o_list_record_remove");
assert.containsNone(target, ".o_is_line_section .o_list_record_remove");
});
});

View file

@ -0,0 +1,5 @@
import { models } from "@web/../tests/web_test_helpers";
export class ResourceResource extends models.ServerModel {
_name = "resource.resource";
}

View file

@ -0,0 +1,10 @@
import { fields, models } from "@web/../tests/web_test_helpers";
export class ResourceTask extends models.Model {
_name = "resource.task";
display_name = fields.Char({ string: "Name" });
resource_ids = fields.Many2many({ string: "Resources", relation: "resource.resource" });
resource_id = fields.Many2one({ string: "Resource", relation: "resource.resource"});
resource_type = fields.Char({ string: "Resource Type" });
}

View file

@ -0,0 +1,12 @@
import { ResourceTask } from "./mock_server/mock_models/resource_task";
import { ResourceResource } from "./mock_server/mock_models/resource_resource";
import { defineModels } from "@web/../tests/web_test_helpers";
export const resourceModels = {
ResourceTask,
ResourceResource,
};
export function defineResourceModels() {
return defineModels(resourceModels);
}