[REF] *: js fields: extractProps receives fieldInfo

This commit is part of the preliminary work to rewrite the form,
list and kanban model. We want this new Model to only be aware of
field related information it needs (whereas in its current
implementation, the model stores all the information extracted
from the field node in the arch). This would allow to properly
manage multiple occurrences of the same field in views, that is,
each occurrence would be represented by a field component (if
visible of course), and that field component would use the field
information of the arch node it represents. To this end, we want
fields from not using anymore information stored in activeFields
in the record datapoint. Instead, we now call extractProps with
the whole fieldInfo (the information extracted from the arch), s.t.
each field can generate the props it needs from those information
(e.g. sub views for x2manys).

This commit doesn't remove the use of record.activeFields in
concrete fields (this will come later), but reworks the fieldInfo
object generated by parseFieldNode, and provide it to the calls of
extractProps. In fieldInfo, the `options` key is no longer inside
`attrs`, as it is now top-level, alongside several other generic
keys that have been processed (like on_change, modifiers...). For
that reason, a lot of extractProps definitions had to be adapted.

Part of task 3179751

closes odoo/odoo#113092

Related: odoo/enterprise#37266
Signed-off-by: Jorge Pinna Puissant (jpp) <jpp@odoo.com>
This commit is contained in:
Aaron Bohy
2023-02-24 11:11:32 +01:00
parent 8401192620
commit 9374a6f2b7
57 changed files with 282 additions and 326 deletions
@@ -592,13 +592,13 @@ export const analyticDistribution = {
component: AnalyticDistribution,
supportedTypes: ["char", "text"],
fieldDependencies: [{ name:"analytic_precision", type: "integer" }],
extractProps: ({ attrs }) => ({
business_domain: attrs.options.business_domain,
account_field: attrs.options.account_field,
product_field: attrs.options.product_field,
extractProps: ({ attrs, options }) => ({
business_domain: options.business_domain,
account_field: options.account_field,
product_field: options.product_field,
business_domain_compute: attrs.business_domain_compute,
force_applicability: attrs.options.force_applicability,
allow_save: !attrs.options.disable_save,
force_applicability: options.force_applicability,
allow_save: !options.disable_save,
}),
};
@@ -24,8 +24,8 @@ export const iconSelectionField = {
component: IconSelectionField,
displayName: _lt("Icon Selection"),
supportedTypes: ["char", "text", "selection"],
extractProps: ({ attrs }) => ({
icons: attrs.options,
extractProps: ({ options }) => ({
icons: options,
}),
};
@@ -36,10 +36,10 @@ FilterableSelectionField.props = {
export const filterableSelectionField = {
...selectionField,
component: FilterableSelectionField,
extractProps: (params) => ({
...selectionField.extractProps(params),
whitelisted_values: params.attrs.options.whitelisted_values,
blacklisted_values: params.attrs.options.blacklisted_values,
extractProps: (fieldInfo) => ({
...selectionField.extractProps(fieldInfo),
whitelisted_values: fieldInfo.options.whitelisted_values,
blacklisted_values: fieldInfo.options.blacklisted_values,
}),
};
@@ -52,21 +52,21 @@ TextField.props = {
};
const charExtractProps = charField.extractProps;
charField.extractProps = (attrs) => {
return Object.assign(charExtractProps(attrs), {
onchangeOnKeydown: archParseBoolean(attrs.onchange_on_keydown),
keydownDebounceDelay: attrs.keydown_debounce_delay
? Number(attrs.keydown_debounce_delay)
charField.extractProps = (fieldInfo) => {
return Object.assign(charExtractProps(fieldInfo), {
onchangeOnKeydown: archParseBoolean(fieldInfo.attrs.onchange_on_keydown),
keydownDebounceDelay: fieldInfo.attrs.keydown_debounce_delay
? Number(fieldInfo.attrs.keydown_debounce_delay)
: 2000,
});
};
const textExtractProps = textField.extractProps;
textField.extractProps = (params) => {
return Object.assign(textExtractProps(params), {
onchangeOnKeydown: archParseBoolean(params.attrs.onchange_on_keydown),
keydownDebounceDelay: params.attrs.keydown_debounce_delay
? Number(params.attrs.keydown_debounce_delay)
textField.extractProps = (fieldInfo) => {
return Object.assign(textExtractProps(fieldInfo), {
onchangeOnKeydown: archParseBoolean(fieldInfo.attrs.onchange_on_keydown),
keydownDebounceDelay: fieldInfo.attrs.keydown_debounce_delay
? Number(fieldInfo.attrs.keydown_debounce_delay)
: 2000,
});
};
@@ -50,9 +50,9 @@ KanbanMany2OneAvatarUserField.props = {
export const kanbanMany2OneAvatarUserField = {
...many2OneAvatarUserField,
component: KanbanMany2OneAvatarUserField,
extractProps: (params) => ({
...many2OneAvatarUserField.extractProps(params),
displayAvatarName: params.attrs.options.display_avatar_name || false,
extractProps: (fieldInfo) => ({
...many2OneAvatarUserField.extractProps(fieldInfo),
displayAvatarName: fieldInfo.options.display_avatar_name || false,
}),
};
@@ -153,10 +153,10 @@ FieldMany2OneMailingFilter.defaultProps = {
export const fieldMany2OneMailingFilter = {
...many2OneField,
component: FieldMany2OneMailingFilter,
extractProps: (params) => ({
...many2OneField.extractProps(params),
domain_field: params.attrs.options.domain_field,
model_field: params.attrs.options.model_field,
extractProps: (fieldInfo) => ({
...many2OneField.extractProps(fieldInfo),
domain_field: fieldInfo.options.domain_field,
model_field: fieldInfo.options.model_field,
}),
};
@@ -599,11 +599,11 @@ export const massMailingHtmlField = {
...htmlField,
component: MassMailingHtmlField,
displayName: _lt("Email"),
extractProps: (params) => ({
...htmlField.extractProps(params),
filterTemplates: params.attrs.options.filterTemplates,
inlineField: params.attrs.options['inline-field'],
iframeHtmlClass: params.attrs['iframeHtmlClass'],
extractProps: (fieldInfo) => ({
...htmlField.extractProps(fieldInfo),
filterTemplates: fieldInfo.options.filterTemplates,
inlineField: fieldInfo.options['inline-field'],
iframeHtmlClass: fieldInfo.attrs.iframeHtmlClass,
}),
};
@@ -20,10 +20,10 @@ patch(PhoneField, "sms.PhoneField", {
});
const patchDescr = {
extractProps({ attrs }) {
extractProps(fieldInfo) {
return {
...this._super({ attrs }),
enableButton: attrs.options.enable_sms,
...this._super(fieldInfo),
enableButton: fieldInfo.options.enable_sms,
};
},
};
@@ -48,7 +48,7 @@
<t t-foreach="slot.record.fieldNames" t-as="fieldName" t-key="fieldName">
<t t-if="!slot.record.isInvisible(fieldName)">
<t t-set="fieldInfo" t-value="props.model.popoverFields[fieldName]" />
<li class="list-group-item flex-shrink-0 d-flex flex-wrap align-items-center" t-att-class="fieldInfo.rawAttrs.class">
<li class="list-group-item flex-shrink-0 d-flex flex-wrap align-items-center" t-att-class="fieldInfo.attrs.class">
<strong class="me-2">
<t t-if="fieldInfo.options.icon">
<b>
@@ -123,8 +123,8 @@ export const aceField = {
component: AceField,
displayName: _lt("Ace Editor"),
supportedTypes: ["text"],
extractProps: ({ attrs }) => ({
mode: attrs.options.mode,
extractProps: ({ options }) => ({
mode: options.mode,
}),
};
@@ -1,8 +1,9 @@
/** @odoo-module **/
import { _lt } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "../standard_field_props";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component } from "@odoo/owl";
const formatters = registry.category("formatters");
@@ -11,6 +12,10 @@ export class BadgeField extends Component {
static template = "web.BadgeField";
static props = {
...standardFieldProps,
decorations: { type: Object, optional: true },
};
static defaultProps = {
decorations: {},
};
get formattedValue() {
@@ -21,8 +26,9 @@ export class BadgeField extends Component {
}
get classFromDecoration() {
const evalContext = this.props.record.evalContext;
for (const decorationName in this.props.decorations) {
if (this.props.decorations[decorationName]) {
if (evaluateExpr(this.props.decorations[decorationName], evalContext)) {
return `text-bg-${decorationName}`;
}
}
@@ -34,6 +40,9 @@ export const badgeField = {
component: BadgeField,
displayName: _lt("Badge"),
supportedTypes: ["selection", "many2one", "char"],
extractProps: ({ decorations }) => {
return { decorations };
},
};
registry.category("fields").add("badge", badgeField);
@@ -69,8 +69,8 @@ export const binaryField = {
component: BinaryField,
displayName: _lt("File"),
supportedTypes: ["binary"],
extractProps: ({ attrs }) => ({
acceptedFileExtensions: attrs.options.accepted_file_extensions,
extractProps: ({ attrs, options }) => ({
acceptedFileExtensions: options.accepted_file_extensions,
fileNameField: attrs.filename,
}),
};
@@ -28,8 +28,8 @@ export const booleanIconField = {
component: BooleanIconField,
displayName: _lt("Boolean Icon"),
supportedTypes: ["boolean"],
extractProps: ({ attrs }) => ({
icon: attrs.options.icon,
extractProps: ({ options }) => ({
icon: options.icon,
}),
};
@@ -98,9 +98,9 @@ export const charField = {
component: CharField,
displayName: _lt("Text"),
supportedTypes: ["char"],
extractProps: ({ attrs }) => ({
extractProps: ({ attrs, options }) => ({
isPassword: archParseBoolean(attrs.password),
dynamicPlaceholder: attrs.options.dynamic_placeholder,
dynamicPlaceholder: options.dynamic_placeholder,
autocomplete: attrs.autocomplete,
placeholder: attrs.placeholder,
}),
@@ -62,8 +62,8 @@ export const dateField = {
component: DateField,
displayName: _lt("Date"),
supportedTypes: ["date", "datetime"],
extractProps: ({ attrs }) => ({
pickerOptions: attrs.options.datepicker,
extractProps: ({ attrs, options }) => ({
pickerOptions: options.datepicker,
placeholder: attrs.placeholder,
}),
};
@@ -154,11 +154,11 @@ export class DateRangeField extends Component {
export const dateRangeField = {
component: DateRangeField,
supportedTypes: ["date", "datetime"],
extractProps: ({ attrs }) => ({
relatedEndDateField: attrs.options.related_end_date,
relatedStartDateField: attrs.options.related_start_date,
extractProps: ({ attrs, options }) => ({
relatedEndDateField: options.related_end_date,
relatedStartDateField: options.related_start_date,
formatType: options.format_type,
placeholder: attrs.placeholder,
formatType: attrs.options.format_type,
}),
};
@@ -52,8 +52,8 @@ export const dateTimeField = {
component: DateTimeField,
displayName: _lt("Date & Time"),
supportedTypes: ["datetime"],
extractProps: ({ attrs }) => ({
pickerOptions: attrs.options.datepicker,
extractProps: ({ attrs, options }) => ({
pickerOptions: options.datepicker,
placeholder: attrs.placeholder,
}),
};
@@ -144,9 +144,9 @@ export const domainField = {
displayName: _lt("Domain"),
supportedTypes: ["char"],
isEmpty: () => false,
extractProps: ({ attrs }) => ({
editInDialog: attrs.options.in_dialog,
resModel: attrs.options.model,
extractProps: ({ options }) => ({
editInDialog: options.in_dialog,
resModel: options.model,
}),
};
+55 -95
View File
@@ -18,39 +18,24 @@ const fieldRegistry = registry.category("fields");
class DefaultField extends Component {}
DefaultField.template = xml``;
function getFieldClassFromRegistry(fieldType, widget, viewType, jsClass) {
if (jsClass && widget) {
const name = `${jsClass}.${widget}`;
if (fieldRegistry.contains(name)) {
return fieldRegistry.get(name);
function getFieldFromRegistry(fieldType, widget, viewType, jsClass) {
const prefixes = jsClass ? [jsClass, viewType, ""] : [viewType, ""];
const findInRegistry = (key) => {
for (const prefix of prefixes) {
const _key = prefix ? `${prefix}.${key}` : key;
if (fieldRegistry.contains(_key)) {
return fieldRegistry.get(_key);
}
}
}
if (viewType && widget) {
const name = `${viewType}.${widget}`;
if (fieldRegistry.contains(name)) {
return fieldRegistry.get(name);
}
}
};
if (widget) {
if (fieldRegistry.contains(widget)) {
return fieldRegistry.get(widget);
const field = findInRegistry(widget);
if (field) {
return field;
}
console.warn(`Missing widget: ${widget} for field of type ${fieldType}`);
}
if (viewType && fieldType) {
const name = `${viewType}.${fieldType}`;
if (fieldRegistry.contains(name)) {
return fieldRegistry.get(name);
}
}
if (fieldRegistry.contains(fieldType)) {
return fieldRegistry.get(fieldType);
}
return { component: DefaultField };
return findInRegistry(fieldType) || { component: DefaultField };
}
export function fieldVisualFeedback(field, record, fieldName, fieldInfo) {
@@ -75,10 +60,11 @@ export function fieldVisualFeedback(field, record, fieldName, fieldInfo) {
export class Field extends Component {
setup() {
this.field = this.props.fieldInfo.field;
if (!this.field) {
if (this.props.fieldInfo) {
this.field = this.props.fieldInfo.field;
} else {
const fieldType = this.props.record.fields[this.props.name].type;
this.field = getFieldClassFromRegistry(fieldType, this.props.type);
this.field = getFieldFromRegistry(fieldType, this.props.type);
}
}
@@ -88,7 +74,7 @@ export class Field extends Component {
this.field,
record,
name,
fieldInfo
fieldInfo || {}
);
const classNames = {
o_field_widget: true,
@@ -108,11 +94,13 @@ export class Field extends Component {
// generate field decorations classNames (only if field-specific decorations
// have been defined in an attribute, e.g. decoration-danger="other_field = 5")
// only handle the text-decoration.
const { decorations } = fieldInfo;
const evalContext = record.evalContext;
for (const decoName in decorations) {
const value = evaluateExpr(decorations[decoName], evalContext);
classNames[getClassNameFromDecoration(decoName)] = value;
if (fieldInfo && fieldInfo.decorations) {
const { decorations } = fieldInfo;
const evalContext = record.evalContext;
for (const decoName in decorations) {
const value = evaluateExpr(decorations[decoName], evalContext);
classNames[getClassNameFromDecoration(decoName)] = value;
}
}
return classNames;
@@ -125,28 +113,22 @@ export class Field extends Component {
get fieldComponentProps() {
const record = this.props.record;
const evalContext = record.evalContext;
const fieldInfo = this.props.fieldInfo;
const modifiers = fieldInfo.modifiers || {};
const readonlyFromModifiers = evalDomain(modifiers.readonly, evalContext);
let readonlyFromModifiers = false;
let propsFromNode = {};
if (this.props.fieldInfo) {
let fieldInfo = this.props.fieldInfo;
// Decoration props
const decorationMap = {};
const { decorations } = fieldInfo;
for (const decoName in decorations) {
const value = evaluateExpr(decorations[decoName], evalContext);
decorationMap[decoName] = value;
}
const modifiers = fieldInfo.modifiers || {};
readonlyFromModifiers = evalDomain(modifiers.readonly, evalContext);
let propsFromAttrs = fieldInfo.propsFromAttrs || {};
if (this.props.attrs) {
const extractProps = this.field.extractProps || (() => ({}));
propsFromAttrs = extractProps.call(this.field, {
attrs: {
...this.props.attrs,
options: evaluateExpr(this.props.attrs.options || "{}"),
},
});
if (this.props.attrs) {
fieldInfo = {
...fieldInfo,
attrs: { ...fieldInfo.attrs, ...this.props.attrs },
};
}
propsFromNode = this.field.extractProps ? this.field.extractProps(fieldInfo) : {};
}
const props = { ...this.props };
@@ -158,11 +140,9 @@ export class Field extends Component {
delete props.type;
return {
...fieldInfo.props,
value: this.props.record.data[this.props.name],
decorations: decorationMap,
readonly: !record.isInEdition || readonlyFromModifiers || false,
...propsFromAttrs,
...propsFromNode,
...props,
};
}
@@ -171,7 +151,7 @@ export class Field extends Component {
if (this.props.showTooltip) {
const tooltip = getTooltipInfo({
field: this.props.record.fields[this.props.name],
fieldInfo: this.props.fieldInfo,
fieldInfo: this.props.fieldInfo || {},
});
if (Boolean(odoo.debug) || (tooltip && JSON.parse(tooltip).field.help)) {
return tooltip;
@@ -187,51 +167,35 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
const widget = node.getAttribute("widget");
const fields = models[modelName];
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
const field = getFieldClassFromRegistry(fields[name].type, widget, viewType, jsClass);
const field = getFieldFromRegistry(fields[name].type, widget, viewType, jsClass);
const fieldInfo = {
name,
viewType,
widget,
modifiers,
field,
context: node.getAttribute("context") || "{}",
string: node.getAttribute("string") || fields[name].string,
help: node.getAttribute("help"),
widget,
modifiers,
onChange: archParseBoolean(node.getAttribute("on_change")),
field,
forceSave: archParseBoolean(node.getAttribute("force_save")),
decorations: {}, // populated below
noLabel: archParseBoolean(node.getAttribute("nolabel")),
props: {},
rawAttrs: {},
options: evaluateExpr(node.getAttribute("options") || "{}"),
alwaysInvisible: modifiers.invisible === true || modifiers.column_invisible === true,
decorations: {}, // populated below
attrs: {}, // populated below
};
if (node.getAttribute("domain")) {
// TODO WOWl: remove with new model?
fieldInfo.domain = node.getAttribute("domain");
}
for (const attribute of node.attributes) {
if (attribute.name in Field.forbiddenAttributeNames) {
throw new Error(Field.forbiddenAttributeNames[attribute.name]);
for (const { name, value } of node.attributes) {
if (name.startsWith("decoration-")) {
// prepare field decorations
fieldInfo.decorations[name.replace("decoration-", "")] = value;
} else if (!name.startsWith("t-att")) {
// all other (non dynamic) attributes
fieldInfo.attrs[name] = value;
}
// prepare field decorations
if (attribute.name.startsWith("decoration-")) {
const decorationName = attribute.name.replace("decoration-", "");
fieldInfo.decorations[decorationName] = attribute.value;
continue;
}
if (!attribute.name.startsWith("t-att")) {
fieldInfo.rawAttrs[attribute.name] = attribute.value;
}
}
if (viewType !== "kanban") {
// FIXME WOWL: find a better solution
const extractProps = field.extractProps || (() => ({}));
fieldInfo.propsFromAttrs = extractProps.call(field, {
attrs: { ...fieldInfo.rawAttrs, options: fieldInfo.options },
});
}
if (X2M_TYPES.includes(fields[name].type)) {
@@ -262,7 +226,6 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
viewMode = viewMode.replace("tree", "list");
}
fieldInfo.viewMode = viewMode;
fieldInfo.relation = fields[name].relation; // not really necessary
fieldInfo.views = views;
let fieldsToFetch = field.fieldsToFetch;
@@ -277,8 +240,5 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
return fieldInfo;
};
Field.forbiddenAttributeNames = {
decorations: `You cannot use the "decorations" attribute name as it is used as generated prop name for the composite decoration-<something> attributes.`,
};
Field.props = ["fieldInfo?", "*"];
Field.defaultProps = { fieldInfo: {}, setDirty: () => {} };
Field.defaultProps = { setDirty: () => {} };
@@ -57,19 +57,19 @@ export const floatField = {
displayName: _lt("Float"),
supportedTypes: ["float"],
isEmpty: () => false,
extractProps: ({ attrs }) => {
extractProps: ({ attrs, options }) => {
// Sadly, digits param was available as an option and an attr.
// The option version could be removed with some xml refactoring.
let digits;
if (attrs.digits) {
digits = JSON.parse(attrs.digits);
} else if (attrs.options.digits) {
digits = attrs.options.digits;
} else if (options.digits) {
digits = options.digits;
}
return {
inputType: attrs.options.type,
step: attrs.options.step,
inputType: options.type,
step: options.step,
digits,
placeholder: attrs.placeholder,
};
@@ -29,9 +29,9 @@ export class FloatFactorField extends FloatField {
export const floatFactorField = {
...floatField,
component: FloatFactorField,
extractProps: (params) => ({
...floatField.extractProps(params),
factor: params.attrs.options.factor,
extractProps: (fieldInfo) => ({
...floatField.extractProps(fieldInfo),
factor: fieldInfo.options.factor,
}),
};
@@ -54,21 +54,21 @@ export const floatToggleField = {
component: FloatToggleField,
supportedTypes: ["float"],
isEmpty: () => false,
extractProps: ({ attrs }) => {
extractProps: ({ attrs, options }) => {
// Sadly, digits param was available as an option and an attr.
// The option version could be removed with some xml refactoring.
let digits;
if (attrs.digits) {
digits = JSON.parse(attrs.digits);
} else if (attrs.options.digits) {
digits = attrs.options.digits;
} else if (options.digits) {
digits = options.digits;
}
return {
digits,
range: attrs.options.range,
factor: attrs.options.factor,
disableReadOnly: attrs.options.force_button || false,
range: options.range,
factor: options.factor,
disableReadOnly: options.force_button || false,
};
},
};
@@ -133,21 +133,14 @@ export const imageField = {
displayName: _lt("Image"),
supportedTypes: ["binary"],
fieldDependencies: [{ name: "write_date", type: "datetime" }],
extractProps: ({ attrs }) => ({
enableZoom: attrs.options.zoom,
zoomDelay: attrs.options.zoom_delay,
previewImage: attrs.options.preview_image,
acceptedFileExtensions: attrs.options.accepted_file_extensions,
width:
attrs.options.size && Boolean(attrs.options.size[0])
? attrs.options.size[0]
: attrs.width,
height:
attrs.options.size && Boolean(attrs.options.size[1])
? attrs.options.size[1]
: attrs.height,
extractProps: ({ attrs, options }) => ({
enableZoom: options.zoom,
zoomDelay: options.zoom_delay,
previewImage: options.preview_image,
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,
}),
};
registry.category("fields").add("image", imageField);
registry.category("fields").add("kanban.image", imageField); // FIXME WOWL: s.t. we don't use the legacy one
@@ -53,12 +53,10 @@ export const imageUrlField = {
component: ImageUrlField,
displayName: _lt("Image"),
supportedTypes: ["char"],
extractProps: ({ attrs }) => ({
width: attrs.options.size ? attrs.options.size[0] : attrs.width,
height: attrs.options.size ? attrs.options.size[1] : attrs.height,
extractProps: ({ attrs, options }) => ({
width: options.size ? options.size[0] : attrs.width,
height: options.size ? options.size[1] : attrs.height,
}),
};
registry.category("fields").add("image_url", imageUrlField);
// TODO WOWL: remove below when old registry is removed.
registry.category("fields").add("kanban.image_url", imageUrlField);
@@ -44,9 +44,9 @@ export const integerField = {
displayName: _lt("Integer"),
supportedTypes: ["integer"],
isEmpty: (record, fieldName) => record.data[fieldName] === false,
extractProps: ({ attrs }) => ({
inputType: attrs.options.type,
step: attrs.options.step,
extractProps: ({ attrs, options }) => ({
inputType: options.type,
step: options.step,
placeholder: attrs.placeholder,
}),
};
@@ -31,8 +31,8 @@ export const labelSelectionField = {
component: LabelSelectionField,
displayName: _lt("Label Selection"),
supportedTypes: ["selection"],
extractProps: ({ attrs }) => ({
classesObj: attrs.options.classes,
extractProps: ({ options }) => ({
classesObj: options.classes,
}),
};
@@ -66,8 +66,8 @@ export const many2ManyBinaryField = {
{ name: "name", type: "char" },
{ name: "mimetype", type: "char" },
],
extractProps: ({ attrs }) => ({
acceptedFileExtensions: attrs.options.accepted_file_extensions,
extractProps: ({ attrs, options }) => ({
acceptedFileExtensions: options.accepted_file_extensions,
className: attrs.class,
}),
};
@@ -250,25 +250,25 @@ export const many2ManyTagsField = {
displayName: _lt("Tags"),
supportedTypes: ["many2many"],
isSet: (value) => value.count > 0,
fieldsToFetch: (fieldInfo) => {
fieldsToFetch: ({ options }) => {
const fieldsToFetch = [{ name: "display_name", type: "char" }];
if (fieldInfo.options.color_field) {
fieldsToFetch.push({ name: fieldInfo.options.color_field, type: "integer" });
if (options.color_field) {
fieldsToFetch.push({ name: options.color_field, type: "integer" });
}
return fieldsToFetch;
},
extractProps: ({ attrs }) => {
const noCreate = Boolean(attrs.options.no_create);
extractProps: ({ attrs, options }) => {
const noCreate = Boolean(options.no_create);
const canCreate = attrs.can_create && Boolean(JSON.parse(attrs.can_create)) && !noCreate;
const noQuickCreate = Boolean(attrs.options.no_quick_create);
const noCreateEdit = Boolean(attrs.options.no_create_edit);
const noQuickCreate = Boolean(options.no_quick_create);
const noCreateEdit = Boolean(options.no_create_edit);
return {
colorField: attrs.options.color_field,
nameCreateField: attrs.options.create_name_field,
colorField: options.color_field,
nameCreateField: options.create_name_field,
canCreate,
canQuickCreate: canCreate && !noQuickCreate,
canCreateEdit: canCreate && !noCreateEdit,
createDomain: attrs.options.create,
createDomain: options.create,
placeholder: attrs.placeholder,
};
},
@@ -359,9 +359,9 @@ export class Many2ManyTagsFieldColorEditable extends Many2ManyTagsField {
export const many2ManyTagsFieldColorEditable = {
...many2ManyTagsField,
component: Many2ManyTagsFieldColorEditable,
extractProps: (params) => ({
...many2ManyTagsField.extractProps(params),
canEditColor: !params.attrs.options.no_edit_color && !!params.attrs.options.color_field,
extractProps: (fieldInfo) => ({
...many2ManyTagsField.extractProps(fieldInfo),
canEditColor: !fieldInfo.options.no_edit_color && !!fieldInfo.options.color_field,
}),
};
@@ -297,20 +297,20 @@ export const many2OneField = {
component: Many2OneField,
displayName: _lt("Many2one"),
supportedTypes: ["many2one"],
extractProps: ({ attrs }) => {
extractProps: ({ attrs, options, string }) => {
const canCreate =
attrs.can_create && Boolean(JSON.parse(attrs.can_create)) && !attrs.options.no_create;
attrs.can_create && Boolean(JSON.parse(attrs.can_create)) && !options.no_create;
return {
placeholder: attrs.placeholder,
canOpen: !attrs.options.no_open,
canOpen: !options.no_open,
canCreate,
canWrite: attrs.can_write && Boolean(JSON.parse(attrs.can_write)),
canQuickCreate: canCreate && !attrs.options.no_quick_create,
canCreateEdit: canCreate && !attrs.options.no_create_edit,
nameCreateField: attrs.options.create_name_field,
canScanBarcode: !!attrs.options.can_scan_barcode,
canQuickCreate: canCreate && !options.no_quick_create,
canCreateEdit: canCreate && !options.no_create_edit,
nameCreateField: options.create_name_field,
canScanBarcode: !!options.can_scan_barcode,
openTarget: attrs.open_target,
string: attrs.string,
string,
};
},
};
@@ -15,8 +15,8 @@ export const many2OneBarcodeField = {
...many2OneField,
component: Many2OneBarcodeField,
displayName: _lt("Many2OneBarcode"),
extractProps: (params) => ({
...many2OneField.extractProps(params),
extractProps: (fieldInfo) => ({
...many2OneField.extractProps(fieldInfo),
canScanBarcode: true,
}),
};
@@ -80,11 +80,11 @@ export const monetaryField = {
component: MonetaryField,
supportedTypes: ["monetary", "float"],
displayName: _lt("Monetary"),
extractProps: ({ attrs }) => ({
currencyField: attrs.options.currency_field,
extractProps: ({ attrs, options }) => ({
currencyField: options.currency_field,
inputType: attrs.type,
useFieldDigits: attrs.options.field_digits,
hideSymbol: attrs.options.no_symbol,
useFieldDigits: options.field_digits,
hideSymbol: options.no_symbol,
placeholder: attrs.placeholder,
}),
};
@@ -89,9 +89,9 @@ export const pdfViewerField = {
component: PdfViewerField,
displayName: _lt("PDF Viewer"),
supportedTypes: ["binary"],
extractProps: ({ attrs }) => ({
extractProps: ({ attrs, options }) => ({
fileNameField: attrs.filename,
previewImage: attrs.options.preview_image,
previewImage: options.preview_image,
}),
};
@@ -28,9 +28,7 @@ export const percentPieField = {
displayName: _lt("PercentPie"),
supportedTypes: ["float", "integer"],
additionalClasses: ["o_field_percent_pie"],
extractProps: ({ attrs }) => ({
string: attrs.string,
}),
extractProps: ({ string }) => ({ string }),
};
registry.category("fields").add("percentpie", percentPieField);
@@ -46,14 +46,14 @@ export const percentageField = {
component: PercentageField,
displayName: _lt("Percentage"),
supportedTypes: ["integer", "float"],
extractProps: ({ attrs }) => {
extractProps: ({ attrs, options }) => {
// Sadly, digits param was available as an option and an attr.
// The option version could be removed with some xml refactoring.
let digits;
if (attrs.digits) {
digits = JSON.parse(attrs.digits);
} else if (attrs.options.digits) {
digits = attrs.options.digits;
} else if (options.digits) {
digits = options.digits;
}
return {
@@ -155,15 +155,14 @@ export const progressBarField = {
component: ProgressBarField,
displayName: _lt("Progress Bar"),
supportedTypes: ["integer", "float"],
extractProps: ({ attrs }) => ({
maxValueField: attrs.options.max_value,
currentValueField: attrs.options.current_value,
isEditable: !attrs.options.readonly && attrs.options.editable,
isEditableInReadonly: attrs.options.editable_readonly,
extractProps: ({ attrs, options }) => ({
maxValueField: options.max_value,
currentValueField: options.current_value,
isEditable: !options.readonly && options.editable,
isEditableInReadonly: options.editable_readonly,
isCurrentValueEditable:
attrs.options.editable &&
(!attrs.options.edit_max_value || attrs.options.edit_current_value),
isMaxValueEditable: attrs.options.editable && attrs.options.edit_max_value,
options.editable && (!options.edit_max_value || options.edit_current_value),
isMaxValueEditable: options.editable && options.edit_max_value,
title: attrs.title,
}),
};
@@ -72,8 +72,8 @@ export const radioField = {
displayName: _lt("Radio"),
supportedTypes: ["many2one", "selection"],
isEmpty: (record, fieldName) => record.data[fieldName] === false,
extractProps: ({ attrs }) => ({
orientation: attrs.options.horizontal ? "horizontal" : "vertical",
extractProps: ({ options }) => ({
orientation: options.horizontal ? "horizontal" : "vertical",
}),
legacySpecialData: "_fetchSpecialMany2ones",
};
@@ -128,7 +128,7 @@ export const referenceField = {
displayName: _lt("Reference"),
supportedTypes: ["reference", "char"],
legacySpecialData: "_fetchSpecialReference",
extractProps: (params) => ({
extractProps: (fieldInfo) => ({
/*
1 - <field name="ref" options="{'model_field': 'model_id'}" />
2 - <field name="ref" options="{'hide_model': True}" />
@@ -137,9 +137,8 @@ export const referenceField = {
We want to display the model selector only in the 4th case.
*/
...many2OneField.extractProps(params),
hideModelSelector:
!!params.attrs.options["hide_model"] || !!params.attrs.options["model_field"],
...many2OneField.extractProps(fieldInfo),
hideModelSelector: !!fieldInfo.options.hide_model || !!fieldInfo.options.model_field,
}),
};
@@ -136,12 +136,12 @@ export class SignatureField extends Component {
export const signatureField = {
component: SignatureField,
extractProps: ({ attrs }) => ({
defaultFont: attrs.options.default_font || "",
fullName: attrs.options.full_name,
height: attrs.options.size ? attrs.options.size[1] || undefined : attrs.height,
previewImage: attrs.options.preview_image,
width: attrs.options.size ? attrs.options.size[0] || undefined : attrs.width,
extractProps: ({ attrs, options }) => ({
defaultFont: options.default_font || "",
fullName: options.full_name,
height: options.size ? options.size[1] || undefined : attrs.height,
previewImage: options.preview_image,
width: options.size ? options.size[0] || undefined : attrs.width,
}),
};
@@ -6,6 +6,5 @@ export const standardFieldProps = {
readonly: { type: Boolean, optional: true },
record: { type: Object },
value: true,
decorations: { type: Object, optional: true },
setDirty: { type: Function, optional: true },
};
@@ -37,19 +37,19 @@ export const statInfoField = {
displayName: _lt("Stat Info"),
supportedTypes: ["float", "integer", "monetary"],
isEmpty: () => false,
extractProps: ({ attrs }) => {
extractProps: ({ attrs, options }) => {
// Sadly, digits param was available as an option and an attr.
// The option version could be removed with some xml refactoring.
let digits;
if (attrs.digits) {
digits = JSON.parse(attrs.digits);
} else if (attrs.options.digits) {
digits = attrs.options.digits;
} else if (options.digits) {
digits = options.digits;
}
return {
digits,
labelField: attrs.options.label_field,
labelField: options.label_field,
noLabel: archParseBoolean(attrs.nolabel),
};
},
@@ -91,8 +91,8 @@ export const stateSelectionField = {
component: StateSelectionField,
displayName: _lt("Label Selection"),
supportedTypes: ["selection"],
extractProps: ({ attrs }) => ({
hideLabel: !!attrs.options.hide_label,
extractProps: ({ options }) => ({
hideLabel: !!options.hide_label,
}),
};
@@ -225,10 +225,10 @@ export const statusBarField = {
supportedTypes: ["many2one", "selection"],
isEmpty: (record, fieldName) => record.model.env.isSmall && !record.data[fieldName],
legacySpecialData: "_fetchSpecialStatus",
extractProps: ({ attrs }) => ({
extractProps: ({ attrs, options }) => ({
canCreate: Boolean(attrs.can_create),
canWrite: Boolean(attrs.can_write),
isDisabled: !attrs.options.clickable,
isDisabled: !options.clickable,
visibleSelection:
attrs.statusbar_visible && attrs.statusbar_visible.trim().split(/\s*,\s*/g),
}),
@@ -129,9 +129,9 @@ export const textField = {
component: TextField,
displayName: _lt("Multiline Text"),
supportedTypes: ["html", "text"],
extractProps: ({ attrs }) => ({
extractProps: ({ attrs, options }) => ({
placeholder: attrs.placeholder,
dynamicPlaceholder: attrs.options.dynamic_placeholder,
dynamicPlaceholder: options.dynamic_placeholder,
rowCount: attrs.rows && parseInteger(attrs.rows),
}),
};
@@ -74,10 +74,10 @@ export const timezoneMismatchField = {
...selectionField,
component: TimezoneMismatchField,
additionalClasses: ["d-flex"],
extractProps: (params) => ({
...selectionField.extractProps(params),
tzOffsetField: params.attrs.options.tz_offset_field,
mismatchTitle: params.attrs.options.mismatch_title,
extractProps: (fieldInfo) => ({
...selectionField.extractProps(fieldInfo),
tzOffsetField: fieldInfo.options.tz_offset_field,
mismatchTitle: fieldInfo.options.mismatch_title,
}),
};
@@ -38,9 +38,9 @@ export const urlField = {
component: UrlField,
displayName: _lt("URL"),
supportedTypes: ["char"],
extractProps: ({ attrs }) => ({
extractProps: ({ attrs, options }) => ({
text: attrs.text,
websitePath: attrs.options.website_path,
websitePath: options.website_path,
placeholder: attrs.placeholder,
}),
};
@@ -116,7 +116,9 @@ export class ListArchParser extends XMLParser {
className: node.getAttribute("class"), // for oe_edit_only and oe_read_only
optional: node.getAttribute("optional") || false,
type: "field",
hasLabel: !(fieldInfo.noLabel || fieldInfo.field.noLabel),
hasLabel: !(
archParseBoolean(fieldInfo.attrs.nolabel) || fieldInfo.field.noLabel
),
label: (fieldInfo.widget && label && label.toString()) || fieldInfo.string,
});
}
@@ -133,7 +135,7 @@ export class ListArchParser extends XMLParser {
name: widgetInfo.name,
// FIXME: this is dumb, we encode it into a weird object so that the widget
// can decode it later...
node: encodeObjectForTemplate({ attrs: widgetInfo.rawAttrs }).slice(1, -1),
node: encodeObjectForTemplate({ attrs: widgetInfo.attrs }).slice(1, -1),
className: node.getAttribute("class") || "",
};
columns.push({
@@ -535,12 +535,12 @@ export class ListRenderer extends Component {
if (type !== "integer" && type !== "float" && type !== "monetary") {
continue;
}
const { rawAttrs, widget } = column;
const { attrs, widget } = column;
const func =
(rawAttrs.sum && "sum") ||
(rawAttrs.avg && "avg") ||
(rawAttrs.max && "max") ||
(rawAttrs.min && "min");
(attrs.sum && "sum") ||
(attrs.avg && "avg") ||
(attrs.max && "max") ||
(attrs.min && "min");
if (func) {
let aggregateValue = 0;
if (func === "max") {
@@ -556,11 +556,11 @@ export class ListRenderer extends Component {
const formatter = formatters.get(widget, false) || formatters.get(type, false);
const formatOptions = {
digits: rawAttrs.digits ? JSON.parse(rawAttrs.digits) : undefined,
digits: attrs.digits ? JSON.parse(attrs.digits) : undefined,
escape: true,
};
aggregates[fieldName] = {
help: rawAttrs[func],
help: attrs[func],
value: formatter ? formatter(aggregateValue, formatOptions) : aggregateValue,
};
}
@@ -569,7 +569,7 @@ export class ListRenderer extends Component {
}
formatAggregateValue(group, column) {
const { widget, rawAttrs } = column;
const { widget, attrs } = column;
const fieldType = this.props.list.fields[column.name].type;
const aggregateValue = group.aggregates[column.name];
if (!(column.name in group.aggregates)) {
@@ -577,7 +577,7 @@ export class ListRenderer extends Component {
}
const formatter = formatters.get(widget, false) || formatters.get(fieldType, false);
const formatOptions = {
digits: rawAttrs.digits ? JSON.parse(rawAttrs.digits) : undefined,
digits: attrs.digits ? JSON.parse(attrs.digits) : undefined,
escape: true,
};
return formatter ? formatter(aggregateValue, formatOptions) : aggregateValue;
@@ -677,12 +677,8 @@ export class ListRenderer extends Component {
classNames.push("o_list_button");
} else if (column.type === "field") {
classNames.push("o_field_cell");
if (
column.rawAttrs &&
column.rawAttrs.class &&
this.canUseFormatter(column, record)
) {
classNames.push(column.rawAttrs.class);
if (column.attrs && column.attrs.class && this.canUseFormatter(column, record)) {
classNames.push(column.attrs.class);
}
const typeClass = FIELD_CLASSES[this.fields[column.name].type];
if (typeClass) {
@@ -743,7 +739,7 @@ export class ListRenderer extends Component {
}
getFieldClass(column) {
return column.rawAttrs && column.rawAttrs.class;
return column.attrs && column.attrs.class;
}
getFormattedValue(column, record) {
@@ -753,8 +749,8 @@ export class ListRenderer extends Component {
const formatOptions = {
escape: false,
data: record.data,
isPassword: "password" in column.rawAttrs,
digits: column.rawAttrs.digits ? JSON.parse(column.rawAttrs.digits) : field.digits,
isPassword: "password" in column.attrs,
digits: column.attrs.digits ? JSON.parse(column.attrs.digits) : field.digits,
field: record.fields[fieldName],
};
return formatter(record.data[fieldName], formatOptions);
@@ -1660,8 +1656,8 @@ export class ListRenderer extends Component {
}
calculateColumnWidth(column) {
if (column.options && column.rawAttrs.width) {
return { type: "absolute", value: column.rawAttrs.width };
if (column.options && column.attrs.width) {
return { type: "absolute", value: column.attrs.width };
}
if (column.type !== "field") {
@@ -69,7 +69,7 @@ const DEFAULT_QUICK_CREATE_VIEW = {
export function isAllowedDateField(groupByField) {
return (
["date", "datetime"].includes(groupByField.type) &&
archParseBoolean(groupByField.rawAttrs.allow_group_range_value)
archParseBoolean(groupByField.attrs.allow_group_range_value)
);
}
@@ -1483,7 +1483,7 @@ class DynamicList extends DataPoint {
}
const [groupByFieldName, granularity] = this.firstGroupBy.split(":");
return {
rawAttrs: {},
attrs: {},
...this.fields[groupByFieldName],
...this.activeFields[groupByFieldName],
granularity: granularity,
+1 -1
View File
@@ -28,7 +28,7 @@ export function addFieldDependencies(activeFields, fields, dependencies = []) {
for (const dependency of dependencies) {
const { name } = dependency;
if (!(name in activeFields)) {
activeFields[name] = Object.assign({ name, rawAttrs: {} }, dependency, {
activeFields[name] = Object.assign({ name, attrs: {} }, dependency, {
modifiers: { invisible: true },
});
}
@@ -5,6 +5,6 @@ export const standardWidgetProps = {
record: { type: Object },
// TODO TO REMOVE - ListView - FGE
options: { type: Object, optional: true },
rawAttrs: { type: Object, optional: true },
attrs: { type: Object, optional: true },
className: { type: String, optional: true },
};
@@ -70,7 +70,7 @@ Widget.parseWidgetNode = function (node) {
return {
options: evaluateExpr(node.getAttribute("options") || "{}"),
name,
rawAttrs: attrs,
attrs,
widget,
};
};
@@ -1,7 +1,9 @@
/** @odoo-module **/
import { uiService } from "@web/core/ui/ui_service";
import { createElement } from "@web/core/utils/xml";
import { registry } from "@web/core/registry";
import { Field } from "@web/views/fields/field";
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
@@ -206,6 +208,8 @@ export const FAKE_FIELDS = {
};
function makeFakeModelState() {
const fakeFieldNode = createElement("field", { name: "name" });
const fakeModels = { event: FAKE_FIELDS };
return {
canCreate: true,
canDelete: true,
@@ -228,7 +232,7 @@ function makeFakeModelState() {
hasEditDialog: false,
hasQuickCreate: false,
popoverFields: {
name: { rawAttrs: {}, options: {} },
name: Field.parseFieldNode(fakeFieldNode, fakeModels, "event", "calendar"),
},
rangeEnd: makeFakeDate().endOf("month"),
rangeStart: makeFakeDate().startOf("month"),
@@ -3566,9 +3566,9 @@ QUnit.module("Views", (hooks) => {
MyField.template = owl.xml`<div>ok</div>`;
fieldRegistry.add("my_field", {
component: MyField,
extractProps: function ({ attrs }) {
assert.deepEqual(attrs.options, { horizontal: true });
return { horizontal: attrs.options.horizontal };
extractProps: function ({ options }) {
assert.deepEqual(options, { horizontal: true });
return { horizontal: options.horizontal };
},
});
@@ -866,7 +866,6 @@ QUnit.module("Views", (hooks) => {
str: "some string",
bool: "true",
num: "4.5",
options: {},
field_id: "int_field",
});
}
@@ -630,49 +630,49 @@ export const htmlField = {
component: HtmlField,
displayName: _lt("Html"),
supportedTypes: ["html"],
extractProps: ({ attrs }) => {
extractProps: ({ attrs, options }) => {
const wysiwygOptions = {
placeholder: attrs.placeholder,
noAttachment: attrs.options['no-attachment'],
inIframe: Boolean(attrs.options.cssEdit),
iframeCssAssets: attrs.options.cssEdit,
noAttachment: options['no-attachment'],
inIframe: Boolean(options.cssEdit),
iframeCssAssets: options.cssEdit,
iframeHtmlClass: attrs.iframeHtmlClass,
snippets: attrs.options.snippets,
snippets: options.snippets,
mediaModalParams: {
noVideos: 'noVideos' in attrs.options ? attrs.options.noVideos : true,
noVideos: 'noVideos' in options ? options.noVideos : true,
useMediaLibrary: true,
},
linkForceNewWindow: true,
tabsize: 0,
height: attrs.options.height,
minHeight: attrs.options.minHeight,
maxHeight: attrs.options.maxHeight,
resizable: 'resizable' in attrs.options ? attrs.options.resizable : false,
height: options.height,
minHeight: options.minHeight,
maxHeight: options.maxHeight,
resizable: 'resizable' in options ? options.resizable : false,
editorPlugins: [QWebPlugin],
};
if ('collaborative' in attrs.options) {
wysiwygOptions.collaborative = attrs.options.collaborative;
if ('collaborative' in options) {
wysiwygOptions.collaborative = options.collaborative;
}
if ('allowCommandImage' in attrs.options) {
if ('allowCommandImage' in options) {
// Set the option only if it is explicitly set in the view so a default
// can be set elsewhere otherwise.
wysiwygOptions.allowCommandImage = Boolean(attrs.options.allowCommandImage);
wysiwygOptions.allowCommandImage = Boolean(options.allowCommandImage);
}
if ('allowCommandVideo' in attrs.options) {
if ('allowCommandVideo' in options) {
// Set the option only if it is explicitly set in the view so a default
// can be set elsewhere otherwise.
wysiwygOptions.allowCommandVideo = Boolean(attrs.options.allowCommandVideo);
wysiwygOptions.allowCommandVideo = Boolean(options.allowCommandVideo);
}
return {
codeview: Boolean(odoo.debug && attrs.options.codeview),
codeview: Boolean(odoo.debug && options.codeview),
placeholder: attrs.placeholder,
isCollaborative: attrs.options.collaborative,
cssReadonlyAssetId: attrs.options.cssReadonly,
dynamicPlaceholder: attrs.options.dynamic_placeholder,
cssEditAssetId: attrs.options.cssEdit,
isInlineStyle: attrs.options['style-inline'],
wrapper: attrs.options.wrapper,
isCollaborative: options.collaborative,
cssReadonlyAssetId: options.cssReadonly,
dynamicPlaceholder: options.dynamic_placeholder,
cssEditAssetId: options.cssEdit,
isInlineStyle: options['style-inline'],
wrapper: options.wrapper,
wysiwygOptions,
};
@@ -94,9 +94,9 @@ GaugeField.props = {
export const gaugeField = {
component: GaugeField,
extractProps: ({ attrs }) => ({
maxValueField: attrs.options.max_field,
title: attrs.options.title,
extractProps: ({ options }) => ({
maxValueField: options.max_field,
title: options.title,
}),
};
@@ -99,8 +99,8 @@ ImageRadioField.props = {
export const imageRadioField = {
component: ImageRadioField,
supportedTypes: ['selection'],
extractProps: ({ attrs }) => ({
images: attrs.options.images,
extractProps: ({ options }) => ({
images: options.images,
}),
};