diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 4360bb69617..f199b842ac7 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -484,6 +484,7 @@ This module provides the core of the Odoo Web Client. "web/static/lib/ace/javascript_highlight_rules.js", "web/static/lib/ace/mode-xml.js", "web/static/lib/ace/mode-qweb.js", + "web/static/lib/ace/mode-python.js", "web/static/lib/ace/mode-scss.js", "web/static/lib/ace/theme-monokai.js", ], 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 26cf6e4faec..d7d62731dbc 100644 --- a/addons/web/static/src/core/code_editor/code_editor.js +++ b/addons/web/static/src/core/code_editor/code_editor.js @@ -32,6 +32,7 @@ export class CodeEditor extends Component { validate: (mode) => CodeEditor.MODES.includes(mode), }, value: { type: String, optional: true }, + readonly: { type: Boolean, optional: true }, onChange: { type: Function, optional: true }, class: { type: String, optional: true }, theme: { @@ -41,6 +42,7 @@ export class CodeEditor extends Component { }, }; static defaultProps = { + readonly: false, value: "", onChange: () => {}, class: "", @@ -64,9 +66,19 @@ export class CodeEditor extends Component { // keep in closure const aceEditor = window.ace.edit(el); this.aceEditor = aceEditor; - this.aceEditor.session.setUseWorker(false); - this.aceEditor.setValue(this.props.value); + this.aceEditor.setOptions({ + maxLines: Infinity, + showPrintMargin: false, + }); + this.aceEditor.session.setOptions({ + useWorker: false, + tabSize: 2, + useSoftTabs: true, + }); + this.aceEditor.$blockScrolling = true; + + this.aceEditor.setValue(this.props.value); this.aceEditor.session.on("change", () => { if (this.props.onChange) { this.props.onChange(this.aceEditor.getValue()); @@ -95,16 +107,36 @@ export class CodeEditor extends Component { ); useEffect( - (theme) => this.aceEditor.setTheme(theme ? `ace/theme/${theme}` : "textmate"), + (theme) => this.aceEditor.setTheme(theme ? `ace/theme/${theme}` : ""), () => [this.props.theme] ); - this.debouncedResize = useDebounced(() => { + useEffect( + (readonly) => { + this.aceEditor.setOptions({ + readOnly: readonly, + highlightActiveLine: !readonly, + highlightGutterLine: !readonly, + }); + + this.aceEditor.renderer.setOptions({ + displayIndentGuides: !readonly, + showGutter: !readonly, + }); + + this.aceEditor.renderer.$cursorLayer.element.style.display = readonly + ? "none" + : "block"; + }, + () => [this.props.readonly] + ); + + const debouncedResize = useDebounced(() => { if (this.aceEditor) { this.aceEditor.resize(); } }, 250); - onResized(this.editorRef, () => this.debouncedResize()); + onResized(this.editorRef, debouncedResize); } } diff --git a/addons/web/static/src/core/code_editor/code_editor.xml b/addons/web/static/src/core/code_editor/code_editor.xml index 73ff2205470..f02481474b5 100644 --- a/addons/web/static/src/core/code_editor/code_editor.xml +++ b/addons/web/static/src/core/code_editor/code_editor.xml @@ -2,7 +2,7 @@ -
+
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 9fa0cc51c0d..16c33935df5 100644 --- a/addons/web/static/src/views/fields/ace/ace_field.js +++ b/addons/web/static/src/views/fields/ace/ace_field.js @@ -1,14 +1,14 @@ /** @odoo-module **/ -/* global ace */ -import { loadJS } from "@web/core/assets"; import { _lt } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { useBus, useService } from "@web/core/utils/hooks"; import { formatText } from "../formatters"; import { standardFieldProps } from "../standard_field_props"; -import { Component, onWillStart, onWillUpdateProps, useEffect, useRef } from "@odoo/owl"; +import { CodeEditor } from "@web/core/code_editor/code_editor"; +import { Component, onWillUpdateProps, useState } from "@odoo/owl"; +import { useDebounced } from "@web/core/utils/timing"; export class AceField extends Component { static template = "web.AceField"; @@ -19,33 +19,20 @@ export class AceField extends Component { static defaultProps = { mode: "qweb", }; + static components = { CodeEditor }; setup() { - this.aceEditor = null; - this.editorRef = useRef("editor"); this.cookies = useService("cookie"); + this.debouncedCommit = useDebounced(this.commitChanges, 10); - onWillStart(async () => { - await loadJS("/web/static/lib/ace/ace.js"); - const jsLibs = [ - "/web/static/lib/ace/mode-python.js", - "/web/static/lib/ace/mode-xml.js", - "/web/static/lib/ace/mode-qweb.js", - ]; - const proms = jsLibs.map((url) => loadJS(url)); - return Promise.all(proms); + this.state = useState({ + onChange: (value) => this.handleChange(value), + class: "ace-view-editor", + theme: this.theme, }); - onWillUpdateProps(this.updateAce); - - useEffect( - () => { - this.setupAce(); - this.updateAce(this.props); - return () => this.destroyAce(); - }, - () => [this.editorRef.el] - ); + this.updateCodeEditor(this.props); + onWillUpdateProps(this.updateCodeEditor); const { model } = this.props.record; useBus(model.bus, "WILL_SAVE_URGENTLY", () => this.commitChanges()); @@ -54,65 +41,24 @@ export class AceField extends Component { ); } - get aceSession() { - return this.aceEditor.getSession(); + get theme() { + return this.cookies.current.color_scheme === "dark" ? "monokai" : ""; } - setupAce() { - this.aceEditor = ace.edit(this.editorRef.el); - this.aceEditor.setOptions({ - maxLines: Infinity, - showPrintMargin: false, - theme: this.cookies.current.color_scheme === "dark" ? "ace/theme/monokai" : "", - }); - this.aceEditor.$blockScrolling = true; - - this.aceSession.setOptions({ - useWorker: false, - tabSize: 2, - useSoftTabs: true, - }); - - this.aceEditor.on("blur", this.commitChanges.bind(this)); + handleChange(newValue) { + this.state.value = newValue; + this.debouncedCommit(); } - updateAce({ mode, readonly, record }) { - if (!this.aceEditor) { - return; - } - - this.aceSession.setOptions({ - mode: `ace/mode/${mode === "xml" ? "qweb" : mode}`, - }); - - this.aceEditor.setOptions({ - readOnly: readonly, - highlightActiveLine: !readonly, - highlightGutterLine: !readonly, - }); - - this.aceEditor.renderer.setOptions({ - displayIndentGuides: !readonly, - showGutter: !readonly, - }); - - this.aceEditor.renderer.$cursorLayer.element.style.display = readonly ? "none" : "block"; - - const formattedValue = formatText(record.data[this.props.name]); - if (this.aceSession.getValue() !== formattedValue) { - this.aceSession.setValue(formattedValue); - } - } - - destroyAce() { - if (this.aceEditor) { - this.aceEditor.destroy(); - } + updateCodeEditor({ record, mode, readonly }) { + this.state.value = formatText(record.data[this.props.name]); + this.state.mode = mode === "xml" ? "qweb" : mode; + this.state.readonly = readonly; } commitChanges() { if (!this.props.readonly) { - const value = this.aceSession.getValue(); + const value = this.state.value; if (this.props.record.data[this.props.name] !== value) { return this.props.record.update({ [this.props.name]: value }); } @@ -130,3 +76,4 @@ export const aceField = { }; registry.category("fields").add("ace", aceField); +registry.category("fields").add("code", aceField); diff --git a/addons/web/static/src/views/fields/ace/ace_field.scss b/addons/web/static/src/views/fields/ace/ace_field.scss index ac0de4b681d..30d8ec9933c 100644 --- a/addons/web/static/src/views/fields/ace/ace_field.scss +++ b/addons/web/static/src/views/fields/ace/ace_field.scss @@ -1,3 +1,3 @@ -.o_field_ace { +.o_field_ace, .o_field_code { display: block !important; } \ No newline at end of file diff --git a/addons/web/static/src/views/fields/ace/ace_field.xml b/addons/web/static/src/views/fields/ace/ace_field.xml index 1930ce2cf9a..492c9690b25 100644 --- a/addons/web/static/src/views/fields/ace/ace_field.xml +++ b/addons/web/static/src/views/fields/ace/ace_field.xml @@ -4,7 +4,14 @@
-
+
diff --git a/addons/web/static/tests/core/code_editor_tests.js b/addons/web/static/tests/core/code_editor_tests.js index ae54d805594..61f409622c6 100644 --- a/addons/web/static/tests/core/code_editor_tests.js +++ b/addons/web/static/tests/core/code_editor_tests.js @@ -38,7 +38,30 @@ QUnit.module("Web Components", (hooks) => { } async function edit(value) { - await editInput(target, "textarea", value); + const textArea = target.querySelector(".ace_editor textarea"); + await editInput(textArea, null, value); + return null; + } + + function getFakeAceEditor() { + return { + session: { + on: () => {}, + setMode: () => {}, + setUseWorker: () => {}, + setOptions: () => {}, + }, + renderer: { + setOptions: () => {}, + $cursorLayer: { element: { style: {} } }, + }, + setOptions: () => {}, + setValue: () => {}, + getValue: () => "", + setTheme: () => {}, + resize: () => {}, + destroy: () => {}, + }; } QUnit.test("Can be rendered", async (assert) => { @@ -53,12 +76,7 @@ QUnit.module("Web Components", (hooks) => { QUnit.test("onChange props called when code is edited", async (assert) => { class Parent extends Component { static components = { CodeEditor }; - static template = xml` - - `; + static template = xml``; onChange(value) { assert.step(value); } @@ -124,19 +142,9 @@ QUnit.module("Web Components", (hooks) => { }); QUnit.test("Mode props update imports the mode", async (assert) => { - const fakeAceEditor = { - session: { - setMode: (mode) => { - assert.step(mode); - }, - setUseWorker: () => {}, - on: () => {}, - }, - setValue: () => {}, - getValue: () => "", - setTheme: () => {}, - resize: () => {}, - destroy: () => {}, + const fakeAceEditor = getFakeAceEditor(); + fakeAceEditor.session.setMode = (mode) => { + assert.step(mode); }; patchWithCleanup(window.ace, { @@ -163,19 +171,9 @@ QUnit.module("Web Components", (hooks) => { }); QUnit.test("Theme props updates imports the theme", async (assert) => { - const fakeAceEditor = { - session: { - setMode: () => {}, - setUseWorker: () => {}, - on: () => {}, - }, - setValue: () => {}, - getValue: () => "", - setTheme: (theme) => { - assert.step(theme); - }, - resize: () => {}, - destroy: () => {}, + const fakeAceEditor = getFakeAceEditor(); + fakeAceEditor.setTheme = (theme) => { + assert.step(theme ? theme : "default"); }; patchWithCleanup(window.ace, { @@ -184,12 +182,7 @@ QUnit.module("Web Components", (hooks) => { class Parent extends Component { static components = { CodeEditor }; - static template = xml` - - `; + static template = xml``; setup() { this.state = useState({ theme: "" }); } @@ -199,7 +192,7 @@ QUnit.module("Web Components", (hooks) => { } const codeEditor = await mount(Parent, target, { env }); - assert.verifySteps(["textmate"], "Default theme should be loaded"); + assert.verifySteps(["default"], "Default theme should be loaded"); await codeEditor.setTheme("monokai"); await nextTick(); diff --git a/addons/web/static/tests/views/fields/ace_editor_field_tests.js b/addons/web/static/tests/views/fields/ace_editor_field_tests.js index f4c57c6a533..ae054c7587f 100644 --- a/addons/web/static/tests/views/fields/ace_editor_field_tests.js +++ b/addons/web/static/tests/views/fields/ace_editor_field_tests.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; -import { getFixture, triggerEvents } from "@web/../tests/helpers/utils"; +import { getFixture, nextTick, triggerEvents } from "@web/../tests/helpers/utils"; import { pagerNext } from "@web/../tests/search/helpers"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { fakeCookieService } from "@web/../tests/helpers/mock_services"; @@ -46,7 +46,7 @@ QUnit.module("Fields", (hooks) => { serverData, arch: `
- + `, }); @@ -57,7 +57,7 @@ QUnit.module("Fields", (hooks) => { "should have rendered something with ace editor" ); - assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop")); + assert.ok(target.querySelector(".o_field_code").textContent.includes("yop")); }); QUnit.test("AceEditorField doesn't crash when editing", async (assert) => { @@ -69,7 +69,7 @@ QUnit.module("Fields", (hooks) => { arch: `
- + `, }); @@ -86,14 +86,15 @@ QUnit.module("Fields", (hooks) => { serverData, arch: /* xml */ `
- + `, }); - assert.ok(target.querySelector(".o_field_ace").textContent.includes("yop")); + assert.ok(target.querySelector(".o_field_code").textContent.includes("yop")); await pagerNext(target); + await nextTick(); - assert.ok(target.querySelector(".o_field_ace").textContent.includes("blip")); + assert.ok(target.querySelector(".o_field_code").textContent.includes("blip")); }); }); diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml index 0da3a9dfdb7..16f13894f22 100644 --- a/addons/website/views/website_views.xml +++ b/addons/website/views/website_views.xml @@ -37,10 +37,10 @@ diff --git a/odoo/addons/base/views/ir_actions_views.xml b/odoo/addons/base/views/ir_actions_views.xml index a7bf730fab3..427af0b5bd2 100644 --- a/odoo/addons/base/views/ir_actions_views.xml +++ b/odoo/addons/base/views/ir_actions_views.xml @@ -313,7 +313,7 @@ - + - +

How to define a computed field

@@ -319,7 +319,7 @@ - +

How to define a computed field

@@ -672,7 +672,7 @@ - + diff --git a/odoo/addons/base/views/ir_ui_view_views.xml b/odoo/addons/base/views/ir_ui_view_views.xml index 69c01e24f2d..8daaccfedb9 100644 --- a/odoo/addons/base/views/ir_ui_view_views.xml +++ b/odoo/addons/base/views/ir_ui_view_views.xml @@ -29,7 +29,7 @@ - +