[IMP] web: Make AceField use the CodeEditor component
Before this commit: The AceField component used the ace library to provide the user a code editing field After this commit: The AceField component is now using the CodeEditor component and the usage of the ace editor lib is now abstracted behind the component. closes odoo/odoo#117782 Related: odoo/enterprise#39365 Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates>
|
||||
|
||||
<t t-name="web.CodeEditor" owl="1">
|
||||
<div t-ref="editorRef" class="w-100 h-100" t-att-class="props.class">
|
||||
<div t-ref="editorRef" class="w-100" t-att-class="props.class">
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.o_field_ace {
|
||||
.o_field_ace, .o_field_code {
|
||||
display: block !important;
|
||||
}
|
||||
@@ -4,7 +4,14 @@
|
||||
<t t-name="web.AceField" owl="1">
|
||||
<!-- TODO check if some classes are useless -->
|
||||
<div class="o_field_widget oe_form_field o_ace_view_editor oe_ace_open">
|
||||
<div class="ace-view-editor" t-ref="editor" />
|
||||
<CodeEditor
|
||||
value="state.value"
|
||||
mode="state.mode"
|
||||
readonly="state.readonly"
|
||||
onChange.bind="handleChange"
|
||||
class="'ace-view-editor'"
|
||||
theme="theme"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -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`
|
||||
<CodeEditor
|
||||
mode="'xml'"
|
||||
onChange.bind="onChange"
|
||||
/>
|
||||
`;
|
||||
static template = xml`<CodeEditor onChange.bind="onChange" />`;
|
||||
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`
|
||||
<CodeEditor
|
||||
mode="'xml'"
|
||||
theme="state.theme"
|
||||
/>
|
||||
`;
|
||||
static template = xml`<CodeEditor theme="state.theme" />`;
|
||||
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();
|
||||
|
||||
@@ -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: `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
<field name="foo" widget="code" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
@@ -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: `
|
||||
<form>
|
||||
<field name="display_name" />
|
||||
<field name="foo" widget="ace" />
|
||||
<field name="foo" widget="code" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
@@ -86,14 +86,15 @@ QUnit.module("Fields", (hooks) => {
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<field name="foo" widget="ace" />
|
||||
<field name="foo" widget="code" />
|
||||
</form>`,
|
||||
});
|
||||
|
||||
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"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
<notebook>
|
||||
<page string="Custom Code" groups="base.group_no_one">
|
||||
<label for="custom_code_head"/>
|
||||
<field name="custom_code_head" widget="ace" options="{'mode': 'xml'}"/>
|
||||
<field name="custom_code_head" widget="code" options="{'mode': 'xml'}"/>
|
||||
|
||||
<label for="custom_code_footer"/>
|
||||
<field name="custom_code_footer" widget="ace" options="{'mode': 'xml'}"/>
|
||||
<field name="custom_code_footer" widget="code" options="{'mode': 'xml'}"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
||||
@@ -313,7 +313,7 @@
|
||||
<notebook>
|
||||
<page string="Python Code" name='code'
|
||||
attrs="{'invisible': [('state', '!=', 'code')]}">
|
||||
<field name="code" widget="ace" options="{'mode': 'python'}" placeholder="Enter Python code here. Help about Python expression is available in the help tab of this document."/>
|
||||
<field name="code" widget="code" options="{'mode': 'python'}" placeholder="Enter Python code here. Help about Python expression is available in the help tab of this document."/>
|
||||
</page>
|
||||
|
||||
<page string="Data to Write" name="page_object"
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<group string="Advanced Properties" groups="base.group_no_one">
|
||||
<field name="related"/>
|
||||
<field name="depends" attrs="{'required': [('compute', 'not in', [False, ''])]}"/>
|
||||
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
|
||||
<field name="compute" widget="code" options="{'mode': 'python'}"/>
|
||||
</group>
|
||||
<div groups="base.group_no_one">
|
||||
<h3>How to define a computed field</h3>
|
||||
@@ -319,7 +319,7 @@
|
||||
<group string="Advanced Properties" groups="base.group_no_one">
|
||||
<field name="related"/>
|
||||
<field name="depends" attrs="{'required': [('compute', 'not in', [False, ''])]}"/>
|
||||
<field name="compute" widget="ace" options="{'mode': 'python'}"/>
|
||||
<field name="compute" widget="code" options="{'mode': 'python'}"/>
|
||||
</group>
|
||||
<div groups="base.group_no_one">
|
||||
<h3>How to define a computed field</h3>
|
||||
@@ -672,7 +672,7 @@
|
||||
<field name="perm_create"/>
|
||||
<field name="perm_unlink"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<notebook>
|
||||
<page string="Architecture" name="architecture">
|
||||
<field name="arch_db" class="oe_no_translation_content"/>
|
||||
<field name="arch_base" string="View Architecture" widget="ace" options="{'mode': 'xml'}" required="1"/>
|
||||
<field name="arch_base" string="View Architecture" widget="code" options="{'mode': 'xml'}" required="1"/>
|
||||
</page>
|
||||
<page string="Access Rights" name="access_rights">
|
||||
<field name="groups_id"/>
|
||||
|
||||
Reference in New Issue
Block a user