diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index e7642c7f5fc..a0d2d854853 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -71,6 +71,8 @@ Featuring 'website_slides/static/src/models/*.js', ], 'web.assets_backend': [ + 'website_slides/static/src/slide_category_one2many_field.js', + 'website_slides/static/src/slide_category_list_renderer.js', 'website_slides/static/src/scss/slide_views.scss', 'website_slides/static/src/js/slide_category_one2many.js', 'website_slides/static/src/js/rating_field_backend.js', diff --git a/addons/website_slides/static/src/slide_category_list_renderer.js b/addons/website_slides/static/src/slide_category_list_renderer.js new file mode 100644 index 00000000000..7a3ad428396 --- /dev/null +++ b/addons/website_slides/static/src/slide_category_list_renderer.js @@ -0,0 +1,81 @@ +/** @odoo-module */ + +import { makeContext } from "@web/core/context"; +import { ListRenderer } from "@web/views/list/list_renderer"; + +const { useEffect } = owl; + +export class SlideCategoryListRenderer extends ListRenderer { + setup() { + super.setup(); + + this.discriminant = "is_category"; + this.titleField = "name"; + + useEffect( + (table) => { + if (table) { + table.classList.add("o_section_list_view"); + } + }, + () => [this.tableRef.el] + ); + } + + add(params) { + let editable = false; + if (params.context && !this.env.isSmall) { + const evaluatedContext = makeContext([params.context]); + if (evaluatedContext[`default_${this.discriminant}`]) { + editable = this.props.editable; + } + } + super.add({ ...params, editable }); + } + + getColumns(record) { + const columns = super.getColumns(record); + if (this.isSection(record)) { + return this.getSectionColumns(columns); + } + return columns; + } + + getRowClass(record) { + const classNames = super.getRowClass(record).split(" "); + if (this.isSection(record)) { + classNames.push(`o_is_section`, `fw-bold`); + } + return classNames.join(" "); + } + + getSectionColumns(columns) { + const sectionColumns = columns.filter((col) => col.widget === "handle"); + const colspan = columns.length - sectionColumns.length; + const titleCol = columns.find( + (col) => col.type === "field" && col.name === this.titleField + ); + sectionColumns.push({ ...titleCol, colspan }); + return sectionColumns; + } + + isInlineEditable(record) { + return this.isSection(record) && this.props.editable; + } + + isSection(record) { + return record.data[this.discriminant]; + } + + onCellKeydownEditMode(hotkey) { + switch (hotkey) { + case "enter": + case "tab": + case "shift+tab": { + this.props.list.unselectRecord(true); + return true; + } + } + return super.onCellKeydownEditMode(...arguments); + } +} diff --git a/addons/website_slides/static/src/slide_category_one2many_field.js b/addons/website_slides/static/src/slide_category_one2many_field.js new file mode 100644 index 00000000000..9f97839b038 --- /dev/null +++ b/addons/website_slides/static/src/slide_category_one2many_field.js @@ -0,0 +1,17 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { SlideCategoryListRenderer } from "./slide_category_list_renderer"; +import { X2ManyField } from "@web/views/fields/x2many/x2many_field"; + +class SlideCategoryOneToManyField extends X2ManyField {} +SlideCategoryOneToManyField.components = { + ...X2ManyField.components, + ListRenderer: SlideCategoryListRenderer, +}; +SlideCategoryOneToManyField.defaultProps = { + ...X2ManyField.defaultProps, + editable: "bottom", +}; + +registry.category("fields").add("slide_category_one2many", SlideCategoryOneToManyField); diff --git a/addons/website_slides/static/tests/qunit_suite_tests/components/slide_category_one2many_field_tests.js b/addons/website_slides/static/tests/qunit_suite_tests/components/slide_category_one2many_field_tests.js new file mode 100644 index 00000000000..10acf45cb38 --- /dev/null +++ b/addons/website_slides/static/tests/qunit_suite_tests/components/slide_category_one2many_field_tests.js @@ -0,0 +1,218 @@ +/** @odoo-module */ + +import { click, clickEdit, getFixture } from "@web/../tests/helpers/utils"; +import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; + +QUnit.module("SlideCategoryOneToManyField", (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: { + is_category: { type: "boolean" }, + name: { type: "char", string: "Name" }, + int: { type: "number", string: "Integer" }, + }, + records: [ + { + id: 1, + is_category: true, + display_name: "firstSectionName", + name: "firstSectionTitle", + int: 4, + }, + { + id: 2, + is_category: false, + display_name: "recordName", + name: "recordTitle", + int: 5, + }, + ], + }, + }, + views: { + "lines_sections,false,form": ` +
+ + + `, + }, + }; + + setupViewRegistries(); + }); + + QUnit.test("basic rendering", async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + + + + + + + +
+ `, + }); + assert.containsOnce(target, ".o_field_x2many .o_list_renderer table.o_section_list_view"); + assert.containsN(target, ".o_data_row", 2); + assert.hasClass(target.querySelectorAll(".o_data_row")[0], "o_is_section fw-bold"); + const rows = target.querySelectorAll(".o_data_row"); + assert.strictEqual(rows[0].textContent, "firstSectionTitle"); + assert.strictEqual(rows[1].textContent, "recordTitlerecordName5"); + assert.strictEqual(rows[0].querySelector("td[name=name]").getAttribute("colspan"), "3"); + assert.strictEqual(rows[1].querySelector("td[name=name]").getAttribute("colspan"), null); + }); + + QUnit.test("click on section behaves as usual in readonly mode", async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + + + + + + +
+ `, + }); + await click(target.querySelector(".o_data_cell")); + assert.containsNone(target, ".o_selected_row"); + assert.containsOnce(target, ".modal .o_form_view_dialog"); + }); + + QUnit.test("click on section edit the section in place", async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + + + + + + +
`, + }); + await clickEdit(target); + await click(target.querySelector(".o_data_cell")); + assert.hasClass(target.querySelector(".o_is_section"), "o_selected_row"); + assert.containsNone(target, ".modal .o_form_view_dialog"); + }); + + QUnit.test("click on real line opens a dialog", async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + + + + + + +
+ `, + }); + await clickEdit(target); + await click(target.querySelector(".o_data_row:nth-child(2) .o_data_cell")); + assert.containsNone(target, ".o_selected_row"); + assert.containsOnce(target, ".modal .o_form_view_dialog"); + }); + + QUnit.test("can create section inline", async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + + + + + + + + + + +
+ `, + }); + + await clickEdit(target); + assert.containsNone(target, ".o_selected_row.o_is_section"); + + await click(target.querySelectorAll(".o_field_x2many_list_row_add a")[1]); + assert.containsOnce(target, ".o_selected_row.o_is_section"); + assert.containsNone(target, ".modal .o_form_view_dialog"); + }); + + QUnit.test("creates real record in form dialog", async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` +
+ + + + + + + + + + + +
+ `, + }); + + await clickEdit(target); + await click(target.querySelector(".o_field_x2many_list_row_add a")); + assert.containsNone(target, ".o_selected_row"); + assert.containsOnce(target, ".modal .o_form_view_dialog"); + }); +}); diff --git a/addons/website_slides/views/slide_channel_views.xml b/addons/website_slides/views/slide_channel_views.xml index 959b9a381ab..733139928d0 100644 --- a/addons/website_slides/views/slide_channel_views.xml +++ b/addons/website_slides/views/slide_channel_views.xml @@ -6,7 +6,7 @@ slide.channel.view.form slide.channel -
+