diff --git a/addons/web/static/src/core/code_editor/code_editor.js b/addons/web/static/src/core/code_editor/code_editor.js
index 335350ca031..5d50d1c6da6 100644
--- a/addons/web/static/src/core/code_editor/code_editor.js
+++ b/addons/web/static/src/core/code_editor/code_editor.js
@@ -31,7 +31,7 @@ export class CodeEditor extends Component {
optional: true,
validate: (mode) => CodeEditor.MODES.includes(mode),
},
- value: { type: String, optional: true },
+ value: { validate: (v) => typeof v === "string", optional: true },
readonly: { type: Boolean, optional: true },
onChange: { type: Function, optional: true },
onBlur: { type: Function, optional: true },
diff --git a/addons/web/static/src/views/fields/ace/ace_field.js b/addons/web/static/src/views/fields/ace/ace_field.js
index 846d50f9b97..731cea07a12 100644
--- a/addons/web/static/src/views/fields/ace/ace_field.js
+++ b/addons/web/static/src/views/fields/ace/ace_field.js
@@ -8,6 +8,7 @@ import { standardFieldProps } from "../standard_field_props";
import { CodeEditor } from "@web/core/code_editor/code_editor";
import { Component, useState } from "@odoo/owl";
import { useRecordObserver } from "@web/model/relational_model/utils";
+import { formatText } from "@web/views/fields/formatters";
export class AceField extends Component {
static template = "web.AceField";
@@ -25,7 +26,7 @@ export class AceField extends Component {
this.state = useState({});
useRecordObserver((record) => {
- this.state.initialValue = record.data[this.props.name];
+ this.state.initialValue = formatText(record.data[this.props.name]);
});
this.isDirty = false;
diff --git a/addons/web/static/src/views/fields/formatters.js b/addons/web/static/src/views/fields/formatters.js
index 05d391a9a82..e82e351b4b0 100644
--- a/addons/web/static/src/views/fields/formatters.js
+++ b/addons/web/static/src/views/fields/formatters.js
@@ -483,6 +483,27 @@ export function formatSelection(value, options = {}) {
return option ? option[1] : "";
}
+/**
+ * Returns the value or an empty string if it's falsy.
+ *
+ * @param {string | false} value
+ * @returns {string}
+ */
+export function formatText(value) {
+ return value ? value.toString() : "";
+}
+
+/**
+ * Returns the value.
+ * Note that, this function is added to be coherent with the rest of the formatters.
+ *
+ * @param {html} value
+ * @returns {html}
+ */
+export function formatHtml(value) {
+ return value;
+}
+
export function formatJson(value) {
return (value && JSON.stringify(value)) || "";
}
@@ -497,7 +518,7 @@ registry
.add("float", formatFloat)
.add("float_factor", formatFloatFactor)
.add("float_time", formatFloatTime)
- .add("html", (value) => value)
+ .add("html", formatHtml)
.add("integer", formatInteger)
.add("json", formatJson)
.add("many2one", formatMany2one)
@@ -510,4 +531,4 @@ registry
.add("properties_definition", formatProperties)
.add("reference", formatReference)
.add("selection", formatSelection)
- .add("text", (value) => value);
+ .add("text", formatText);
diff --git a/addons/web/static/tests/core/code_editor_tests.js b/addons/web/static/tests/core/code_editor_tests.js
index 0888984868c..5cb00ce0422 100644
--- a/addons/web/static/tests/core/code_editor_tests.js
+++ b/addons/web/static/tests/core/code_editor_tests.js
@@ -10,8 +10,11 @@ import {
editInput,
} from "@web/../tests/helpers/utils";
-import { Component, useState, xml } from "@odoo/owl";
+import { Component, markup, useState, xml } from "@odoo/owl";
import { CodeEditor } from "@web/core/code_editor/code_editor";
+import { registerCleanup } from "@web/../tests/helpers/cleanup";
+import { registry } from "@web/core/registry";
+import { errorService } from "@web/core/errors/error_service";
QUnit.module("Web Components", (hooks) => {
QUnit.module("Code Editor");
@@ -80,6 +83,50 @@ QUnit.module("Web Components", (hooks) => {
assert.containsOnce(target, ".ace_editor", "Code editor is rendered");
});
+ QUnit.test("CodeEditor shouldn't accepts markup values", async (assert) => {
+ const _console = window.console;
+ window.console = Object.assign(Object.create(_console), {
+ warn(msg) {
+ assert.step(msg);
+ },
+ });
+ registerCleanup(() => {
+ window.console = _console;
+ });
+ registry.category("services").add("error", errorService);
+ const handler = (ev) => {
+ assert.step(ev.reason.message);
+ // need to preventDefault to remove error from console (so python test pass)
+ ev.preventDefault();
+ };
+ window.addEventListener("unhandledrejection", handler);
+ registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
+
+ patchWithCleanup(QUnit, {
+ onUnhandledRejection: () => {},
+ });
+ class Parent extends Component {
+ static components = { CodeEditor };
+ static template = xml`
My little HTML Test
", + }); + serverData.models.partner.onchanges = { htmlField: function () {} }; + await makeView({ + type: "form", + resModel: "partner", + resId: 3, + serverData, + arch: ` + `, + mockRPC(route, args) { + if (args.method) { + assert.step(args.method); + if (args.method === "write") { + assert.deepEqual(args.args[1], { foo: "DEF" }); + } + if (args.method === "onchange") { + throw new Error("Should not call onchange, htmlField wasn't changed"); + } + } + }, + }); + + assert.ok("ace" in window, "the ace library should be loaded"); + assert.containsOnce( + target, + "div.ace_content", + "should have rendered something with ace editor" + ); + + assert.ok( + target.querySelector(".o_field_code").textContent.includes("My little HTML Test") + ); + + // Modify foo and save + await editInput(target, ".o_field_widget[name=foo] textarea", "DEF"); + await clickSave(target); + + assert.verifySteps(["get_views", "web_read", "write", "web_read"]); + }); + QUnit.test("AceEditorField doesn't crash when editing", async (assert) => { await makeView({ type: "form", diff --git a/addons/web/static/tests/views/fields/formatters_tests.js b/addons/web/static/tests/views/fields/formatters_tests.js index 97fcfc5a87e..43a3e27ac25 100644 --- a/addons/web/static/tests/views/fields/formatters_tests.js +++ b/addons/web/static/tests/views/fields/formatters_tests.js @@ -1,5 +1,6 @@ /** @odoo-module **/ +import { markup } from "@odoo/owl"; import { defaultLocalization } from "@web/../tests/helpers/mock_services"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; import { currencies } from "@web/core/currency"; @@ -15,6 +16,7 @@ import { formatMonetary, formatPercentage, formatReference, + formatText, formatX2many, } from "@web/views/fields/formatters"; @@ -239,6 +241,16 @@ QUnit.module("Fields", (hooks) => { assert.strictEqual(formatMany2one([1, "M2O value"], { escape: true }), "M2O%20value"); }); + QUnit.test("formatText", function (assert) { + assert.strictEqual(formatText(false), ""); + assert.strictEqual(formatText("value"), "value"); + assert.strictEqual(formatText(1), "1"); + assert.strictEqual(formatText(1.5), "1.5"); + assert.strictEqual(formatText(markup("This is a Test
")), "This is a Test
"); + assert.strictEqual(formatText([1, 2, 3, 4, 5]), "1,2,3,4,5"); + assert.strictEqual(formatText({ a: 1, b: 2 }), "[object Object]"); + }); + QUnit.test("formatX2many", function (assert) { // Results are cast as strings since they're lazy translated. assert.strictEqual(String(formatX2many({ currentIds: [] })), "No records");