diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py index e78a4008f95..72370816acb 100644 --- a/addons/survey/__manifest__.py +++ b/addons/survey/__manifest__.py @@ -88,6 +88,7 @@ sent mails with personal token for the invitation of the survey. 'survey/static/src/scss/survey_reports.scss', ], 'web.assets_backend': [ + 'survey/static/src/question_page/*.js', 'survey/static/src/js/fields_section_one2many.js', 'survey/static/src/js/fields_form_page_description.js', 'survey/static/src/js/survey_renderers.js', @@ -97,7 +98,10 @@ sent mails with personal token for the invitation of the survey. 'survey/static/src/scss/survey_templates_results.scss', ], 'web.assets_tests': [ - 'survey/static/tests/**/*', + 'survey/static/tests/tours/*.js', + ], + 'web.qunit_suite_tests': [ + 'survey/static/tests/components/*.js', ], 'web.assets_common': [ 'survey/static/src/js/tours/survey_tour.js', diff --git a/addons/survey/static/src/question_page/question_page_list_renderer.js b/addons/survey/static/src/question_page/question_page_list_renderer.js new file mode 100644 index 00000000000..93d12ca459b --- /dev/null +++ b/addons/survey/static/src/question_page/question_page_list_renderer.js @@ -0,0 +1,99 @@ +/** @odoo-module */ + +import { makeContext } from "@web/core/context"; +import { ListRenderer } from "@web/views/list/list_renderer"; + +const { useEffect } = owl; + +export class QuestionPageListRenderer extends ListRenderer { + setup() { + super.setup(); + + this.discriminant = "is_page"; + this.fieldsToShow = ["random_questions_count"]; + this.titleField = "title"; + + 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 = []; + + let titleColumnIndex = 0; + for (const col of columns) { + if (col.type !== "field") { + continue; + } + if (col.widget === "handle") { + titleColumnIndex = 1; + sectionColumns.push(col); + continue; + } + if (this.fieldsToShow.includes(col.name) && col.name !== this.titleField) { + sectionColumns.push(col); + } + } + + const colspan = columns.length - sectionColumns.length; + const titleCol = columns.find( + (col) => col.type === "field" && col.name === this.titleField + ); + sectionColumns.splice(titleColumnIndex, 0, { ...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/survey/static/src/question_page/question_page_one2many_field.js b/addons/survey/static/src/question_page/question_page_one2many_field.js new file mode 100644 index 00000000000..11f54bba9dd --- /dev/null +++ b/addons/survey/static/src/question_page/question_page_one2many_field.js @@ -0,0 +1,17 @@ +/** @odoo-module */ + +import { QuestionPageListRenderer } from "./question_page_list_renderer"; +import { registry } from "@web/core/registry"; +import { X2ManyField } from "@web/views/fields/x2many/x2many_field"; + +class QuestionPageOneToManyField extends X2ManyField {} +QuestionPageOneToManyField.components = { + ...X2ManyField.components, + ListRenderer: QuestionPageListRenderer, +}; +QuestionPageOneToManyField.defaultProps = { + ...X2ManyField.defaultProps, + editable: "bottom", +}; + +registry.category("fields").add("question_page_one2many", QuestionPageOneToManyField); diff --git a/addons/survey/static/tests/components/question_page_one2many_field_tests.js b/addons/survey/static/tests/components/question_page_one2many_field_tests.js new file mode 100644 index 00000000000..4de7c300ac2 --- /dev/null +++ b/addons/survey/static/tests/components/question_page_one2many_field_tests.js @@ -0,0 +1,257 @@ +/** @odoo-module */ + +import { + click, + clickEdit, + editInput, + getFixture, + nextTick, + triggerHotkey, +} from "@web/../tests/helpers/utils"; +import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; + +QUnit.module("QuestionPageOneToManyField", (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_page: { type: "boolean" }, + title: { type: "char", string: "Title" }, + random_questions_count: { type: "number", string: "Question Count" }, + }, + records: [ + { + id: 1, + is_page: true, + title: "firstSectionTitle", + random_questions_count: 4, + }, + { + id: 2, + is_page: false, + title: "recordTitle", + random_questions_count: 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); + const rows = target.querySelectorAll(".o_data_row"); + assert.hasClass(rows[0], "o_is_section"); + assert.hasClass(rows[0], "fw-bold"); + assert.strictEqual(rows[0].textContent, "firstSectionTitle4"); + assert.strictEqual(rows[1].textContent, "recordTitle5"); + assert.strictEqual(rows[0].querySelector("td[name=title]").getAttribute("colspan"), "1"); + assert.strictEqual(rows[1].querySelector("td[name=title]").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"); + + 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"); + }); + + QUnit.test( + "press enter with focus in a edited section pass the section in readonly mode", + async (assert) => { + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + serverData, + arch: ` + + `, + }); + await clickEdit(target); + await click(target.querySelector(".o_data_row .o_data_cell")); + assert.containsOnce(target, ".o_selected_row.o_is_section"); + + await editInput(target, "[name='title'] input", "a"); + + triggerHotkey("Enter"); + await nextTick(); + + assert.containsNone(target, ".o_selected_row.o_is_section"); + assert.strictEqual(target.querySelector(".o_is_section [name=title]").innerText, "a"); + } + ); +}); diff --git a/addons/survey/views/survey_survey_views.xml b/addons/survey/views/survey_survey_views.xml index 0461eda42b4..ed9717b6954 100644 --- a/addons/survey/views/survey_survey_views.xml +++ b/addons/survey/views/survey_survey_views.xml @@ -5,7 +5,7 @@