-
+
+
diff --git a/addons/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js b/addons/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js
index a8f1aa5583c..1f3096f52ba 100644
--- a/addons/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js
+++ b/addons/web/static/src/views/fields/copy_clipboard/copy_clipboard_field.js
@@ -1,8 +1,8 @@
/** @odoo-module **/
+import { evalDomain } from "@web/core/domain";
import { _lt } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
-import { evalDomain } from "@web/views/utils";
import { registry } from "@web/core/registry";
import { omit } from "@web/core/utils/objects";
diff --git a/addons/web/static/src/views/fields/field.js b/addons/web/static/src/views/fields/field.js
index 9e3ce5d3932..b48aac5020d 100644
--- a/addons/web/static/src/views/fields/field.js
+++ b/addons/web/static/src/views/fields/field.js
@@ -1,19 +1,17 @@
/** @odoo-module **/
-import { makeContext } from "@web/core/context";
-import { Domain } from "@web/core/domain";
+import { Domain, evalDomain } from "@web/core/domain";
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
-import {
- archParseBoolean,
- evalDomain,
- getClassNameFromDecoration,
- X2M_TYPES,
-} from "@web/views/utils";
+import { utils } from "@web/core/ui/ui_service";
+import { getFieldContext } from "@web/model/relational_model/utils";
+import { archParseBoolean, getClassNameFromDecoration, X2M_TYPES } from "@web/views/utils";
import { getTooltipInfo } from "./field_tooltip";
import { Component, xml } from "@odoo/owl";
+const isSmall = utils.isSmall;
+
const viewRegistry = registry.category("views");
const fieldRegistry = registry.category("fields");
@@ -55,11 +53,63 @@ export function fieldVisualFeedback(field, record, fieldName, fieldInfo) {
return {
readonly,
required: evalDomain(modifiers.required, record.evalContext),
- invalid: record.isInvalid(fieldName),
+ invalid: record.isFieldInvalid(fieldName),
empty,
};
}
+export function getPropertyFieldInfo(propertyField) {
+ const { name, relatedPropertyField, string, type } = propertyField;
+
+ const fieldInfo = {
+ name,
+ string,
+ type,
+ widget: type,
+ options: {},
+ modifiers: {},
+ attrs: {},
+ relatedPropertyField,
+
+ // ??? We don t use it ? But it s in the fieldInfo of the field
+ context: "{}",
+ help: undefined,
+ onChange: false,
+ forceSave: false,
+ alwaysInvisible: false,
+ decorations: {},
+ // ???
+ };
+
+ if (type === "many2one" || type === "many2many") {
+ const { domain, relation } = propertyField;
+ fieldInfo.relation = relation;
+ fieldInfo.domain = domain;
+
+ if (relation === "res.users" || relation === "res.partner") {
+ fieldInfo.widget =
+ propertyField.type === "many2one" ? "many2one_avatar" : "many2many_tags_avatar";
+ } else {
+ fieldInfo.widget = propertyField.type === "many2one" ? type : "many2many_tags";
+ }
+ } else if (type === "tags") {
+ fieldInfo.tags = propertyField.tags;
+ fieldInfo.widget = `property_tags`;
+ } else if (type === "selection") {
+ fieldInfo.selection = propertyField.selection;
+ }
+
+ fieldInfo.field = getFieldFromRegistry(propertyField.type, fieldInfo.widget);
+ let { relatedFields } = fieldInfo.field;
+ if (relatedFields) {
+ if (relatedFields instanceof Function) {
+ relatedFields = relatedFields({ options: {}, attrs: {} });
+ }
+ fieldInfo.relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));
+ }
+
+ return fieldInfo;
+}
export class Field extends Component {
setup() {
if (this.props.fieldInfo) {
@@ -134,21 +184,7 @@ export class Field extends Component {
}
const dynamicInfo = {
get context() {
- const evalContext = record.getEvalContext
- ? record.getEvalContext(false)
- : record.evalContext;
-
- const context = {};
- for (const key in record.context) {
- if (!key.startsWith("default_") && !key.endsWith("_view_ref")) {
- context[key] = record.context[key];
- }
- }
-
- return {
- ...context,
- ...makeContext([fieldInfo.context], evalContext),
- };
+ return getFieldContext(record, fieldInfo.name, fieldInfo.context);
},
domain() {
const evalContext = record.getEvalContext
@@ -256,43 +292,59 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
fieldInfo.attrs[name] = value;
}
}
+ if (name === "id") {
+ fieldInfo.modifiers.readonly = true;
+ }
+
+ if (widget === "handle") {
+ fieldInfo.isHandle = true;
+ }
if (X2M_TYPES.includes(fields[name].type)) {
const views = {};
- for (const child of node.children) {
- const viewType = child.tagName === "tree" ? "list" : child.tagName;
- const { ArchParser } = viewRegistry.get(viewType);
- const xmlSerializer = new XMLSerializer();
- const subArch = xmlSerializer.serializeToString(child);
- const archInfo = new ArchParser().parse(subArch, models, fields[name].relation);
- views[viewType] = {
- ...archInfo,
- fields: models[fields[name].relation],
- };
- fieldInfo.relatedFields = models[fields[name].relation];
- }
-
- let viewMode = node.getAttribute("mode");
- if (!viewMode) {
- if (views.list && !views.kanban) {
- viewMode = "list";
- } else if (!views.list && views.kanban) {
- viewMode = "kanban";
- } else if (views.list && views.kanban) {
- viewMode = "list,kanban";
- }
- } else {
- viewMode = viewMode.replace("tree", "list");
- }
- fieldInfo.viewMode = viewMode;
- fieldInfo.views = views;
-
- let relatedFields = field.relatedFields;
+ let relatedFields = fieldInfo.field.relatedFields;
if (relatedFields) {
if (relatedFields instanceof Function) {
relatedFields = relatedFields(fieldInfo);
}
- fieldInfo.relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));
+ relatedFields = Object.fromEntries(relatedFields.map((f) => [f.name, f]));
+ views.default = { fieldNodes: relatedFields, fields: relatedFields };
+ fieldInfo.viewMode = "default";
+ } else {
+ for (const child of node.children) {
+ const viewType = child.tagName === "tree" ? "list" : child.tagName;
+ const { ArchParser } = viewRegistry.get(viewType);
+ const xmlSerializer = new XMLSerializer();
+ const subArch = xmlSerializer.serializeToString(child);
+ const archInfo = new ArchParser().parse(subArch, models, fields[name].relation);
+ views[viewType] = {
+ ...archInfo,
+ limit: archInfo.limit || 40,
+ fields: models[fields[name].relation],
+ };
+ }
+
+ let viewMode = node.getAttribute("mode");
+ if (!viewMode) {
+ if (views.list && !views.kanban) {
+ viewMode = "list";
+ } else if (!views.list && views.kanban) {
+ viewMode = "kanban";
+ } else if (views.list && views.kanban) {
+ viewMode = isSmall() ? "kanban" : "list";
+ }
+ } else {
+ if (viewMode.split(",").length !== 1) {
+ viewMode = isSmall() ? "kanban" : "list";
+ } else {
+ viewMode = viewMode === "tree" ? "list" : viewMode;
+ }
+ }
+ fieldInfo.viewMode = viewMode;
+ }
+ if (Object.keys(views).length) {
+ fieldInfo.relatedFields = models[fields[name].relation];
+ fieldInfo.views = views;
}
}
diff --git a/addons/web/static/src/views/fields/font_selection/font_selection_field.js b/addons/web/static/src/views/fields/font_selection/font_selection_field.js
index 0ac405a6997..f9a01d6b21d 100644
--- a/addons/web/static/src/views/fields/font_selection/font_selection_field.js
+++ b/addons/web/static/src/views/fields/font_selection/font_selection_field.js
@@ -49,7 +49,6 @@ export const fontSelectionField = {
required: dynamicInfo.required,
};
},
- legacySpecialData: "_fetchSpecialRelation",
};
registry.category("fields").add("font", fontSelectionField);
diff --git a/addons/web/static/src/views/fields/formatters.js b/addons/web/static/src/views/fields/formatters.js
index 197fe8fb485..05d391a9a82 100644
--- a/addons/web/static/src/views/fields/formatters.js
+++ b/addons/web/static/src/views/fields/formatters.js
@@ -133,10 +133,7 @@ export function formatBoolean(value) {
}
/**
- * Returns a string representing a char. If the value is false, then we return
- * an empty string.
- *
- * @param {string|false} value
+ * @param {string} value
* @param {Object} [options] additional options
* @param {boolean} [options.escape=false] if true, escapes the formatted value
* @param {boolean} [options.isPassword=false] if true, returns '********'
@@ -144,7 +141,6 @@ export function formatBoolean(value) {
* @returns {string}
*/
export function formatChar(value, options) {
- value = typeof value === "string" ? value : "";
if (options && options.isPassword) {
return "*".repeat(value ? value.length : 0);
}
@@ -487,16 +483,6 @@ 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 || "";
-}
-
export function formatJson(value) {
return (value && JSON.stringify(value)) || "";
}
@@ -524,4 +510,4 @@ registry
.add("properties_definition", formatProperties)
.add("reference", formatReference)
.add("selection", formatSelection)
- .add("text", formatText);
+ .add("text", (value) => value);
diff --git a/addons/web/static/src/views/fields/image/image_field.js b/addons/web/static/src/views/fields/image/image_field.js
index 16a42a37e55..13067aeaa7b 100644
--- a/addons/web/static/src/views/fields/image/image_field.js
+++ b/addons/web/static/src/views/fields/image/image_field.js
@@ -9,7 +9,7 @@ import { isBinarySize } from "@web/core/utils/binary";
import { FileUploader } from "../file_handler";
import { standardFieldProps } from "../standard_field_props";
-import { Component, useState, onWillUpdateProps } from "@odoo/owl";
+import { Component, useState } from "@odoo/owl";
const { DateTime } = luxon;
export const fileTypeMagicWordMap = {
@@ -49,6 +49,7 @@ export class ImageField extends Component {
acceptedFileExtensions: { type: String, optional: true },
width: { type: Number, optional: true },
height: { type: Number, optional: true },
+ noReload: { type: Boolean, optional: true },
};
static defaultProps = {
acceptedFileExtensions: "image/*",
@@ -61,15 +62,11 @@ export class ImageField extends Component {
this.state = useState({
isValid: true,
});
+ this.lastURL = undefined;
+ }
- this.rawCacheKey = this.props.record.data.write_date;
- onWillUpdateProps((nextProps) => {
- const { record } = this.props;
- const { record: nextRecord } = nextProps;
- if (record.resId !== nextRecord.resId || nextRecord.mode === "readonly") {
- this.rawCacheKey = nextRecord.data.write_date;
- }
- });
+ get rawCacheKey() {
+ return this.props.record.data.write_date;
}
get sizeStyle() {
@@ -101,12 +98,12 @@ export class ImageField extends Component {
}
getUrl(previewFieldName) {
+ if (this.props.noReload && this.lastURL) {
+ return this.lastURL;
+ }
if (this.state.isValid && this.props.record.data[this.props.name]) {
if (isBinarySize(this.props.record.data[this.props.name])) {
- if (!this.rawCacheKey) {
- this.rawCacheKey = this.props.record.data.write_date;
- }
- return url("/web/image", {
+ this.lastURL = url("/web/image", {
model: this.props.record.resModel,
id: this.props.record.resId,
field: previewFieldName,
@@ -116,8 +113,11 @@ export class ImageField extends Component {
// Use magic-word technique for detecting image type
const magic =
fileTypeMagicWordMap[this.props.record.data[this.props.name][0]] || "png";
- return `data:image/${magic};base64,${this.props.record.data[this.props.name]}`;
+ this.lastURL = `data:image/${magic};base64,${
+ this.props.record.data[this.props.name]
+ }`;
}
+ return this.lastURL;
}
return placeholder;
}
@@ -127,8 +127,6 @@ export class ImageField extends Component {
}
async onFileUploaded(info) {
this.state.isValid = true;
- // Invalidate the `rawCacheKey`.
- this.rawCacheKey = null;
if (info.type === 'image/webp') {
// Generate alternate sizes and format for reports.
const image = document.createElement('img');
@@ -230,6 +228,7 @@ export const imageField = {
acceptedFileExtensions: options.accepted_file_extensions,
width: options.size && Boolean(options.size[0]) ? options.size[0] : attrs.width,
height: options.size && Boolean(options.size[1]) ? options.size[1] : attrs.height,
+ noReload: Boolean(options.no_reload),
}),
};
diff --git a/addons/web/static/src/views/fields/input_field_hook.js b/addons/web/static/src/views/fields/input_field_hook.js
index 0660f24a3d8..b44054d60e7 100644
--- a/addons/web/static/src/views/fields/input_field_hook.js
+++ b/addons/web/static/src/views/fields/input_field_hook.js
@@ -113,7 +113,11 @@ export function useInputField(params) {
* If it is not such a case, we update the field with the new value.
*/
useEffect(() => {
- if (inputRef.el && !isDirty && !component.props.record.isInvalid(component.props.name)) {
+ if (
+ inputRef.el &&
+ !isDirty &&
+ !component.props.record.isFieldInvalid(component.props.name)
+ ) {
inputRef.el.value = params.getValue();
lastSetValue = inputRef.el.value;
}
@@ -154,10 +158,9 @@ export function useInputField(params) {
}
if ((val || false) !== (component.props.record.data[component.props.name] || false)) {
- const nextValue = inputRef.el.value;
+ lastSetValue = inputRef.el.value;
await component.props.record.update({ [component.props.name]: val });
component.props.record.model.bus.trigger("FIELD_IS_DIRTY", false);
- lastSetValue = nextValue;
}
}
}
diff --git a/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.js b/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.js
index 9c834abe4e8..51732d65a84 100644
--- a/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.js
+++ b/addons/web/static/src/views/fields/many2many_binary/many2many_binary_field.js
@@ -30,7 +30,12 @@ export class Many2ManyBinaryField extends Component {
return this.props.record.fields[this.props.name].string;
}
get files() {
- return this.props.record.data[this.props.name].records.map((record) => record.data);
+ return this.props.record.data[this.props.name].records.map((record) => {
+ return {
+ ...record.data,
+ id: record.resId,
+ };
+ });
}
getUrl(id) {
@@ -55,7 +60,7 @@ export class Many2ManyBinaryField extends Component {
async onFileRemove(deleteId) {
const record = this.props.record.data[this.props.name].records.find(
- (record) => record.data.id === deleteId
+ (record) => record.resId === deleteId
);
this.operations.removeRecord(record);
}
diff --git a/addons/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js b/addons/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js
index e65bedfed48..cf7170e4bd7 100644
--- a/addons/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js
+++ b/addons/web/static/src/views/fields/many2many_checkboxes/many2many_checkboxes_field.js
@@ -1,41 +1,76 @@
/** @odoo-module **/
+import { Component, onWillUnmount } from "@odoo/owl";
import { CheckBox } from "@web/core/checkbox/checkbox";
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
+import { useBus } from "@web/core/utils/hooks";
+import { debounce } from "@web/core/utils/timing";
+import { useSpecialData } from "@web/views/fields/relational_utils";
import { standardFieldProps } from "../standard_field_props";
-import { Component } from "@odoo/owl";
-
export class Many2ManyCheckboxesField extends Component {
static template = "web.Many2ManyCheckboxesField";
static components = { CheckBox };
static props = {
...standardFieldProps,
+ domain: { type: Array, optional: true },
};
+ setup() {
+ this.specialData = useSpecialData((orm, props) => {
+ const { relation } = props.record.fields[props.name];
+ return orm.call(relation, "name_search", ["", props.domain]);
+ });
+ // these two sets track pending changes in the relation, and allow us to
+ // batch consecutive changes into a single replaceWith, thus saving
+ // unnecessary potential intermediate onchanges
+ this.idsToAdd = new Set();
+ this.idsToRemove = new Set();
+ this.debouncedCommitChanges = debounce(this.commitChanges.bind(this), 500);
+ useBus(this.props.record.model.bus, "NEED_LOCAL_CHANGES", this.commitChanges.bind(this));
+ onWillUnmount(this.commitChanges.bind(this));
+ }
+
get items() {
- return this.props.record.preloadedData[this.props.name];
+ return this.specialData.data;
}
isSelected(item) {
return this.props.record.data[this.props.name].currentIds.includes(item[0]);
}
+ commitChanges() {
+ if (this.idsToAdd.size === 0 && this.idsToRemove.size === 0) {
+ return;
+ }
+ const ids = new Set(this.props.record.data[this.props.name].currentIds);
+ for (const el of this.idsToAdd) {
+ ids.add(el);
+ }
+ for (const el of this.idsToRemove) {
+ ids.delete(el);
+ }
+ this.idsToAdd.clear();
+ this.idsToRemove.clear();
+ return this.props.record.data[this.props.name].replaceWith([...ids]);
+ }
+
onChange(resId, checked) {
if (checked) {
- this.props.record.data[this.props.name].replaceWith([
- ...this.props.record.data[this.props.name].currentIds,
- resId,
- ]);
- } else {
- const currentIds = [...this.props.record.data[this.props.name].currentIds];
- const index = currentIds.indexOf(resId);
- if (index > -1) {
- currentIds.splice(index, 1);
+ if (this.idsToRemove.has(resId)) {
+ this.idsToRemove.delete(resId);
+ } else {
+ this.idsToAdd.add(resId);
+ }
+ } else {
+ if (this.idsToAdd.has(resId)) {
+ this.idsToAdd.delete(resId);
+ } else {
+ this.idsToRemove.add(resId);
}
- this.props.record.data[this.props.name].replaceWith(currentIds);
}
+ this.debouncedCommitChanges();
}
}
@@ -44,7 +79,11 @@ export const many2ManyCheckboxesField = {
displayName: _lt("Checkboxes"),
supportedTypes: ["many2many"],
isEmpty: () => false,
- legacySpecialData: "_fetchSpecialRelation",
+ extractProps(fieldInfo, dynamicInfo) {
+ return {
+ domain: dynamicInfo.domain(),
+ };
+ },
};
registry.category("fields").add("many2many_checkboxes", many2ManyCheckboxesField);
@@ -53,8 +92,3 @@ export function preloadMany2ManyCheckboxes(orm, record, fieldName, { domain }) {
const field = record.fields[fieldName];
return orm.call(field.relation, "name_search", ["", domain]);
}
-
-registry.category("preloadedData").add("many2many_checkboxes", {
- loadOnTypes: ["many2many"],
- preload: preloadMany2ManyCheckboxes,
-});
diff --git a/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js b/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js
index 349fb4cb0e9..045fa4ebffd 100644
--- a/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js
+++ b/addons/web/static/src/views/fields/many2many_tags/many2many_tags_field.js
@@ -271,7 +271,6 @@ export const many2ManyTagsField = {
},
],
supportedTypes: ["many2many"],
- isSet: (value) => value.count > 0,
relatedFields: ({ options }) => {
const relatedFields = [{ name: "display_name", type: "char" }];
if (options.color_field) {
diff --git a/addons/web/static/src/views/fields/many2one/many2one_field.js b/addons/web/static/src/views/fields/many2one/many2one_field.js
index d737d8a9464..c720ac13c04 100644
--- a/addons/web/static/src/views/fields/many2one/many2one_field.js
+++ b/addons/web/static/src/views/fields/many2one/many2one_field.js
@@ -106,11 +106,9 @@ export class Many2OneField extends Component {
const resId = this.value[0];
const fields = ["display_name"];
const { context } = this.props;
+ // use unity read + relatedFields from Field Component
const records = await this.orm.read(this.relation, [resId], fields, { context });
await this.updateRecord(m2oTupleFromData(records[0]));
- if (this.props.record.model.root.id !== this.props.record.id) {
- this.props.record.switchMode("readonly");
- }
},
onClose: () => this.focusInput(),
fieldString: this.string,
diff --git a/addons/web/static/src/views/fields/progress_bar/progress_bar_field.js b/addons/web/static/src/views/fields/progress_bar/progress_bar_field.js
index ca7c78aba27..711896b19d2 100644
--- a/addons/web/static/src/views/fields/progress_bar/progress_bar_field.js
+++ b/addons/web/static/src/views/fields/progress_bar/progress_bar_field.js
@@ -6,9 +6,8 @@ import { useNumpadDecimal } from "../numpad_decimal_hook";
import { parseFloat } from "../parsers";
import { standardFieldProps } from "../standard_field_props";
-import { Component, onWillUpdateProps, useRef, useState } from "@odoo/owl";
+import { Component, useRef, useState } from "@odoo/owl";
const formatters = registry.category("formatters");
-const parsers = registry.category("parsers");
export class ProgressBarField extends Component {
static template = "web.ProgressBarField";
@@ -28,76 +27,66 @@ export class ProgressBarField extends Component {
this.root = useRef("numpadDecimal");
this.maxValueRef = useRef("maxValue");
this.currentValueRef = useRef("currentValue");
+
+ const { currentValueField, maxValueField, name } = this.props;
+ this.currentValueField = currentValueField ? currentValueField : name;
+ if (maxValueField) {
+ this.maxValueField = maxValueField;
+ }
+
this.state = useState({
- currentValue: this.getCurrentValue(this.props),
- maxValue: this.getMaxValue(this.props),
isEditing: false,
});
- onWillUpdateProps((nextProps) => {
- Object.assign(this.state, {
- currentValue: this.getCurrentValue(nextProps),
- maxValue: this.getMaxValue(nextProps),
- });
- });
}
- get isCurrentValueInteger() {
- return this.state.currentValue % 1 === 0;
- }
get isEditable() {
return this.props.isEditable && !this.props.readonly;
}
- get isMaxValueInteger() {
- return this.state.maxValue % 1 === 0;
- }
get isPercentage() {
return !this.props.maxValueField || !isNaN(this.props.maxValueField);
}
- getCurrentValueField(p) {
- return typeof p.currentValueField === "string" ? p.currentValueField : p.name;
- }
- getMaxValueField(p) {
- return typeof p.maxValueField === "string" ? p.maxValueField : p.name;
+ get currentValue() {
+ return this.props.record.data[this.currentValueField] || 0;
}
- getCurrentValue(p) {
- return p.record.data[this.getCurrentValueField(p)] || 0;
- }
- getMaxValue(p) {
- if (p.maxValueField) {
- return p.record.data[p.maxValueField] || 100;
- }
- return 100;
+ get maxValue() {
+ return this.props.record.data[this.maxValueField] || 100;
}
formatCurrentValue(humanReadable = !this.state.isEditing) {
- const formatter = formatters.get(this.isCurrentValueInteger ? "integer" : "float");
- return formatter(this.state.currentValue, { humanReadable });
+ const formatter = formatters.get(Number.isInteger(this.currentValue) ? "integer" : "float");
+ return formatter(this.currentValue, { humanReadable });
}
formatMaxValue(humanReadable = !this.state.isEditing) {
- const formatter = formatters.get(this.isMaxValueInteger ? "integer" : "float");
- return formatter(this.state.maxValue, { humanReadable });
+ const formatter = formatters.get(Number.isInteger(this.maxValue) ? "integer" : "float");
+ return formatter(this.maxValue, { humanReadable });
}
- onCurrentValueChange(ev) {
+ onValueChange(value, fieldName) {
let parsedValue;
try {
- parsedValue = parseFloat(ev.target.value);
+ parsedValue = parseFloat(value);
} catch {
this.props.record.setInvalidField(this.props.name);
return;
}
- if (this.isCurrentValueInteger) {
+ if (this.props.record.fields[fieldName].type === "integer") {
parsedValue = Math.floor(parsedValue);
}
- this.state.currentValue = parsedValue;
- this.props.record.update({ [this.getCurrentValueField(this.props)]: parsedValue });
+ this.props.record.update({ [fieldName]: parsedValue });
if (this.props.readonly) {
this.props.record.save();
}
}
+ onCurrentValueChange(ev) {
+ this.onValueChange(ev.target.value, this.currentValueField);
+ }
+ onMaxValueChange(ev) {
+ this.onValueChange(ev.target.value, this.maxValueField);
+ }
+
onInputBlur() {
if (
document.activeElement !== this.maxValueRef.el &&
@@ -109,40 +98,6 @@ export class ProgressBarField extends Component {
onInputFocus() {
this.state.isEditing = true;
}
- onMaxValueChange(ev) {
- let parsedValue;
- try {
- parsedValue = parseFloat(ev.target.value);
- } catch {
- this.props.record.setInvalidField(this.props.name);
- return;
- }
-
- if (this.isMaxValueInteger) {
- parsedValue = Math.floor(parsedValue);
- }
- this.state.maxValue = parsedValue;
- this.props.record.update({ [this.getMaxValueField(this.props)]: parsedValue });
- if (this.props.readonly) {
- this.props.record.save();
- }
- }
- onCurrentValueInput(ev) {
- const parser = parsers.get(this.isCurrentValueInteger ? "integer" : "float");
- try {
- this.state.currentValue = parser(ev.target.value);
- } catch {
- // pass
- }
- }
- onMaxValueInput(ev) {
- const parser = parsers.get(this.isMaxValueInteger ? "integer" : "float");
- try {
- this.state.maxValue = parser(ev.target.value);
- } catch {
- // pass
- }
- }
}
export const progressBarField = {
diff --git a/addons/web/static/src/views/fields/progress_bar/progress_bar_field.xml b/addons/web/static/src/views/fields/progress_bar/progress_bar_field.xml
index a3ae7201445..31f11ac64b6 100644
--- a/addons/web/static/src/views/fields/progress_bar/progress_bar_field.xml
+++ b/addons/web/static/src/views/fields/progress_bar/progress_bar_field.xml
@@ -4,8 +4,8 @@
-
-
+
@@ -18,7 +18,6 @@
t-att-value="formatCurrentValue() or ''"
t-att-required="props.required"
t-on-change="onCurrentValueChange"
- t-on-input="onCurrentValueInput"
t-on-focus="onInputFocus"
t-on-blur="onInputBlur"
/>
@@ -34,7 +33,6 @@
inputmode="decimal"
t-att-value="formatCurrentValue()"
t-on-change="onCurrentValueChange"
- t-on-input="onCurrentValueInput"
t-on-focus="onInputFocus"
t-on-blur="onInputBlur"
/>
@@ -48,7 +46,6 @@
inputmode="decimal"
t-att-value="formatMaxValue()"
t-on-change="onMaxValueChange"
- t-on-input="onMaxValueInput"
t-on-focus="onInputFocus"
t-on-blur="onInputBlur"
/>
diff --git a/addons/web/static/src/views/fields/properties/properties_field.js b/addons/web/static/src/views/fields/properties/properties_field.js
index bc695f6aa11..86dc201596c 100644
--- a/addons/web/static/src/views/fields/properties/properties_field.js
+++ b/addons/web/static/src/views/fields/properties/properties_field.js
@@ -91,13 +91,7 @@ export class PropertiesField extends Component {
* @returns {array}
*/
get propertiesList() {
- const propertiesValues = (this.props.record.data[this.props.name] || []).map(property => {
- if (["date", "datetime"].includes(property.type)) {
- // Date and Datetime are immutable luxon objects.
- return property;
- }
- return JSON.parse(JSON.stringify(property));
- });
+ const propertiesValues = this.props.record.data[this.props.name] || [];
return propertiesValues.filter((definition) => !definition.definition_deleted);
}
diff --git a/addons/web/static/src/views/fields/radio/radio_field.js b/addons/web/static/src/views/fields/radio/radio_field.js
index aa4acb2b2df..e1cb207e787 100644
--- a/addons/web/static/src/views/fields/radio/radio_field.js
+++ b/addons/web/static/src/views/fields/radio/radio_field.js
@@ -1,10 +1,10 @@
/** @odoo-module **/
-import { registry } from "@web/core/registry";
-import { _lt } from "@web/core/l10n/translation";
-import { standardFieldProps } from "../standard_field_props";
-
import { Component } from "@odoo/owl";
+import { _lt } from "@web/core/l10n/translation";
+import { registry } from "@web/core/registry";
+import { useSpecialData } from "@web/views/fields/relational_utils";
+import { standardFieldProps } from "../standard_field_props";
export class RadioField extends Component {
static template = "web.RadioField";
@@ -12,6 +12,7 @@ export class RadioField extends Component {
...standardFieldProps,
orientation: { type: String, optional: true },
label: { type: String, optional: true },
+ domain: { type: Array, optional: true },
};
static defaultProps = {
orientation: "vertical",
@@ -19,28 +20,34 @@ export class RadioField extends Component {
static nextId = 0;
- static getItems(fieldName, record) {
- switch (record.fields[fieldName].type) {
+ setup() {
+ this.type = this.props.record.fields[this.props.name].type;
+ if (this.type === "many2one") {
+ this.specialData = useSpecialData(async (orm, props) => {
+ const { relation } = props.record.fields[props.name];
+ const kwargs = {
+ specification: { display_name: 1 },
+ domain: props.domain,
+ };
+ const { records } = await orm.call(relation, "unity_web_search_read", [], kwargs);
+ return records.map((record) => [record.id, record.display_name]);
+ });
+ }
+ }
+
+ get items() {
+ switch (this.type) {
case "selection":
- return record.fields[fieldName].selection;
+ return this.props.record.fields[this.props.name].selection;
case "many2one": {
- const value = record.preloadedData[fieldName] || [];
- return value.map((item) => [item.id, item.display_name]);
+ return this.specialData.data;
}
default:
return [];
}
}
-
- setup() {
- this.id = `radio_field_${++RadioField.nextId}`;
- }
-
- get items() {
- return RadioField.getItems(this.props.name, this.props.record);
- }
get value() {
- switch (this.props.record.fields[this.props.name].type) {
+ switch (this.type) {
case "selection":
return this.props.record.data[this.props.name];
case "many2one":
@@ -56,7 +63,7 @@ export class RadioField extends Component {
* @param {any} value
*/
onChange(value) {
- switch (this.props.record.fields[this.props.name].type) {
+ switch (this.type) {
case "selection":
this.props.record.update({ [this.props.name]: value[0] });
break;
@@ -79,22 +86,11 @@ export const radioField = {
],
supportedTypes: ["many2one", "selection"],
isEmpty: (record, fieldName) => record.data[fieldName] === false,
- extractProps: ({ options, string }) => ({
+ extractProps: ({ options, string }, dynamicInfo) => ({
orientation: options.horizontal ? "horizontal" : "vertical",
label: string,
+ domain: dynamicInfo.domain(),
}),
- legacySpecialData: "_fetchSpecialMany2ones",
};
registry.category("fields").add("radio", radioField);
-
-export async function preloadRadio(orm, record, fieldName, { domain }) {
- const field = record.fields[fieldName];
- const records = await orm.searchRead(field.relation, domain, ["id"]);
- return await orm.nameGet(field.relation, [records.map((record) => record.id)]);
-}
-
-registry.category("preloadedData").add("radio", {
- loadOnTypes: ["many2one"],
- preload: preloadRadio,
-});
diff --git a/addons/web/static/src/views/fields/reference/reference_field.js b/addons/web/static/src/views/fields/reference/reference_field.js
index dbd09ed21c9..890b7e00ea6 100644
--- a/addons/web/static/src/views/fields/reference/reference_field.js
+++ b/addons/web/static/src/views/fields/reference/reference_field.js
@@ -2,14 +2,39 @@
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
+import { useRecordObserver } from "@web/model/relational_model/utils";
import { many2OneField, Many2OneField } from "../many2one/many2one_field";
-import { Component, onWillUpdateProps, useState } from "@odoo/owl";
+import { Component, useState } from "@odoo/owl";
-function valuesEqual(a, b) {
- return a.resId === b.resId && a.resModel === b.resModel;
-}
+/**
+ * @typedef ReferenceValue
+ * @property {string} resModel
+ * @property {number} resId
+ * @property {string} displayName
+ */
+/**
+ * 1. Reference field is a char field
+ * 2. Reference widget has model_field prop
+ * 3. Standard case
+ */
+
+/**
+ * This class represents a reference field widget. It can be used to display
+ * a reference field OR a char field.
+ * The res_model of the relation is defined either by the reference field itself
+ * or by the model_field prop.
+ *
+ * 1) Reference field is a char field
+ * We have to fetch the display name (name_get) of the referenced record.
+ *
+ * 2) Reference widget has model_field prop
+ * We have to fetch the technical name of the co model.
+ *
+ * 3) Standard case
+ * The value is already in record.data[fieldName]
+ */
export class ReferenceField extends Component {
static template = "web.ReferenceField";
static components = {
@@ -17,120 +42,198 @@ export class ReferenceField extends Component {
};
static props = {
...Many2OneField.props,
- hideModelSelector: { type: Boolean, optional: true },
+ hideModel: { type: Boolean, optional: true },
+ modelField: { type: String, optional: true },
};
static defaultProps = {
...Many2OneField.defaultProps,
};
setup() {
+ /** @type {{formattedCharValue?: ReferenceValue, modelName?: string}} */
this.state = useState({
- resModel: this.relation,
+ formattedCharValue: undefined, // Value extracted from reference char field
+ modelName: undefined, // Name get of the value of the model field
});
-
- this.currentValue = this.getValue(this.props);
-
- onWillUpdateProps((nextProps) => {
- if (
- valuesEqual(this.currentValue || {}, this.getValue(nextProps) || {}) &&
- this.state.resModel &&
- this.getRelation(nextProps) !== this.state.resModel
- ) {
- nextProps.record.update({ [this.props.name]: false });
- }
- this.currentValue = this.getValue(this.props);
- });
- }
-
- getPreloadedData(p) {
- return p.record.preloadedData[p.name];
- }
- getValue(p) {
- if (p.record.fields[p.name].type === "char") {
- const pdata = this.getPreloadedData(p);
- if (!pdata) {
- return null;
- }
- return {
- resModel: pdata.model,
- resId: pdata.data.id,
- displayName: pdata.data.display_name,
- };
+ this.currentValue = undefined;
+ this.currentRelation = this.getRelation();
+ if (this._isCharField(this.props)) {
+ /** Fetch the display name of the record referenced by the field */
+ useRecordObserver(async (record) => {
+ if (this.currentValue !== record.data[this.props.name]) {
+ this.state.formattedCharValue = await this._fetchReferenceCharData(this.props);
+ this.currentValue = record.data[this.props.name];
+ }
+ });
+ } else if (this.props.modelField) {
+ /** Fetch the technical name of the co model */
+ useRecordObserver(async (record) => {
+ if (this.currentModelId !== record.data[this.props.modelField]?.[0]) {
+ this.state.modelName = await this._fetchModelTechnicalName(this.props);
+ if (this.currentModelId !== undefined) {
+ record.update({ [this.props.name]: false });
+ }
+ this.currentModelId = record.data[this.props.modelField]?.[0];
+ }
+ });
} else {
- return p.record.data[p.name];
+ this.currentValue = this.props.record.data[this.props.name];
}
}
+
get m2oProps() {
- const value = this.getValue(this.props);
+ const value = this.getValue();
const p = {
...this.props,
- relation: this.relation,
+ relation: this.getRelation(),
value: value && [value.resId, value.displayName],
update: this.updateM2O.bind(this),
};
- delete p.hideModelSelector;
+ delete p.hideModel;
+ delete p.modelField;
return p;
}
get selection() {
- if (
- this.props.record.fields[this.props.name].type !== "char" &&
- !this.props.hideModelSelector
- ) {
+ if (!this._isCharField(this.props) && !this.hideModelSelector) {
return this.props.record.fields[this.props.name].selection;
}
return [];
}
get relation() {
- return this.getRelation(this.props);
+ return this.getRelation();
}
- getRelation(props) {
- const modelName = this.getModelName(props);
+ get hideModelSelector() {
+ return this.props.hideModel || this.props.modelField;
+ }
+
+ getRelation() {
+ const modelName = this.getModelName();
if (modelName) {
return modelName;
}
- const value = this.getValue(props);
+ const value = this.getValue();
if (value && value.resModel) {
return value.resModel;
} else {
- return this.state && this.state.resModel;
+ return this.currentRelation;
}
}
- getModelName(p) {
- if (p.hideModelSelector && p.record.preloadedData[p.name]) {
- return p.record.preloadedData[p.name].modelName;
+ /**
+ * @returns {ReferenceValue|false}
+ */
+ getValue() {
+ if (this._isCharField(this.props)) {
+ return this.state.formattedCharValue;
+ } else {
+ return this.props.record.data[this.props.name];
}
- return null;
+ }
+
+ /**
+ * @returns {string|undefined}
+ */
+ getModelName() {
+ return this.hideModelSelector && this.state.modelName;
}
updateModel(value) {
- this.state.resModel = value;
+ this.currentRelation = value;
this.props.record.update({ [this.props.name]: false });
}
updateM2O(data) {
const value = data[this.props.name];
- if (!this.state.resModel) {
- this.state.resModel = this.relation;
+ if (!this.currentRelation) {
+ this.currentRelation = this.getRelation();
}
this.props.record.update({
[this.props.name]: value && {
- resModel: this.state.resModel,
+ resModel: this.currentRelation,
resId: value[0],
displayName: value[1],
},
});
}
+
+ /**
+ * Return true if the reference field is a char field.
+ */
+ _isCharField(props) {
+ return props.record.fields[props.name].type === "char";
+ }
+
+ /**
+ * Fetch special data if the reference field is a char field.
+ * It fetches the display name of the record.
+ *
+ * @returns {Promise<{ resId: number, resModel: string, displayName: string }|false>}
+ */
+ async _fetchReferenceCharData(props) {
+ const recordData = props.record.data[props.name];
+ if (!recordData) {
+ return false;
+ }
+ const [resModel, _resId] = recordData.split(",");
+ const resId = parseInt(_resId, 10);
+ if (resModel && resId) {
+ const { specialDataCaches, orm } = props.record.model;
+ const key = `__reference__name_get-${recordData}`;
+ if (!specialDataCaches[key]) {
+ specialDataCaches[key] = orm.nameGet(resModel, [resId]);
+ }
+ const result = await specialDataCaches[key];
+ return {
+ resId,
+ resModel,
+ displayName: result[0][1],
+ };
+ }
+ return false;
+ }
+
+ /**
+ * Ensure that the modelField is a many2one to ir.model
+ */
+ _assertMany2OneToIrModel(props) {
+ const field = props.modelField && props.record.fields[props.modelField];
+ if (field && (field.type !== "many2one" || field.relation !== "ir.model")) {
+ throw new Error(
+ `The model_field (${props.modelField}) of the reference field ${props.name} must be a many2one('ir.model').`
+ );
+ }
+ }
+
+ /**
+ * Fetch the technical name of the model which is selected in the modelField
+ * props
+ *
+ * @returns {Promise}
+ */
+ async _fetchModelTechnicalName(props) {
+ this._assertMany2OneToIrModel(props);
+ const record = props.record;
+ const modelId = record.data[props.modelField]?.[0];
+ if (!modelId) {
+ return false;
+ }
+ const { specialDataCaches, orm } = props.record.model;
+ const key = `__reference__ir_model-${modelId}`;
+ if (!specialDataCaches[key]) {
+ specialDataCaches[key] = orm.read("ir.model", [modelId], ["model"]);
+ }
+ const result = await specialDataCaches[key];
+ return result[0].model;
+ }
}
export const referenceField = {
component: ReferenceField,
displayName: _lt("Reference"),
supportedTypes: ["reference", "char"],
- legacySpecialData: "_fetchSpecialReference",
extractProps({ options }) {
/*
1 -
@@ -141,7 +244,8 @@ export const referenceField = {
We want to display the model selector only in the 4th case.
*/
const props = many2OneField.extractProps(...arguments);
- props.hideModelSelector = !!options.hide_model || !!options.model_field;
+ props.hideModel = !!options.hide_model;
+ props.modelField = options.model_field;
return props;
},
};
diff --git a/addons/web/static/src/views/fields/reference/reference_field.xml b/addons/web/static/src/views/fields/reference/reference_field.xml
index 82604929b20..5e444140504 100644
--- a/addons/web/static/src/views/fields/reference/reference_field.xml
+++ b/addons/web/static/src/views/fields/reference/reference_field.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js
index 1c4e41f1b5e..c698430d469 100644
--- a/addons/web/static/src/views/list/list_renderer.js
+++ b/addons/web/static/src/views/list/list_renderer.js
@@ -2,7 +2,7 @@
import { browser } from "@web/core/browser/browser";
import { CheckBox } from "@web/core/checkbox/checkbox";
-import { Domain } from "@web/core/domain";
+import { Domain, evalDomain } from "@web/core/domain";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
@@ -12,9 +12,9 @@ import { registry } from "@web/core/registry";
import { useBus, useService } from "@web/core/utils/hooks";
import { useSortable } from "@web/core/utils/sortable";
import { getTabableElements } from "@web/core/utils/ui";
-import { Field } from "@web/views/fields/field";
+import { Field, getPropertyFieldInfo } from "@web/views/fields/field";
import { getTooltipInfo } from "@web/views/fields/field_tooltip";
-import { evalDomain, getClassNameFromDecoration } from "@web/views/utils";
+import { getClassNameFromDecoration } from "@web/views/utils";
import { ViewButton } from "@web/views/view_button/view_button";
import { useBounceButton } from "@web/views/view_hook";
import { Widget } from "@web/views/widgets/widget";
@@ -110,7 +110,10 @@ export class ListRenderer extends Component {
this.cellToFocus = null;
this.activeRowId = null;
- onMounted(() => {
+ onMounted(async () => {
+ // Due to the way elements are mounted in the DOM by Owl (bottom-to-top),
+ // we need to wait the next micro task tick to set the activeElement.
+ await Promise.resolve();
this.activeElement = this.uiService.activeElement;
});
onWillPatch(() => {
@@ -121,7 +124,11 @@ export class ListRenderer extends Component {
this.allColumns = this.processAllColumn(nextProps.archInfo.columns, nextProps.list);
this.state.columns = this.getActiveColumns(nextProps.list);
});
- onPatched(() => {
+ onPatched(async () => {
+ // HACK: we need to wait for the next tick to be sure that the Field components are patched.
+ // OWL don't wait the patch for the children components if the children trigger a patch by himself.
+ await Promise.resolve();
+
const editedRecord = this.props.list.editedRecord;
if (editedRecord && this.activeRowId !== editedRecord.id) {
if (this.cellToFocus && this.cellToFocus.record === editedRecord) {
@@ -187,7 +194,7 @@ export class ListRenderer extends Component {
() => {
this.freezeColumnWidths();
},
- () => [this.state.columns, this.isEmpty]
+ () => [this.state.columns, this.isEmpty, this.props.list.offset, this.props.list.limit]
);
useExternalListener(window, "resize", () => {
this.columnWidths = null;
@@ -238,32 +245,36 @@ export class ListRenderer extends Component {
processAllColumn(allColumns, list) {
return allColumns.flatMap((column) => {
if (column.type === "field" && list.fields[column.name].type === "properties") {
- return Object.values(list.activeFields)
- .filter(
- (activeField) =>
- activeField.relatedPropertyField &&
- activeField.relatedPropertyField.fieldName === column.name
- )
- .map((activeField) => ({
- ...activeField,
- id: `${column.id}_${activeField.name}`,
- classNames: column.classNames,
- optional: "hide",
- type: "field",
- hasLabel: true,
- label: activeField.string,
- }));
+ return this.getPropertyFieldColumns(column, list);
} else {
return [column];
}
});
}
+ getPropertyFieldColumns(column, list) {
+ return Object.values(list.fields)
+ .filter(
+ (field) =>
+ field.relatedPropertyField &&
+ field.relatedPropertyField.fieldName === column.name
+ )
+ .map((propertyField) => {
+ return {
+ ...getPropertyFieldInfo(propertyField),
+ id: `${column.id}_${propertyField.name}`,
+ classNames: column.classNames,
+ optional: "hide",
+ type: "field",
+ hasLabel: true,
+ label: propertyField.string,
+ sortable: false,
+ };
+ });
+ }
+
getFieldProps(record, column) {
- if (
- this.isCellReadonly(column, record) ||
- (this.props.activeActions?.edit === false && !record.isNew)
- ) {
+ if (this.isCellReadonly(column, record) || this.isRecordReadonly(record)) {
return {
readonly: true,
};
@@ -397,8 +408,7 @@ export class ListRenderer extends Component {
if (!this.props.list.canResequence()) {
return false;
}
- const orderBy = this.props.list.orderBy;
- const handleField = this.props.archInfo.handleField;
+ const { handleField, orderBy } = this.props.list;
return !orderBy.length || (orderBy.length && orderBy[0].name === handleField);
}
@@ -425,7 +435,30 @@ export class ListRenderer extends Component {
}
canUseFormatter(column, record) {
- return !record.isInEdition && !column.widget;
+ if (column.widget) {
+ return false;
+ }
+ if (record.isInEdition && (record.model.multiEdit || this.isInlineEditable(record))) {
+ // in a x2many non editable list, a record is in edition when it is opened in a dialog,
+ // but in the list we want it to still be displayed in readonly.
+ return false;
+ }
+ return true;
+ }
+
+ isRecordReadonly(record) {
+ if (record.isNew) {
+ return false;
+ }
+ if (this.props.activeActions?.edit === false) {
+ return true;
+ }
+ if (record.isInEdition && !this.isInlineEditable(record) && !record.model.multiEdit) {
+ // in a x2many non editable list, a record is in edition when it is opened in a dialog,
+ // but in the list we want it to still be displayed in readonly.
+ return true;
+ }
+ return false;
}
focusCell(column, forward = true) {
@@ -499,7 +532,7 @@ export class ListRenderer extends Component {
createKeyOptionalFields() {
let keyParts = {
- fields: this.props.list.fieldNames,
+ fields: this.props.list.fieldNames, // FIXME: use something else?
model: this.props.list.resModel,
viewMode: "list",
viewId: this.env.config.viewId,
@@ -617,7 +650,7 @@ export class ListRenderer extends Component {
let currencyId;
if (type === "monetary" || widget === "monetary") {
const currencyField =
- this.props.list.activeFields[fieldName].options.currency_field ||
+ column.options.currency_field ||
this.fields[fieldName].currency_field ||
"currency_id";
if (!(currencyField in this.props.list.activeFields)) {
@@ -811,7 +844,7 @@ export class ListRenderer extends Component {
if (required && evalDomain(required, record.evalContext)) {
classNames.push("o_required_modifier");
}
- if (record.isInvalid(column.name)) {
+ if (record.isFieldInvalid(column.name)) {
classNames.push("o_invalid_cell");
}
if (this.isCellReadonly(column, record)) {
@@ -1049,11 +1082,11 @@ export class ListRenderer extends Component {
this.cellToFocus = null;
} else {
await recordAfterResequence();
- await record.switchMode("edit");
+ await this.props.list.enterEditMode(record);
this.cellToFocus = { column, record };
}
} else if (this.props.list.editedRecord && this.props.list.editedRecord !== record) {
- this.props.list.unselectRecord(true);
+ this.props.list.leaveEditMode();
} else if (!this.props.archInfo.noOpen) {
this.props.openRecord(record);
}
@@ -1063,8 +1096,8 @@ export class ListRenderer extends Component {
this.keepColumnWidths = true;
const editedRecord = this.props.list.editedRecord;
if (editedRecord && editedRecord !== record) {
- const unselected = await this.props.list.unselectRecord(true);
- if (!unselected) {
+ const leaved = await this.props.list.leaveEditMode();
+ if (!leaved) {
return;
}
}
@@ -1143,13 +1176,8 @@ export class ListRenderer extends Component {
/**
* @param {KeyboardEvent} ev
- * @param { import('@web/views/relational_model').Group
- * | null
- * } group
- * @param { import('@web/views/relational_model').Record
- * | import('@web/views/basic_relational_model').Record
- * | null
- * } record
+ * @param { import('@web/model/relational_model/group').Group | null } group
+ * @param { import('@web/model/relational_model/record').Record | null } record
*/
onCellKeydown(ev, group = null, record = null) {
if (this.props.list.model.useSampleModel) {
@@ -1271,7 +1299,7 @@ export class ListRenderer extends Component {
let toFocus, futureRecord;
const index = list.selection.indexOf(record);
if (this.lastIsDirty && ["tab", "shift+tab", "enter"].includes(hotkey)) {
- record.switchMode("readonly");
+ list.leaveEditMode();
return true;
}
@@ -1304,7 +1332,7 @@ export class ListRenderer extends Component {
case "enter":
if (list.selection.length === 1) {
- record.switchMode("readonly");
+ list.leaveEditMode();
return true;
}
futureRecord = list.selection[index + 1] || list.selection[0];
@@ -1312,7 +1340,7 @@ export class ListRenderer extends Component {
}
if (futureRecord) {
- futureRecord.switchMode("edit");
+ list.enterEditMode(futureRecord);
return true;
}
return false;
@@ -1322,7 +1350,7 @@ export class ListRenderer extends Component {
const { editable } = this.props;
const groupIndex = group.list.records.indexOf(record);
const isLastOfGroup = groupIndex === group.list.records.length - 1;
- const isDirty = record.isDirty || this.lastIsDirty;
+ const isDirty = record.dirty || this.lastIsDirty;
const isEnterBehavior = hotkey === "enter" && (!record.canBeAbandoned || isDirty);
const isTabBehavior = hotkey === "tab" && !record.canBeAbandoned && isDirty;
if (
@@ -1360,12 +1388,8 @@ export class ListRenderer extends Component {
/**
* @param {string} hotkey
* @param {HTMLTableCellElement} cell
- * @param { import('@web/views/relational_model').Group
- * | null
- * } group
- * @param { import('@web/views/relational_model').Record
- * | import('@web/views/basic_relational_model').Record
- * } record
+ * @param { import('@web/model/relational_model/group').Group | null } group
+ * @param { import('@web/model/relational_model/record').Record } record
* @returns {boolean} true if some behavior has been taken
*/
onCellKeydownEditMode(hotkey, cell, group, record) {
@@ -1395,8 +1419,8 @@ export class ListRenderer extends Component {
const lastIndex = topReCreate ? 0 : list.records.length - 1;
if (index === lastIndex) {
if (this.displayRowCreates) {
- if (record.isNew && !record.isDirty) {
- list.unselectRecord(true);
+ if (record.isNew && !record.dirty) {
+ list.leaveEditMode();
return false;
}
// add a line
@@ -1405,12 +1429,12 @@ export class ListRenderer extends Component {
} else if (
this.canCreate &&
!record.canBeAbandoned &&
- (record.isDirty || this.lastIsDirty)
+ (record.dirty || this.lastIsDirty)
) {
this.add({ group });
} else if (cycleOnTab) {
if (record.canBeAbandoned) {
- list.unselectRecord(true);
+ list.leaveEditMode();
}
const futureRecord = list.records[0];
if (record === futureRecord) {
@@ -1418,14 +1442,14 @@ export class ListRenderer extends Component {
const toFocus = this.findNextFocusableOnRow(row);
this.focus(toFocus);
} else {
- futureRecord.switchMode("edit");
+ list.enterEditMode(futureRecord);
}
} else {
return false;
}
} else {
const futureRecord = list.records[index + 1];
- futureRecord.switchMode("edit");
+ list.enterEditMode(futureRecord);
}
break;
}
@@ -1434,7 +1458,7 @@ export class ListRenderer extends Component {
if (index === 0) {
if (cycleOnTab) {
if (record.canBeAbandoned) {
- list.unselectRecord(true);
+ list.leaveEditMode();
}
const futureRecord = list.records[list.records.length - 1];
if (record === futureRecord) {
@@ -1443,16 +1467,16 @@ export class ListRenderer extends Component {
this.focus(toFocus);
} else {
this.cellToFocus = { forward: false, record: futureRecord };
- futureRecord.switchMode("edit");
+ list.enterEditMode(futureRecord);
}
} else {
- list.unselectRecord(true);
+ list.leaveEditMode();
return false;
}
} else {
const futureRecord = list.records[index - 1];
this.cellToFocus = { forward: false, record: futureRecord };
- futureRecord.switchMode("edit");
+ list.enterEditMode(futureRecord);
}
break;
}
@@ -1468,19 +1492,22 @@ export class ListRenderer extends Component {
}
if (futureRecord) {
- futureRecord.switchMode("edit", { checkValidity: true });
+ list.leaveEditMode({ validate: true }).then((canProceed) => {
+ if (canProceed) {
+ list.enterEditMode(futureRecord);
+ }
+ });
} else if (this.lastIsDirty || !record.canBeAbandoned || this.displayRowCreates) {
this.add({ group });
} else {
futureRecord = list.records.at(0);
- futureRecord.switchMode("edit", { checkValidity: true });
+ list.enterEditMode(futureRecord);
}
break;
}
case "escape": {
// TODO this seems bad: refactor this
- record.discard();
- list.unselectRecord(true);
+ list.leaveEditMode({ discard: true });
const firstAddButton = this.tableRef.el.querySelector(
".o_field_x2many_list_row_add a"
);
@@ -1519,13 +1546,10 @@ export class ListRenderer extends Component {
/**
* @param {string} hotkey
* @param {HTMLTableCellElement} cell
- * @param { import('@web/views/relational_model').Group
+ * @param { import('@web/model/relational_model/group').Group
* | null
* } group
- * @param { import('@web/views/relational_model').Record
- * | import('@web/views/basic_relational_model').Record
- * | null
- * } record
+ * @param { import('@web/model/relational_model/record').Record | null } record
* @returns {boolean} true if some behavior has been taken
*/
onCellKeydownReadOnlyMode(hotkey, cell, group, record) {
@@ -1631,7 +1655,7 @@ export class ListRenderer extends Component {
(c) => c.name === cell.getAttribute("name")
);
this.cellToFocus = { column, record };
- record.switchMode("edit");
+ this.props.list.enterEditMode(record);
return true;
}
@@ -1833,7 +1857,7 @@ export class ListRenderer extends Component {
if (ev.target.closest(".ui-autocomplete")) {
return;
}
- this.props.list.unselectRecord(true);
+ this.props.list.leaveEditMode();
}
calculateColumnWidth(column) {
@@ -2000,13 +2024,11 @@ export class ListRenderer extends Component {
* @param {HTMLElement} [params.previous]
*/
async sortDrop(dataRowId, { element, previous }) {
- if (this.props.list.editedRecord) {
- this.props.list.unselectRecord(true);
- }
+ await this.props.list.leaveEditMode();
element.classList.remove("o_row_draggable");
const refId = previous ? previous.dataset.id : null;
this.resequencePromise = this.props.list.resequence(dataRowId, refId, {
- handleField: this.props.archInfo.handleField,
+ handleField: this.props.list.handleField,
});
await this.resequencePromise;
element.classList.add("o_row_draggable");
diff --git a/addons/web/static/src/views/list/list_renderer.xml b/addons/web/static/src/views/list/list_renderer.xml
index cf1983bf7c5..9898864c0a0 100644
--- a/addons/web/static/src/views/list/list_renderer.xml
+++ b/addons/web/static/src/views/list/list_renderer.xml
@@ -77,7 +77,7 @@
-