[REF] web: change field.extractProps API

Before this commit, the record was given to `field.extractProps`
so it could return props based on the record.
Now, only the attrs and field is given so it can be called
only once when parsing the arch (except for kanban).

closes odoo/odoo#95389

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Michael (mcm)
2022-07-08 14:33:52 +02:00
parent 5ccc32fcf7
commit d85609501d
81 changed files with 631 additions and 663 deletions
@@ -320,6 +320,7 @@ DatePicker.props = {
useCurrent: { type: Boolean, optional: true },
widgetParent: { type: String, optional: true },
daysOfWeekDisabled: { type: Array, optional: true },
placeholder: { type: String, optional: true },
};
DatePicker.template = "web.DatePicker";
@@ -101,7 +101,7 @@ AceField.defaultProps = {
AceField.displayName = _lt("Ace Editor");
AceField.supportedTypes = ["text"];
AceField.extractProps = (fieldName, record, attrs) => {
AceField.extractProps = ({ attrs }) => {
return {
mode: attrs.options.mode,
};
@@ -7,13 +7,27 @@ import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
export class BadgeSelectionField extends Component {
get options() {
switch (this.props.record.fields[this.props.name].type) {
case "many2one":
// WOWL: conversion needed while we keep using the legacy model
return Object.values(this.props.record.preloadedData[this.props.name]).map((v) => {
return [v.id, v.display_name];
});
case "selection":
return this.props.record.fields[this.props.name].selection;
default:
return [];
}
}
get string() {
switch (this.props.type) {
case "many2one":
return this.props.value ? this.props.value[1] : "";
case "selection":
return this.props.value !== false
? this.props.options.find((o) => o[0] === this.props.value)[1]
? this.options.find((o) => o[0] === this.props.value)[1]
: "";
default:
return "";
@@ -37,7 +51,7 @@ export class BadgeSelectionField extends Component {
if (value === false) {
this.props.update(false);
} else {
this.props.update(this.props.options.find((option) => option[0] === value));
this.props.update(this.options.find((option) => option[0] === value));
}
break;
case "selection":
@@ -50,33 +64,12 @@ export class BadgeSelectionField extends Component {
BadgeSelectionField.template = "web.BadgeSelectionField";
BadgeSelectionField.props = {
...standardFieldProps,
horizontal: { type: Boolean, optional: true },
options: Object,
placeholder: { type: String, optional: true },
};
BadgeSelectionField.displayName = _lt("Badges");
BadgeSelectionField.supportedTypes = ["many2one", "selection"];
BadgeSelectionField.isEmpty = (record, fieldName) => record.data[fieldName] === false;
BadgeSelectionField.extractProps = (fieldName, record, attrs) => {
const getOptions = () => {
switch (record.fields[fieldName].type) {
case "many2one":
// WOWL: conversion needed while we keep using the legacy model
return Object.values(record.preloadedData[fieldName]).map((v) => {
return [v.id, v.display_name];
});
case "selection":
return record.fields[fieldName].selection;
default:
return [];
}
};
return {
options: getOptions(),
};
};
registry.category("fields").add("selection_badge", BadgeSelectionField);
@@ -6,9 +6,10 @@
<span t-esc="string" t-att-raw-value="value" />
</t>
<t t-else="">
<t t-foreach="props.options" t-as="option" t-key="option[0]">
<t t-foreach="options" t-as="option" t-key="option[0]">
<span
t-attf-class="o_selection_badge {{value === option[0] ? 'active' : ''}}"
class="o_selection_badge"
t-att-class="{ active: value === option[0] }"
t-att-value="stringify(option[0])"
t-esc="option[1]"
t-on-click="() => this.onChange(option[0])"
@@ -17,7 +17,7 @@ export class BinaryField extends Component {
setup() {
this.notification = useService("notification");
this.state = useState({
fileName: this.props.fileName || "",
fileName: this.fileName || "",
isValid: true,
});
onWillUpdateProps((nextProps) => {
@@ -26,34 +26,52 @@ export class BinaryField extends Component {
}
});
}
get fileName() {
return this.props.record.data[this.props.fileNameField];
}
get file() {
return {
data: this.props.fileData,
name: this.state.fileName || this.props.fileData || null,
data: this.props.value,
name: this.state.fileName || this.props.value || null,
};
}
get isDownloadable() {
return !(
this.props.record.isReadonly(this.props.name) && this.props.record.mode === "edit"
);
}
update(file) {
const changes = { [this.props.name]: file.data || false };
if (this.props.fileNameField && this.props.fileNameField !== this.props.name) {
changes[this.props.fileNameField] = file.name || false;
}
return this.props.record.update(changes);
}
async onFileDownload() {
await download({
data: {
model: this.props.resModel,
id: this.props.resId,
model: this.props.record.resModel,
id: this.props.record.resId,
field: this.props.name,
filename_field: this.file.name,
filename: this.file.name || "",
download: true,
data: isBinarySize(this.props.fileData) ? null : this.props.fileData,
data: isBinarySize(this.props.value) ? null : this.props.value,
},
url: "/web/content",
});
}
onFileRemove() {
this.state.isValid = true;
this.props.update(false);
this.update(false);
}
onFileUploaded(file) {
this.state.fileName = file.name;
this.state.isValid = true;
this.props.update(file);
this.update(file);
}
onLoadFailed() {
this.state.isValid = false;
@@ -71,36 +89,19 @@ BinaryField.components = {
BinaryField.props = {
...standardFieldProps,
acceptedFileExtensions: { type: String, optional: true },
fileData: { type: String, optional: true },
fileName: { type: String, optional: true },
isDownloadable: { type: Boolean, optional: true },
resId: { type: [Number, Boolean], optional: true },
resModel: { type: String, optional: true },
fileNameField: { type: String, optional: true },
};
BinaryField.defaultProps = {
acceptedFileExtensions: "*",
fileData: "",
isDownloadable: true,
};
BinaryField.displayName = _lt("File");
BinaryField.supportedTypes = ["binary"];
BinaryField.extractProps = (fieldName, record, attrs) => {
BinaryField.extractProps = ({ attrs }) => {
return {
acceptedFileExtensions: attrs.options.accepted_file_extensions,
fileData: record.data[fieldName] || "",
fileName: record.data[attrs.filename] || "",
isDownloadable: !(record.isReadonly(fieldName) && record.mode === "edit"),
resId: record.resId,
resModel: record.resModel,
update: (file) => {
const changes = { [fieldName]: file.data || false };
if (attrs.filename && attrs.filename !== fieldName) {
changes[attrs.filename] = file.name || false;
}
return record.update(changes);
},
fileNameField: attrs.filename,
};
};
@@ -38,7 +38,7 @@
</label>
</t>
</t>
<t t-elif="file.data and props.isDownloadable">
<t t-elif="file.data and isDownloadable">
<FileDownloader file="file" onDownload.bind="onFileDownload"/>
</t>
</t>
@@ -8,6 +8,10 @@ import { CheckBox } from "@web/core/checkbox/checkbox";
const { Component } = owl;
export class BooleanField extends Component {
get isReadonly() {
return !(this.props.record.isInEdition && !this.props.record.isReadonly(this.props.name));
}
/**
* @param {boolean} newValue
*/
@@ -20,20 +24,11 @@ BooleanField.template = "web.BooleanField";
BooleanField.components = { CheckBox };
BooleanField.props = {
...standardFieldProps,
isReadonlyEnabled: { type: Boolean, optional: true },
};
BooleanField.displayName = _lt("Checkbox");
BooleanField.supportedTypes = ["boolean"];
BooleanField.isEmpty = () => false;
BooleanField.extractProps = (fieldName, record) => {
return {
// should we make a list specialization?
readonly: !(record.isInEdition && !record.isReadonly(fieldName)),
isReadonlyEnabled:
record.activeFields[fieldName].viewType === "list" && !record.isReadonly(fieldName),
};
};
registry.category("fields").add("boolean", BooleanField);
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.BooleanField" owl="1">
<CheckBox id="props.id" value="props.value or false" disabled="props.readonly" onChange.bind="onChange" />
<CheckBox id="props.id" value="props.value or false" disabled="isReadonly" onChange.bind="onChange" />
</t>
</templates>
@@ -22,7 +22,7 @@ BooleanFavoriteField.displayName = _lt("Favorite");
BooleanFavoriteField.supportedTypes = ["boolean"];
BooleanFavoriteField.isEmpty = () => false;
BooleanFavoriteField.extractProps = (fieldName, record, attrs) => {
BooleanFavoriteField.extractProps = ({ attrs }) => {
return {
noLabel: archParseBoolean(attrs.nolabel),
};
@@ -7,7 +7,11 @@ import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
export class BooleanToggleField extends Component {}
export class BooleanToggleField extends Component {
get isReadonly() {
return this.props.record.isReadonly(this.props.name);
}
}
BooleanToggleField.template = "web.BooleanToggleField";
BooleanToggleField.components = { CheckBox };
@@ -19,10 +23,5 @@ BooleanToggleField.displayName = _lt("Toggle");
BooleanToggleField.supportedTypes = ["boolean"];
BooleanToggleField.isEmpty = () => false;
BooleanToggleField.extractProps = (fieldName, record) => {
return {
readonly: record.isReadonly(fieldName),
};
};
registry.category("fields").add("boolean_toggle", BooleanToggleField);
@@ -6,7 +6,7 @@
className="'o_field_boolean o_boolean_toggle'"
id="props.id"
value="props.value or false"
disabled="props.readonly"
disabled="isReadonly"
onChange="props.update"
>
&#8203; <!-- Zero width space needed to set height -->
@@ -3,9 +3,7 @@
import { registry } from "@web/core/registry";
import { BooleanToggleField } from "./boolean_toggle_field";
const { Component } = owl;
export class ListBooleanToggleField extends Component {
export class ListBooleanToggleField extends BooleanToggleField {
onClick() {
if (!this.props.readonly) {
this.props.update(!this.props.value);
@@ -14,6 +12,5 @@ export class ListBooleanToggleField extends Component {
}
ListBooleanToggleField.template = "web.ListBooleanToggleField";
ListBooleanToggleField.components = { BooleanToggleField };
registry.category("fields").add("list.boolean_toggle", ListBooleanToggleField);
@@ -3,7 +3,7 @@
<t t-name="web.ListBooleanToggleField" owl="1">
<div t-on-click="onClick">
<BooleanToggleField t-props="props" readonly="false" />
<t t-call="web.BooleanToggleField" />
</div>
</t>
@@ -39,20 +39,16 @@ CharField.props = {
shouldTrim: { type: Boolean, optional: true },
maxLength: { type: Number, optional: true },
isTranslatable: { type: Boolean, optional: true },
resId: { type: [Number, Boolean], optional: true },
resModel: { type: String, optional: true },
};
CharField.displayName = _lt("Text");
CharField.supportedTypes = ["char"];
CharField.extractProps = (fieldName, record, attrs) => {
CharField.extractProps = ({ attrs, field }) => {
return {
shouldTrim: record.fields[fieldName].trim,
maxLength: record.fields[fieldName].size,
isTranslatable: record.fields[fieldName].translate,
resId: record.resId,
resModel: record.resModel,
shouldTrim: field.trim,
maxLength: field.size,
isTranslatable: field.translate,
autocomplete: attrs.autocomplete,
isPassword: archParseBoolean(attrs.password),
placeholder: attrs.placeholder,
@@ -19,8 +19,8 @@
<t t-if="props.isTranslatable">
<TranslationButton
fieldName="props.name"
resId="props.resId"
resModel="props.resModel"
resId="props.record.resId"
resModel="props.record.resModel"
value="props.value"
updateField="props.update"
/>
@@ -11,17 +11,17 @@ export class ColorField extends Component {
color: this.props.value || "#000000",
});
}
get isReadonly() {
return this.props.record.isReadonly(this.props.name);
}
}
ColorField.template = "web.ColorField";
ColorField.props = {
...standardFieldProps,
};
ColorField.extractProps = (fieldName, record) => {
return {
readonly: record.isReadonly(fieldName),
};
};
ColorField.supportedTypes = ["char"];
registry.category("fields").add("color", ColorField);
@@ -2,8 +2,8 @@
<templates xml:space="preserve">
<t t-name="web.ColorField" owl="1">
<div class="o_field_color d-flex" t-att-class="{ 'o_field_cursor_disabled': props.readonly }" t-attf-style="background-color: {{state.color}}">
<input t-on-click.stop="" class="w-100 h-100 opacity-0" type="color" t-att-value="props.value" t-att-disabled="props.readonly" t-on-input="(ev) => this.state.color = ev.target.value" t-on-change="(ev) => this.props.update(ev.target.value)" />
<div class="o_field_color d-flex" t-att-class="{ 'o_field_cursor_disabled': readonly }" t-attf-style="background-color: {{state.color}}">
<input t-on-click.stop="" class="w-100 h-100 opacity-0" type="color" t-att-value="props.value" t-att-disabled="readonly" t-on-input="(ev) => this.state.color = ev.target.value" t-on-change="(ev) => this.props.update(ev.target.value)" />
</div>
</t>
@@ -19,6 +19,7 @@ ColorPickerField.components = {
ColorPickerField.props = {
...standardFieldProps,
};
ColorPickerField.supportedTypes = ["integer"];
ColorPickerField.RECORD_COLORS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
@@ -9,6 +9,9 @@ import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
export class DateField extends Component {
get isDateTime() {
return this.props.record.fields[this.props.name].type === "datetime";
}
get date() {
return this.props.value && this.props.value.startOf("day");
}
@@ -16,7 +19,7 @@ export class DateField extends Component {
get formattedValue() {
return formatDate(this.props.value, {
// get local date if field type is datetime
timezone: this.props.isDateTime,
timezone: this.isDateTime,
});
}
@@ -33,21 +36,20 @@ DateField.components = {
};
DateField.props = {
...standardFieldProps,
isDateTime: { type: Boolean, optional: true },
pickerOptions: { type: Object, optional: true },
placeholder: { type: String, optional: true },
};
DateField.defaultProps = {
isDateTime: false,
pickerOptions: {},
};
DateField.displayName = _lt("Date");
DateField.supportedTypes = ["date", "datetime"];
DateField.extractProps = (fieldName, record, attrs) => {
DateField.extractProps = ({ attrs }) => {
return {
isDateTime: record.fields[fieldName].type === "datetime",
pickerOptions: attrs.options.datepicker,
placeholder: attrs.placeholder,
};
};
@@ -9,6 +9,7 @@
<DatePicker
t-props="props.pickerOptions"
date="date"
placeholder="props.placeholder"
onDateTimeChanged="(date) => this.onDateTimeChanged(date)"
/>
</t>
@@ -4,6 +4,7 @@ import { localization } from "@web/core/l10n/localization";
import { registry } from "@web/core/registry";
import { loadJS } from "@web/core/assets";
import { useService } from "@web/core/utils/hooks";
import { standardFieldProps } from "../standard_field_props";
const { Component, onWillStart, useExternalListener, useRef, useEffect } = owl;
const formatters = registry.category("formatters");
@@ -61,13 +62,22 @@ export class DateRangeField extends Component {
get formattedValue() {
return this.formatValue(this.props.formatType, this.props.value);
}
get formattedEndDate() {
return this.formatValue(this.props.formatType, this.props.endDate);
return this.formatValue(this.props.formatType, this.endDate);
}
get formattedStartDate() {
return this.formatValue(this.props.formatType, this.props.startDate);
return this.formatValue(this.props.formatType, this.startDate);
}
get startDate() {
return this.props.record.data[this.props.relatedStartDateField || this.props.name];
}
get endDate() {
return this.props.record.data[this.props.relatedEndDateField || this.props.name];
}
get relatedDateRangeField() {
return this.props.relatedStartDateField
? this.props.relatedStartDateField
: this.props.relatedEndDateField;
}
formatValue(format, value) {
@@ -76,18 +86,25 @@ export class DateRangeField extends Component {
try {
formattedValue = formatter(value, { timezone: this.isDateTime });
} catch {
this.props.invalidate();
this.props.record.setInvalidField(this.props.name);
}
return formattedValue;
}
updateRange(start, end) {
return this.props.record.update({
[this.props.relatedStartDateField || this.props.name]: start,
[this.props.relatedEndDateField || this.props.name]: end,
});
}
onChangeInput(ev) {
const parse = parsers.get(this.props.formatType);
let value;
try {
value = parse(ev.target.value, { timezone: this.isDateTime });
} catch {
this.props.invalidate();
this.props.record.setInvalidField(this.props.name);
return;
}
this.props.update(value);
@@ -114,9 +131,9 @@ export class DateRangeField extends Component {
timezone: this.isDateTime,
});
});
await this.props.updateRange(dates[0], dates[1]);
await this.updateRange(dates[0], dates[1]);
const input = document.querySelector(
`.o_field_daterange[name='${this.props.relatedDateRange}'] input`
`.o_field_daterange[name='${this.relatedDateRangeField}'] input`
);
const target = window.$(input).data("daterangepicker");
target.setStartDate(picker.startDate);
@@ -130,25 +147,22 @@ export class DateRangeField extends Component {
}
}
DateRangeField.template = "web.DateRangeField";
DateRangeField.props = {
...standardFieldProps,
relatedEndDateField: { type: String, optional: true },
relatedStartDateField: { type: String, optional: true },
formatType: { type: String, optional: true },
placeholder: { type: String, optional: true },
};
DateRangeField.supportedTypes = ["date", "datetime"];
DateRangeField.extractProps = (fieldName, record, attrs) => {
const relatedEndDate = attrs.options.related_end_date;
const relatedStartDate = attrs.options.related_start_date;
DateRangeField.extractProps = ({ attrs, field }) => {
return {
relatedDateRange: relatedEndDate ? relatedEndDate : relatedStartDate,
endDate: record.data[relatedEndDate || fieldName],
formatType: attrs.options.format_type || record.fields[fieldName].type,
invalidate: () => record.setInvalidField(fieldName),
startDate: record.data[relatedStartDate || fieldName],
async updateRange(start, end) {
await record.update({
[relatedStartDate || fieldName]: start,
[relatedEndDate || fieldName]: end,
});
},
relatedEndDateField: attrs.options.related_end_date,
relatedStartDateField: attrs.options.related_start_date,
formatType: attrs.options.format_type || field.type,
placeholder: attrs.placeholder,
};
};
@@ -6,7 +6,7 @@
<t t-esc="formattedValue" />
</t>
<t t-else="">
<input t-ref="root" type="text" t-att-value="formattedValue" t-on-change="onChangeInput" />
<input t-ref="root" type="text" t-att-value="formattedValue" t-att-placeholder="props.placeholder" t-on-change="onChangeInput" />
</t>
</t>
@@ -27,6 +27,7 @@ DateTimeField.components = {
DateTimeField.props = {
...standardFieldProps,
pickerOptions: { type: Object, optional: true },
placeholder: { type: String, optional: true },
};
DateTimeField.defaultProps = {
pickerOptions: {},
@@ -35,9 +36,10 @@ DateTimeField.defaultProps = {
DateTimeField.displayName = _lt("Date & Time");
DateTimeField.supportedTypes = ["datetime"];
DateTimeField.extractProps = (fieldName, record, attrs) => {
DateTimeField.extractProps = ({ attrs }) => {
return {
pickerOptions: attrs.options.datepicker,
placeholder: attrs.placeholder,
};
};
@@ -9,6 +9,7 @@
<DateTimePicker
t-props="props.pickerOptions"
date="props.value"
placeholder="props.placeholder"
onDateTimeChanged="(datetime) => this.onDateTimeChanged(datetime)"
/>
</t>
@@ -40,20 +40,31 @@ export class DomainField extends Component {
ev.detail.proms.push(prom);
await prom;
if (!this.state.isValid) {
this.props.invalidate();
this.props.record.setInvalidField(this.props.name);
}
}
});
}
getContext(p) {
return p.record.getFieldContext(p.name);
}
getResModel(p) {
let resModel = p.resModel;
if (p.record.fieldNames.includes(resModel)) {
resModel = p.record.data[resModel];
}
return resModel;
}
onButtonClick() {
this.dialog.add(SelectCreateDialog, {
title: this.env._t("Selected records"),
noCreate: true,
multiSelect: false,
resModel: this.props.resModel,
domain: this.getDomain(this.props.value).toList(this.props.context) || [],
context: this.props.context || {},
resModel: this.getResModel(this.props),
domain: this.getDomain(this.props.value).toList(this.getContext(this.props)) || [],
context: this.getContext(this.props) || {},
});
}
get isValidDomain() {
@@ -70,16 +81,19 @@ export class DomainField extends Component {
return new Domain(value || "[]");
}
async loadCount(props) {
if (!props.resModel) {
if (!this.getResModel(props)) {
Object.assign(this.state, { recordCount: 0, isValid: true });
}
let recordCount;
try {
const domain = this.getDomain(props.value).toList(props.context);
recordCount = await this.orm.silent.call(props.resModel, "search_count", [domain], {
context: props.context,
});
const domain = this.getDomain(props.value).toList(this.getContext(props));
recordCount = await this.orm.silent.call(
this.getResModel(props),
"search_count",
[domain],
{ context: this.getContext(props) }
);
} catch (_e) {
// WOWL TODO: rethrow error when not the expected type
Object.assign(this.state, { recordCount: 0, isValid: false });
@@ -100,29 +114,16 @@ DomainField.components = {
};
DomainField.props = {
...standardFieldProps,
context: { type: Object, optional: true },
invalidate: { type: Function, optional: true },
resModel: { type: String, optional: true },
};
DomainField.defaultProps = {
context: {},
invalidate: () => {},
};
DomainField.displayName = _lt("Domain");
DomainField.supportedTypes = ["char"];
DomainField.isEmpty = () => false;
DomainField.extractProps = (fieldName, record, attrs) => {
let resModel = attrs.options.model;
if (record.fieldNames.includes(resModel)) {
resModel = record.data[resModel];
}
DomainField.extractProps = ({ attrs }) => {
return {
context: record.getFieldContext(fieldName),
invalidate: () => record.setInvalidField(fieldName),
resModel,
resModel: attrs.options.model,
};
};
@@ -3,9 +3,9 @@
<t t-name="web.DomainField" owl="1">
<div t-att-class="{ o_inline_mode: !env.inDialog }">
<t t-if="props.resModel">
<t t-if="getResModel(props)">
<DomainSelector
resModel="props.resModel"
resModel="getResModel(props)"
value="displayedDomain || '[]'"
readonly="props.readonly"
update.bind="update"
@@ -18,7 +18,7 @@ EmailField.props = {
...standardFieldProps,
placeholder: { type: String, optional: true },
};
EmailField.extractProps = (fieldName, record, attrs) => {
EmailField.extractProps = ({ attrs }) => {
return {
placeholder: attrs.placeholder,
};
+21 -6
View File
@@ -132,12 +132,16 @@ export class Field extends Component {
decorationMap[decoName] = value;
}
let propsFromAttrs = {};
if (this.FieldComponent.extractProps) {
const attrs = Object.assign({}, this.props.attrs || fieldInfo.rawAttrs, {
options: fieldInfo.options || {},
let propsFromAttrs = fieldInfo.propsFromAttrs;
if (this.props.attrs) {
const extractProps = this.FieldComponent.extractProps || (() => ({}));
propsFromAttrs = extractProps({
field,
attrs: {
...this.props.attrs,
options: evaluateExpr(this.props.attrs.options || "{}"),
},
});
propsFromAttrs = this.FieldComponent.extractProps(this.props.name, record, attrs);
}
const props = { ...this.props };
@@ -221,7 +225,18 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
continue;
}
fieldInfo.rawAttrs[attribute.name] = attribute.value;
if (!attribute.name.startsWith("t-att")) {
fieldInfo.rawAttrs[attribute.name] = attribute.value;
}
}
if (viewType !== "kanban") {
// FIXME WOWL: find a better solution
const extractProps = fieldInfo.FieldComponent.extractProps || (() => ({}));
fieldInfo.propsFromAttrs = extractProps({
field,
attrs: { ...fieldInfo.rawAttrs, options: fieldInfo.options },
});
}
if (fieldInfo.modifiers.invisible !== true && X2M_TYPES.includes(field.type)) {
@@ -52,28 +52,23 @@ FloatField.props = {
inputType: { type: String, optional: true },
step: { type: Number, optional: true },
digits: { type: Array, optional: true },
invalidate: { type: Function, optional: true },
placeholder: { type: String, optional: true },
};
FloatField.defaultProps = {
inputType: "text",
invalidate: () => {},
};
FloatField.displayName = _lt("Float");
FloatField.supportedTypes = ["float"];
FloatField.isEmpty = () => false;
FloatField.extractProps = (fieldName, record, attrs) => {
FloatField.extractProps = ({ attrs, field }) => {
return {
invalidate: () => record.setInvalidField(fieldName),
inputType: attrs.options.type,
step: attrs.options.step,
// Sadly, digits param was available as an option and an attr.
// The option version could be removed with some xml refactoring.
digits:
(attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) ||
record.fields[fieldName].digits,
digits: (attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) || field.digits,
placeholder: attrs.placeholder,
};
};
@@ -2,7 +2,6 @@
import { registry } from "@web/core/registry";
import { FloatField } from "../float/float_field";
import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
export class FloatFactorField extends Component {
@@ -20,26 +19,20 @@ export class FloatFactorField extends Component {
FloatFactorField.template = "web.FloatFactorField";
FloatFactorField.components = { FloatField };
FloatFactorField.props = {
...standardFieldProps,
inputType: { type: String, optional: true },
digits: { type: Array, optional: true },
invalidate: { type: Function, optional: true },
...FloatField.props,
factor: { type: Number, optional: true },
};
FloatFactorField.defaultProps = {
inputType: "text",
invalidate: () => {},
...FloatField.defaultProps,
factor: 1,
};
FloatFactorField.supportedTypes = ["float"];
FloatFactorField.isEmpty = () => false;
FloatFactorField.extractProps = (fieldName, record, attrs) => {
FloatFactorField.extractProps = ({ attrs, field }) => {
return {
invalidate: record.setInvalidField.bind(record),
inputType: attrs.options.type,
digits: attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits,
...FloatField.extractProps({ attrs, field }),
factor: attrs.options.factor,
};
};
@@ -28,20 +28,15 @@ export class FloatTimeField extends Component {
FloatTimeField.template = "web.FloatTimeField";
FloatTimeField.props = {
...standardFieldProps,
invalidate: { type: Function, optional: true },
placeholder: { type: String, optional: true },
};
FloatTimeField.defaultProps = {
invalidate: () => {},
};
FloatTimeField.displayName = _lt("Time");
FloatTimeField.supportedTypes = ["float"];
FloatTimeField.isEmpty = () => false;
FloatTimeField.extractProps = (fieldName, record, attrs) => {
FloatTimeField.extractProps = ({ attrs }) => {
return {
invalidate: () => record.setInvalidField(fieldName),
placeholder: attrs.placeholder,
};
};
@@ -42,11 +42,9 @@ FloatToggleField.defaultProps = {
FloatToggleField.supportedTypes = ["float"];
FloatToggleField.isEmpty = () => false;
FloatToggleField.extractProps = (fieldName, record, attrs) => {
FloatToggleField.extractProps = ({ attrs, field }) => {
return {
digits:
(attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) ||
record.fields[fieldName].digits,
digits: (attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) || field.digits,
range: attrs.options.range,
factor: attrs.options.factor,
disableReadOnly: attrs.options.force_button || false,
@@ -8,8 +8,14 @@ import { formatSelection } from "../formatters";
const { Component } = owl;
export class FontSelectionField extends Component {
get options() {
return Array.from(this.props.record.fields[this.props.name].selection);
}
get isRequired() {
return this.props.record.isRequired(this.props.name);
}
get string() {
return formatSelection(this.props.value, { selection: this.props.options });
return formatSelection(this.props.value, { selection: this.options });
}
stringify(value) {
@@ -28,21 +34,14 @@ export class FontSelectionField extends Component {
FontSelectionField.template = "web.FontSelectionField";
FontSelectionField.props = {
...standardFieldProps,
options: Object,
placeholder: { type: String, optional: true },
required: { type: Boolean, optional: true },
};
FontSelectionField.defaultProps = {
required: false,
};
FontSelectionField.displayName = _lt("Font Selection");
FontSelectionField.supportedTypes = ["selection"];
FontSelectionField.extractProps = (fieldName, record, attrs) => {
FontSelectionField.extractProps = ({ attrs }) => {
return {
options: Array.from(record.fields[fieldName].selection),
required: record.isRequired(fieldName),
placeholder: attrs.placeholder,
};
};
@@ -3,23 +3,23 @@
<t t-name="web.FontSelectionField" owl="1">
<t t-if="props.readonly">
<span t-esc="string" t-att-raw-value="props.value" t-attf-style="font-family:{{ props.value }}"/>
<span t-esc="string" t-att-raw-value="props.value" t-attf-style="font-family:{{ props.value }};"/>
</t>
<t t-else="">
<select class="o_input" t-on-change="onChange" t-attf-style="font-family:{{ props.value }}">
<t t-if="!props.required">
<select class="o_input" t-on-change="onChange" t-attf-style="font-family:{{ props.value }};">
<t t-if="!isRequired">
<option
value="false"
t-att-selected="!props.value"
t-esc="props.placeholder"
/>
</t>
<t t-foreach="props.options" t-as="option" t-key="option[0]">
<t t-foreach="options" t-as="option" t-key="option[0]">
<option
t-att-selected="option[0] === value"
t-att-value="stringify(option[0])"
t-esc="option[1]"
t-attf-style="font-family:{{ option[1] }}"
t-attf-style="font-family:{{ option[1] }};"
/>
</t>
</select>
@@ -53,8 +53,8 @@ export class ImageField extends Component {
if (this.state.isValid && this.props.value) {
if (previewFieldName !== this.props.name || isBinarySize(this.props.value)) {
return url("/web/image", {
model: this.props.resModel,
id: this.props.resId,
model: this.props.record.resModel,
id: this.props.record.resId,
field: previewFieldName,
});
} else {
@@ -93,8 +93,6 @@ ImageField.props = {
acceptedFileExtensions: { type: String, optional: true },
width: { type: Number, optional: true },
height: { type: Number, optional: true },
resId: { type: [Number, Boolean], optional: true },
resModel: { type: String, optional: true },
};
ImageField.defaultProps = {
acceptedFileExtensions: "image/*",
@@ -103,7 +101,7 @@ ImageField.defaultProps = {
ImageField.displayName = _lt("Image");
ImageField.supportedTypes = ["binary"];
ImageField.extractProps = (fieldName, record, attrs) => {
ImageField.extractProps = ({ attrs }) => {
return {
enableZoom: attrs.options.zoom,
zoomDelay: attrs.options.zoom_delay,
@@ -111,8 +109,6 @@ ImageField.extractProps = (fieldName, record, attrs) => {
acceptedFileExtensions: attrs.options.accepted_file_extensions,
width: attrs.options.size ? attrs.options.size[0] : attrs.width,
height: attrs.options.size ? attrs.options.size[1] : attrs.height,
resId: record.resId,
resModel: record.resModel,
};
};
@@ -52,7 +52,7 @@ ImageUrlField.props = {
ImageUrlField.displayName = _lt("Image");
ImageUrlField.supportedTypes = ["char"];
ImageUrlField.extractProps = (fieldName, record, attrs) => {
ImageUrlField.extractProps = ({ attrs }) => {
return {
width: attrs.options.size ? attrs.options.size[0] : attrs.width,
height: attrs.options.size ? attrs.options.size[1] : attrs.height,
@@ -33,7 +33,7 @@ export function useInputField(params) {
const val = params.parse ? params.parse(ev.target.value) : ev.target.value;
component.props.update(val);
} catch (_e) {
component.props.invalidate();
component.props.record.setInvalidField(component.props.name);
}
if (component.props.setDirty) {
component.props.setDirty(isDirty);
@@ -73,7 +73,7 @@ export function useInputField(params) {
if (urgent) {
return;
} else {
component.props.invalidate();
component.props.record.setInvalidField(component.props.name);
}
}
if (component.props.setDirty) {
@@ -33,23 +33,20 @@ IntegerField.props = {
...standardFieldProps,
inputType: { type: String, optional: true },
step: { type: Number, optional: true },
invalidate: { type: Function, optional: true },
placeholder: { type: String, optional: true },
};
IntegerField.defaultProps = {
inputType: "text",
invalidate: () => {},
};
IntegerField.displayName = _lt("Integer");
IntegerField.supportedTypes = ["integer"];
IntegerField.isEmpty = (record, fieldName) => (record.data[fieldName] === false ? true : false);
IntegerField.extractProps = (fieldName, record, attrs) => {
IntegerField.extractProps = ({ attrs }) => {
return {
inputType: attrs.options.type,
step: attrs.options.step,
invalidate: () => record.setInvalidField(fieldName),
placeholder: attrs.placeholder,
};
};
@@ -136,13 +136,12 @@ export class JournalDashboardGraphField extends Component {
JournalDashboardGraphField.template = "web.JournalDashboardGraphField";
JournalDashboardGraphField.props = {
...standardFieldProps,
className: { type: String, optional: true },
graphType: String,
};
JournalDashboardGraphField.supportedTypes = ["text"];
JournalDashboardGraphField.extractProps = (fieldName, record, attrs) => {
JournalDashboardGraphField.extractProps = ({ attrs }) => {
return {
graphType: attrs.graph_type,
};
@@ -12,7 +12,9 @@ export class LabelSelectionField extends Component {
return this.props.classesObj[this.props.value] || "primary";
}
get string() {
return formatSelection(this.props.value, { selection: this.props.options });
return formatSelection(this.props.value, {
selection: Array.from(this.props.record.fields[this.props.name].selection),
});
}
}
@@ -20,7 +22,6 @@ LabelSelectionField.template = "web.LabelSelectionField";
LabelSelectionField.props = {
...standardFieldProps,
classesObj: { type: Object, optional: true },
options: Object,
};
LabelSelectionField.defaultProps = {
classesObj: {},
@@ -29,10 +30,9 @@ LabelSelectionField.defaultProps = {
LabelSelectionField.displayName = _lt("Label Selection");
LabelSelectionField.supportedTypes = ["selection"];
LabelSelectionField.extractProps = (fieldName, record, attrs) => {
LabelSelectionField.extractProps = ({ attrs }) => {
return {
classesObj: attrs.options.classes,
options: Array.from(record.fields[fieldName].selection),
};
};
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.LabelSelectionField" owl="1">
<span t-attf-class="{{ 'badge text-bg-' + className}} " t-esc="string" t-att-raw-value="props.value"/>
<span t-attf-class="badge text-bg-{{className}} " t-esc="string" t-att-raw-value="props.value"/>
</t>
</templates>
@@ -52,17 +52,16 @@ Many2ManyBinaryField.props = {
acceptedFileExtensions: { type: String, optional: true },
className: { type: String, optional: true },
uploadText: { type: String, optional: true },
items: { type: Object, optional: true },
};
Many2ManyBinaryField.supportedTypes = ["many2many"];
Many2ManyBinaryField.isEmpty = () => false;
Many2ManyBinaryField.extractProps = (fieldName, record, attrs) => {
Many2ManyBinaryField.extractProps = ({ attrs, field }) => {
return {
acceptedFileExtensions: attrs.options.accepted_file_extensions,
className: attrs.class,
uploadText: record.fields[fieldName].string,
uploadText: field.string,
};
};
@@ -8,6 +8,10 @@ import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
export class Many2ManyCheckboxesField extends Component {
get items() {
return this.props.record.preloadedData[this.props.name];
}
isSelected(item) {
return this.props.value.currentIds.includes(item[0]);
}
@@ -30,18 +34,12 @@ Many2ManyCheckboxesField.template = "web.Many2ManyCheckboxesField";
Many2ManyCheckboxesField.components = { CheckBox };
Many2ManyCheckboxesField.props = {
...standardFieldProps,
items: Object,
};
Many2ManyCheckboxesField.displayName = _lt("Checkboxes");
Many2ManyCheckboxesField.supportedTypes = ["many2many"];
Many2ManyCheckboxesField.isEmpty = () => false;
Many2ManyCheckboxesField.extractProps = (fieldName, record) => {
return {
items: record.preloadedData[fieldName],
};
};
registry.category("fields").add("many2many_checkboxes", Many2ManyCheckboxesField);
@@ -3,7 +3,7 @@
<t t-name="web.Many2ManyCheckboxesField" owl="1">
<div aria-atomic="true">
<t t-foreach="props.items" t-as="item" t-key="item[0]">
<t t-foreach="items" t-as="item" t-key="item[0]">
<div>
<CheckBox
value="isSelected(item)"
@@ -45,7 +45,7 @@ export class Many2ManyTagsField extends Component {
},
getEvalParams: (props) => {
return {
evalContext: props.evalContext,
evalContext: this.evalContext,
readonly: props.readonly,
};
},
@@ -62,13 +62,29 @@ export class Many2ManyTagsField extends Component {
if (this.props.canQuickCreate) {
this.quickCreate = async (name) => {
const created = await this.orm.call(this.props.relation, "name_create", [name], {
context: this.props.context,
context: this.context,
});
return saveRecord([created[0]]);
};
}
}
get domain() {
return this.props.record.getFieldDomain(this.props.name);
}
get context() {
return this.props.record.getFieldContext(this.props.name);
}
get evalContext() {
return this.props.record.evalContext;
}
get canEditColor() {
return this.props.canEditColor && this.props.record.viewType !== "list";
}
get string() {
return this.props.record.activeFields[this.props.name].string;
}
get tags() {
return this.props.value.records.map((record) => ({
id: record.id, // datapoint_X
@@ -80,7 +96,7 @@ export class Many2ManyTagsField extends Component {
}
get canOpenColorDropdown() {
return this.handlesColor() && this.props.canEditColor;
return this.handlesColor() && this.canEditColor;
}
get showM2OSelectionField() {
@@ -123,9 +139,9 @@ export class Many2ManyTagsField extends Component {
getDomain() {
return Domain.and([
this.props.domain,
this.domain,
Domain.not([["id", "in", this.props.value.currentIds]]),
]).toList(this.props.context);
]).toList(this.context);
}
onBadgeClick(ev, record) {
@@ -271,12 +287,7 @@ Many2ManyTagsField.props = {
createDomain: { type: Array, optional: true },
placeholder: { type: String, optional: true },
relation: { type: String },
domain: { type: Domain },
context: { type: Object },
evalContext: { type: Object },
nameCreateField: { type: String, optional: true },
itemsVisible: { type: Number, optional: true },
string: { type: String },
};
Many2ManyTagsField.defaultProps = {
canEditColor: true,
@@ -290,19 +301,15 @@ Many2ManyTagsField.fieldsToFetch = {
display_name: { name: "display_name", type: "char" },
};
Many2ManyTagsField.extractProps = (fieldName, record, attrs) => {
const activeField = record.activeFields[fieldName];
Many2ManyTagsField.extractProps = ({ attrs, field }) => {
return {
colorField: attrs.options.color_field,
nameCreateField: attrs.options.create_name_field,
canEditColor: !attrs.options.no_edit_color && activeField.viewType !== "list",
relation: activeField.relation,
domain: record.getFieldDomain(fieldName),
context: record.getFieldContext(fieldName),
canEditColor: !attrs.options.no_edit_color,
relation: field.relation,
canQuickCreate: !attrs.options.no_quick_create,
createDomain: attrs.options.create,
evalContext: record.evalContext,
string: activeField.string,
placeholder: attrs.placeholder,
};
};
@@ -9,11 +9,11 @@
placeholder="props.placeholder"
resModel="props.relation"
autoSelect="true"
fieldString="props.string"
fieldString="string"
activeActions="activeActions"
update="update"
quickCreate="activeActions.canCreate ? quickCreate : null"
context="props.context"
context="context"
getDomain.bind="getDomain"
isToMany="true"
nameCreateField="props.nameCreateField"
@@ -22,23 +22,13 @@ Many2ManyTagsAvatarField.components = {
TagsList,
};
Many2ManyTagsAvatarField.supportedTypes = ["many2many"];
Many2ManyTagsAvatarField.extractProps = (fieldName, record, attrs) => {
return {
relation: record.activeFields[fieldName].relation,
domain: record.getFieldDomain(fieldName),
context: record.getFieldContext(fieldName),
evalContext: record.evalContext,
canQuickCreate: !attrs.options.no_quick_create,
createDomain: attrs.options.create,
string: record.activeFields[fieldName].string,
};
};
registry.category("fields").add("many2many_tags_avatar", Many2ManyTagsAvatarField);
class ListKanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
get itemsVisible() {
return this.props.record.activeFields[this.props.name].viewType === "list" ? 5 : 3;
}
get tags() {
return this.props.value.records.map((record) => ({
id: record.id, // datapoint_X
@@ -48,18 +38,5 @@ class ListKanbanMany2ManyTagsAvatarField extends Many2ManyTagsAvatarField {
}
}
ListKanbanMany2ManyTagsAvatarField.extractProps = (fieldName, record, attrs) => {
return {
itemsVisible: record.activeFields[fieldName].viewType === "list" ? 5 : 3,
relation: record.activeFields[fieldName].relation,
domain: record.getFieldDomain(fieldName),
context: record.getFieldContext(fieldName),
evalContext: record.evalContext,
canQuickCreate: !attrs.options.no_quick_create,
createDomain: attrs.options.create,
string: record.activeFields[fieldName].string,
};
};
registry.category("fields").add("list.many2many_tags_avatar", ListKanbanMany2ManyTagsAvatarField);
registry.category("fields").add("kanban.many2many_tags_avatar", ListKanbanMany2ManyTagsAvatarField);
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.Many2ManyTagsAvatarField" owl="1">
<TagsList tags="tags" itemsVisible="props.itemsVisible" t-on-keydown="onTagsKeydown"/>
<TagsList tags="tags" itemsVisible="itemsVisible" t-on-keydown="onTagsKeydown"/>
<div t-if="showM2OSelectionField" class="o_field_many2many_selection d-inline-block mw-100" t-on-keydown="onAutoCompleteKeydown" t-ref="autoComplete">
<Many2XAutocomplete
id="props.id"
@@ -13,7 +13,7 @@
activeActions="activeActions"
update="update"
quickCreate="activeActions.canCreate ? quickCreate : null"
context="props.context"
context="context"
getDomain.bind="getDomain"
isToMany="true"
/>
@@ -6,7 +6,6 @@ import { _lt } from "@web/core/l10n/translation";
import { useChildRef, useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { standardFieldProps } from "../standard_field_props";
import { Domain } from "@web/core/domain";
import { Many2XAutocomplete, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
const { Component, onWillUpdateProps, useState } = owl;
@@ -62,7 +61,7 @@ export class Many2OneField extends Component {
computeActiveActions(this.props);
this.openMany2X = useOpenMany2XRecord({
resModel: this.props.relation,
resModel: this.relation,
activeActions: this.state.activeActions,
isToMany: false,
onRecordSaved: async (record) => {
@@ -106,6 +105,16 @@ export class Many2OneField extends Component {
});
}
get relation() {
return this.props.relation || this.props.record.fields[this.props.name].relation;
}
get context() {
return this.props.record.getFieldContext(this.props.name);
}
get domain() {
return this.props.record.getFieldDomain(this.props.name);
}
get hasExternalButton() {
return this.props.canOpen && !!this.props.value && !this.state.isFloating;
}
@@ -128,19 +137,16 @@ export class Many2OneField extends Component {
}
getDomain() {
return this.props.getDomain().toList(this.props.getContext());
return this.domain.toList(this.context);
}
async openAction() {
const action = await this.orm.call(
this.props.relation,
"get_formview_action",
[[this.resId]],
{ context: this.props.getContext() }
);
const action = await this.orm.call(this.relation, "get_formview_action", [[this.resId]], {
context: this.context,
});
await this.action.doAction(action);
}
async openDialog(resId) {
return this.openMany2X({ resId, context: this.props.getContext() });
return this.openMany2X({ resId, context: this.context });
}
async openConfirmationDialog(request) {
@@ -187,10 +193,8 @@ Many2OneField.props = {
canCreateEdit: { type: Boolean, optional: true },
createNameField: { type: String, optional: true },
searchLimit: { type: Number, optional: true },
relation: String,
relation: { type: String, optional: true },
string: { type: String, optional: true },
getContext: { type: Function, optional: true },
getDomain: { type: Function, optional: true },
};
Many2OneField.defaultProps = {
canOpen: true,
@@ -201,14 +205,12 @@ Many2OneField.defaultProps = {
createNameField: "name",
searchLimit: 7,
string: "",
getContext: () => ({}),
getDomain: () => new Domain(),
};
Many2OneField.displayName = _lt("Many2one");
Many2OneField.supportedTypes = ["many2one"];
Many2OneField.extractProps = (fieldName, record, attrs) => {
Many2OneField.extractProps = ({ attrs, field }) => {
const noOpen = Boolean(attrs.options.no_open);
const noCreate = Boolean(attrs.options.no_create);
const canCreate = attrs.can_create && Boolean(JSON.parse(attrs.can_create)) && !noCreate;
@@ -223,10 +225,8 @@ Many2OneField.extractProps = (fieldName, record, attrs) => {
canWrite,
canQuickCreate: canCreate && !noQuickCreate,
canCreateEdit: canCreate && !noCreateEdit,
relation: record.fields[fieldName].relation,
string: attrs.string || record.fields[fieldName].string,
getContext: () => record.getFieldContext(fieldName),
getDomain: () => record.getFieldDomain(fieldName),
relation: field.relation,
string: attrs.string || field.string,
createNameField: attrs.options.create_name_field,
};
};
@@ -27,7 +27,7 @@
<t t-else="">
<a
class="o_form_uri"
t-att-href="props.value ? `#id=${props.value[0]}&amp;model=${props.relation}` : '#'"
t-att-href="props.value ? `#id=${props.value[0]}&amp;model=${relation}` : '#'"
t-on-click.prevent="onClick"
>
<span t-esc="displayName" />
@@ -44,13 +44,13 @@
value="displayName"
id="props.id"
placeholder="props.placeholder"
resModel="props.relation"
resModel="relation"
autoSelect="true"
fieldString="props.string"
activeActions="state.activeActions"
update="update"
quickCreate="quickCreate"
context="props.getContext()"
context="context"
getDomain.bind="getDomain"
nameCreateField="props.nameCreateField"
setInputFloats="setFloating"
@@ -1,9 +1,7 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Domain } from "@web/core/domain";
import { Many2OneField } from "../many2one/many2one_field";
import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
@@ -14,25 +12,11 @@ Many2OneAvatarField.components = {
Many2OneField,
};
Many2OneAvatarField.props = {
...standardFieldProps,
placeholder: { type: String, optional: true },
relation: String,
getContext: { type: Function, optional: true },
getDomain: { type: Function, optional: true },
};
Many2OneAvatarField.defaultProps = {
getContext: () => ({}),
getDomain: () => new Domain(),
...Many2OneField.props,
};
Many2OneAvatarField.supportedTypes = ["many2one"];
Many2OneAvatarField.extractProps = (fieldName, record) => {
return {
relation: record.fields[fieldName].relation,
getContext: () => record.getFieldContext(fieldName),
getDomain: () => record.getFieldDomain(fieldName),
};
};
Many2OneAvatarField.extractProps = Many2OneField.extractProps;
registry.category("fields").add("many2one_avatar", Many2OneAvatarField);
@@ -16,14 +16,20 @@ export class MonetaryField extends Component {
useInputField({
getValue: () => this.formattedValue,
refName: "numpadDecimal",
parse: (v) => parseMonetary(v, { currencyId: this.props.currencyId }),
parse: (v) => parseMonetary(v, { currencyId: this.currencyId }),
});
useNumpadDecimal();
}
get currencyId() {
return this.props.currencyField
? this.props.record.data[this.props.currencyField][0]
: (this.props.record.data.currency_id && this.props.record.data.currency_id[0]) ||
undefined;
}
get currency() {
if (!isNaN(this.props.currencyId) && this.props.currencyId in session.currencies) {
return session.currencies[this.props.currencyId];
if (!isNaN(this.currencyId) && this.currencyId in session.currencies) {
return session.currencies[this.currencyId];
}
return null;
}
@@ -43,7 +49,7 @@ export class MonetaryField extends Component {
if (!this.currency) {
return null;
}
return session.currencies[this.props.currencyId].digits;
return session.currencies[this.currencyId].digits;
}
get formattedValue() {
@@ -52,7 +58,7 @@ export class MonetaryField extends Component {
}
return formatMonetary(this.props.value, {
digits: this.currencyDigits,
currencyId: this.props.currencyId,
currencyId: this.currencyId,
noSymbol: !this.props.readonly || this.props.hideSymbol,
});
}
@@ -61,31 +67,26 @@ export class MonetaryField extends Component {
MonetaryField.template = "web.MonetaryField";
MonetaryField.props = {
...standardFieldProps,
currencyId: { type: Number, optional: true },
currencyField: { type: String, optional: true },
inputType: { type: String, optional: true },
digits: { type: Array, optional: true },
hideSymbol: { type: Boolean, optional: true },
invalidate: { type: Function, optional: true },
placeholder: { type: String, optional: true },
};
MonetaryField.defaultProps = {
hideSymbol: false,
inputType: "text",
invalidate: () => {},
};
MonetaryField.supportedTypes = ["monetary", "float"];
MonetaryField.displayName = _lt("Monetary");
MonetaryField.extractProps = function (fieldName, record, attrs) {
MonetaryField.extractProps = ({ attrs }) => {
return {
currencyId: attrs.options.currency_field
? record.data[attrs.options.currency_field][0]
: (record.data.currency_id && record.data.currency_id[0]) || undefined,
currencyField: attrs.options.currency_field,
inputType: attrs.type,
digits: [16, 2],
digits: [16, 2], // FIXME WOWL
hideSymbol: attrs.options.no_symbol,
invalidate: () => record.setInvalidField(fieldName),
placeholder: attrs.placeholder,
};
};
@@ -13,7 +13,7 @@ export class PdfViewerField extends Component {
setup() {
this.notification = useService("notification");
this.state = useState({
fileName: this.props.fileName || "",
fileName: this.fileName || "",
isValid: true,
objectUrl: "",
});
@@ -24,10 +24,16 @@ export class PdfViewerField extends Component {
}
});
}
get defaultPage() {
return this.props.record.data[`${this.props.name}_page`];
}
get fileName() {
return this.props.record.data[this.props.fileNameField];
}
get file() {
return {
data: this.props.fileData,
name: this.state.fileName || this.props.fileData || null,
data: this.props.value || "",
name: this.state.fileName || this.props.value || null,
};
}
get url() {
@@ -37,8 +43,8 @@ export class PdfViewerField extends Component {
encodeURIComponent(
this.state.objectUrl ||
url("/web/content", {
model: this.props.resModel,
id: this.props.resId,
model: this.props.record.resModel,
id: this.props.record.resId,
field: this.props.previewImage || this.props.name,
})
) +
@@ -71,28 +77,17 @@ PdfViewerField.components = {
};
PdfViewerField.props = {
...standardFieldProps,
defaultPage: { type: Number, optional: true },
fileData: { type: String, optional: true },
fileName: { type: String, optional: true },
fileNameField: { type: String, optional: true },
previewImage: { type: String, optional: true },
resId: { type: [Number, Boolean], optional: true },
resModel: { type: String, optional: true },
};
PdfViewerField.defaultProps = {
fileData: "",
};
PdfViewerField.displayName = _lt("PDF Viewer");
PdfViewerField.supportedTypes = ["binary"];
PdfViewerField.extractProps = (fieldName, record, attrs) => {
PdfViewerField.extractProps = ({ attrs }) => {
return {
defaultPage: record.data[fieldName + "_page"],
fileData: record.data[fieldName] || "",
fileName: record.data[attrs.filename] || "",
fileNameField: attrs.filename,
previewImage: attrs.options.preview_image,
resId: record.resId,
resModel: record.resModel,
};
};
@@ -26,7 +26,7 @@ PercentPieField.props = {
PercentPieField.displayName = _lt("PercentPie");
PercentPieField.supportedTypes = ["float", "integer"];
PercentPieField.extractProps = (fieldName, record, attrs) => {
PercentPieField.extractProps = ({ attrs }) => {
return {
string: attrs.string,
};
@@ -30,23 +30,16 @@ export class PercentageField extends Component {
PercentageField.template = "web.PercentageField";
PercentageField.props = {
...standardFieldProps,
invalidate: { type: Function, optional: true },
digits: { type: Array, optional: true },
placeholder: { type: String, optional: true },
};
PercentageField.defaultProps = {
invalidate: () => {},
};
PercentageField.displayName = _lt("Percentage");
PercentageField.supportedTypes = ["integer", "float"];
PercentageField.extractProps = (fieldName, record, attrs) => {
PercentageField.extractProps = ({ attrs, field }) => {
return {
invalidate: () => record.setInvalidField(fieldName),
digits:
(attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) ||
record.fields[fieldName].digits,
digits: (attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) || field.digits,
placeholder: attrs.placeholder,
};
};
@@ -22,7 +22,7 @@ PhoneField.props = {
PhoneField.displayName = _lt("Phone");
PhoneField.supportedTypes = ["char"];
PhoneField.extractProps = (fieldName, record, attrs) => {
PhoneField.extractProps = ({ attrs }) => {
return {
placeholder: attrs.placeholder,
};
@@ -12,15 +12,21 @@ export class PriorityField extends Component {
this.state = useState({
index: -1,
});
if (this.props.addCommand) {
if (this.props.record.activeFields[this.props.name].viewType === "form") {
this.initiateCommand();
}
}
get options() {
return Array.from(this.props.record.fields[this.props.name].selection);
}
get index() {
return this.state.index > -1
? this.state.index
: this.props.options.findIndex((o) => o[0] === this.props.value);
: this.options.findIndex((o) => o[0] === this.props.value);
}
get isReadonly() {
return this.props.record.isReadonly(this.props.name);
}
getTooltip(value) {
@@ -32,7 +38,7 @@ export class PriorityField extends Component {
onStarClicked(value) {
if (this.props.value === value) {
this.state.index = -1;
this.props.update(this.props.options[0][0]);
this.props.update(this.options[0][0]);
} else {
this.props.update(value);
}
@@ -41,7 +47,7 @@ export class PriorityField extends Component {
try {
const commandService = useService("command");
const provide = () => {
return this.props.options.map((value) => ({
return this.options.map((value) => ({
name: value[1],
action: () => {
this.props.update(value[0]);
@@ -68,21 +74,16 @@ export class PriorityField extends Component {
PriorityField.template = "web.PriorityField";
PriorityField.props = {
options: Object,
...standardFieldProps,
addCommand: { type: Boolean, optional: true },
tooltipLabel: { type: String, optional: true },
};
PriorityField.displayName = _lt("Priority");
PriorityField.supportedTypes = ["selection"];
PriorityField.extractProps = (fieldName, record) => {
PriorityField.extractProps = ({ field }) => {
return {
addCommand: record.activeFields[fieldName].viewType === "form",
options: Array.from(record.fields[fieldName].selection),
tooltipLabel: record.fields[fieldName].string,
readonly: record.isReadonly(fieldName),
tooltipLabel: field.string,
};
};
@@ -3,9 +3,9 @@
<t t-name="web.PriorityField" owl="1">
<div class="o_priority" role="radiogroup" name="priority" aria-label="Priority">
<t t-foreach="props.options" t-as="value" t-key="value">
<t t-foreach="options" t-as="value" t-key="value">
<t t-if="!value_first">
<t t-if="props.readonly">
<t t-if="isReadonly">
<span
class="o_priority_star fa"
role="radio"
@@ -14,14 +14,14 @@ export class ProgressBarField extends Component {
setup() {
useNumpadDecimal();
this.state = useState({
currentValue: this.props.currentValue.value,
maxValue: this.props.maxValue.value,
currentValue: this.getCurrentValue(this.props),
maxValue: this.getMaxValue(this.props),
isEditing: false,
});
onWillUpdateProps((nextProps) => {
Object.assign(this.state, {
currentValue: nextProps.currentValue.value,
maxValue: nextProps.maxValue.value,
currentValue: this.getCurrentValue(nextProps),
maxValue: this.getMaxValue(nextProps),
});
if (nextProps.readonly) {
this.state.isEditing = false;
@@ -29,31 +29,76 @@ export class ProgressBarField extends Component {
});
}
getFormattedValue(part, humanReadable = false) {
const formatter = formatters.get(this.props[part].type);
return formatter(this.state[part], { humanReadable });
get isCurrentValueInteger() {
return this.state.currentValue % 1 === 0;
}
get isMaxValueInteger() {
return this.state.maxValue % 1 === 0;
}
/**
* @param {String} value
* @param {String} part
*/
onChangeValue(value, part) {
getCurrentValueField(p) {
return typeof p.currentValueField === "string" ? p.currentValueField : p.name;
}
getMaxValueField(p) {
return typeof p.maxValueField === "string" ? p.maxValueField : p.name;
}
getCurrentValue(p) {
return p.record.data[this.getCurrentValueField(p)] || 0;
}
getMaxValue(p) {
if (p.maxValueField) {
return p.record.data[p.maxValueField] || 100;
}
return 100;
}
formatCurrentValue(humanReadable = false) {
const formatter = formatters.get(this.isCurrentValueInteger ? "integer" : "float");
return formatter(this.state.currentValue, { humanReadable });
}
formatMaxValue(humanReadable = false) {
const formatter = formatters.get(this.isMaxValueInteger ? "integer" : "float");
return formatter(this.state.maxValue, { humanReadable });
}
onCurrentValueChange(value) {
let parsedValue;
try {
let parsedValue = parseFloat(value);
if (this.props[part].type === "integer") {
parsedValue = Math.floor(parsedValue);
}
this.state[part] = parsedValue;
this.props.updatePart(part, parsedValue);
if (this.props.readonly) {
this.state.isEditing = false;
this.props.record.save();
}
parsedValue = parseFloat(value);
} catch {
this.props.invalidate();
this.props.record.setInvalidField(this.props.name);
return;
}
if (this.isCurrentValueInteger) {
parsedValue = Math.floor(parsedValue);
}
this.state.currentValue = parsedValue;
this.props.record.update({ [this.getCurrentValueField(this.props)]: parsedValue });
if (this.props.readonly) {
this.state.isEditing = false;
this.props.record.save();
}
}
onMaxValueChange(value) {
let parsedValue;
try {
parsedValue = parseFloat(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.state.isEditing = false;
this.props.record.save();
}
}
onClick() {
@@ -61,17 +106,24 @@ export class ProgressBarField extends Component {
this.state.isEditing = true;
}
}
onBlur() {
if (this.props.readonly) {
this.state.isEditing = false;
}
}
onInput(ev, part) {
const parser = parsers.get(this.props[part].type);
onCurrentValueInput(ev) {
const parser = parsers.get(this.isCurrentValueInteger ? "integer" : "float");
try {
this.state[part] = parser(ev.target.value);
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
}
@@ -81,55 +133,22 @@ export class ProgressBarField extends Component {
ProgressBarField.template = "web.ProgressBarField";
ProgressBarField.props = {
...standardFieldProps,
currentValue: { type: Object, optional: true },
maxValueField: { type: [String, Number], optional: true },
currentValueField: { type: String, optional: true },
isPercentage: { type: Boolean, optional: true },
maxValue: { type: Object, optional: true },
isEditable: { type: Boolean, optional: true },
isEditableInReadonly: { type: Boolean, optional: true },
isCurrentValueEditable: { type: Boolean, optional: true },
isMaxValueEditable: { type: Boolean, optional: true },
invalidate: { type: Function, optional: true },
updatePart: { type: Function, optional: true },
};
ProgressBarField.defaultProps = {
invalidate: () => {},
updatePart: () => {},
};
ProgressBarField.displayName = _lt("Progress Bar");
ProgressBarField.supportedTypes = ["integer", "float"];
ProgressBarField.extractProps = (fieldName, record, attrs) => {
const getPart = (part) => {
if (attrs.options[part]) {
let value = attrs.options[part];
let name;
if (isNaN(value)) {
value =
(record.data[attrs.options[part]] !== undefined &&
record.data[attrs.options[part]]) ||
0;
name = attrs.options[part];
}
return {
fieldName: name,
value,
type: value % 1 === 0 ? "integer" : "float",
};
}
const value = part === "max_value" ? 100 : record.data[fieldName] || 0;
return {
fieldName: fieldName,
value,
type: value % 1 === 0 ? "integer" : "float",
};
};
const parts = {
currentValue: getPart("current_value"),
maxValue: getPart("max_value"),
};
ProgressBarField.extractProps = ({ attrs }) => {
return {
...parts,
maxValueField: attrs.options.max_value,
currentValueField: attrs.options.current_value,
isPercentage: !attrs.options.max_value,
isEditable: attrs.options.editable,
isEditableInReadonly: attrs.options.editable_readonly,
@@ -137,8 +156,6 @@ ProgressBarField.extractProps = (fieldName, record, attrs) => {
attrs.options.editable &&
(!attrs.options.edit_max_value || attrs.options.edit_current_value),
isMaxValueEditable: attrs.options.editable && attrs.options.edit_max_value,
invalidate: () => record.setInvalidField(fieldName),
updatePart: (part, value) => record.update({ [parts[part].fieldName]: value }),
};
};
@@ -8,10 +8,10 @@
<div class="o_progressbar_complete" t-att-style="'width: min(' + 100 * state.currentValue / state.maxValue + '%, 100%)'"></div>
</div>
<t t-if="props.isPercentage">
<span class="o_progressbar_value" t-esc="getFormattedValue('currentValue', true) + '%'"/>
<span class="o_progressbar_value" t-esc="formatCurrentValue(true) + '%'"/>
</t>
<t t-else="">
<div class="o_progressbar_value" t-esc="getFormattedValue('currentValue', true) + ' / ' + getFormattedValue('maxValue', true)"/>
<div class="o_progressbar_value" t-esc="formatCurrentValue(true) + ' / ' + formatMaxValue(true)"/>
</t>
</div>
</t>
@@ -26,11 +26,11 @@
class="o_input o_progressbar_value"
type="text"
inputmode="numeric"
t-att-value="getFormattedValue('currentValue') or ''"
t-att-value="formatCurrentValue() or ''"
t-att-required="props.required"
t-on-change="(ev) => this.onChangeValue(ev.target.value, 'currentValue')"
t-on-input="(ev) => this.onInput(ev, 'currentValue')"
t-on-blur="(ev) => this.onBlur(ev.target.value, 'currentValue')"
t-on-change="(ev) => this.onCurrentValueChange(ev.target.value)"
t-on-input="(ev) => this.onCurrentValueInput(ev)"
t-on-blur="onBlur"
/>
<span>%</span>
</div>
@@ -41,10 +41,10 @@
class="o_progressbar_value o_input"
type="text"
inputmode="numeric"
t-att-value="getFormattedValue('currentValue')"
t-on-change="(ev) => this.onChangeValue(ev.target.value, 'currentValue')"
t-on-input="(ev) => this.onInput(ev, 'currentValue')"
t-on-blur="(ev) => this.onBlur(ev.target.value, 'currentValue')"
t-att-value="formatCurrentValue()"
t-on-change="(ev) => this.onCurrentValueChange(ev.target.value)"
t-on-input="(ev) => this.onCurrentValueInput(ev)"
t-on-blur="onBlur"
/>
<span t-if="props.isCurrentValueEditable and props.isMaxValueEditable" t-esc="'/'" />
<input
@@ -52,9 +52,9 @@
class="o_progressbar_value o_input"
type="text"
inputmode="numeric"
t-att-value="getFormattedValue('maxValue')"
t-on-change="(ev) => this.onChangeValue(ev.target.value, 'maxValue')"
t-on-input="(ev) => this.onInput(ev, 'maxValue')"
t-att-value="formatMaxValue()"
t-on-change="(ev) => this.onMaxValueChange(ev.target.value)"
t-on-input="(ev) => this.onMaxValueInput(ev)"
t-on-blur="onBlur"
/>
</t>
@@ -11,6 +11,21 @@ export class RadioField extends Component {
this.id = `radio_field_${++RadioField.nextId}`;
}
get items() {
switch (this.props.record.fields[this.props.name].type) {
case "selection":
return this.props.record.fields[this.props.name].selection;
case "many2one": {
const value = this.props.record.preloadedData[this.props.name] || [];
return value.map((item) => [item.id, item.display_name]);
}
default:
return [];
}
}
get string() {
return this.props.record.activeFields[this.props.name].string;
}
get value() {
switch (this.props.type) {
case "selection":
@@ -43,8 +58,6 @@ RadioField.template = "web.RadioField";
RadioField.props = {
...standardFieldProps,
orientation: { type: String, optional: true },
radioItems: Array,
string: { type: String, optional: true },
};
RadioField.defaultProps = {
orientation: "vertical",
@@ -54,23 +67,9 @@ RadioField.displayName = _lt("Radio");
RadioField.supportedTypes = ["many2one", "selection"];
RadioField.isEmpty = () => false;
RadioField.extractProps = (fieldName, record, attrs) => {
const getRadioItems = () => {
switch (record.fields[fieldName].type) {
case "selection":
return record.fields[fieldName].selection;
case "many2one": {
const value = record.preloadedData[fieldName] || [];
return value.map((item) => [item.id, item.display_name]);
}
default:
return [];
}
};
RadioField.extractProps = ({ attrs }) => {
return {
orientation: attrs.options.horizontal ? "horizontal" : "vertical",
radioItems: getRadioItems(),
string: record.activeFields[fieldName].string,
};
};
@@ -5,9 +5,9 @@
<div
role="radiogroup"
t-attf-class="o_{{ props.orientation }}"
t-att-aria-label="props.string"
t-att-aria-label="string"
>
<t t-foreach="props.radioItems" t-as="item" t-key="item[0]">
<t t-foreach="items" t-as="item" t-key="item[0]">
<div class="custom-control form-check o_radio_item" aria-atomic="true">
<input
type="radio"
@@ -2,9 +2,7 @@
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { Domain } from "@web/core/domain";
import { Many2OneField } from "../many2one/many2one_field";
import { standardFieldProps } from "../standard_field_props";
const { Component, onWillUpdateProps, useState } = owl;
@@ -18,39 +16,67 @@ export class ReferenceField extends Component {
resModel: this.relation,
});
let modelName = this.getModelName(this.props);
onWillUpdateProps((nextProps) => {
if (
valuesEqual(this.props.value || {}, nextProps.value || {}) &&
nextProps.modelFieldValue !== this.props.modelFieldValue
valuesEqual(this.getValue(this.props) || {}, this.getValue(nextProps) || {}) &&
this.getModelName(nextProps) !== modelName
) {
modelName = this.getModelName(nextProps);
nextProps.update(false);
}
});
}
getPreloadedData(p) {
return p.record.preloadedData[p.name];
}
getValue(p) {
if (p.type === "char") {
return {
resModel: this.getPreloadedData(p).model,
resId: this.getPreloadedData(p).data.id,
displayName: this.getPreloadedData(p).data.display_name,
};
} else {
return p.value;
}
}
get m2oProps() {
const value = this.getValue(this.props);
const p = {
...this.props,
relation: this.relation,
value: this.props.value && [this.props.value.resId, this.props.value.displayName],
value: value && [value.resId, value.displayName],
update: this.updateM2O.bind(this),
};
delete p.hideModelSelector;
delete p.modelSelection;
delete p.modelFieldValue;
return p;
}
get selection() {
if (this.props.type !== "char" && !this.props.hideModelSelector) {
return this.props.record.fields[this.props.name].selection;
}
return [];
}
get relation() {
if (this.props.modelFieldValue) {
return this.props.modelFieldValue;
} else if (this.props.value && this.props.value.resModel) {
return this.props.value.resModel;
if (this.getModelName(this.props)) {
return this.getModelName(this.props);
} else if (this.getValue(this.props) && this.getValue(this.props).resModel) {
return this.getValue(this.props).resModel;
} else {
return this.state && this.state.resModel;
}
}
getModelName(p) {
if (p.hideModelSelector && p.record.preloadedData[p.name]) {
return p.record.preloadedData[p.name].modelName;
}
return null;
}
updateModel(value) {
this.state.resModel = value;
this.props.update(false);
@@ -75,60 +101,20 @@ ReferenceField.components = {
Many2OneField,
};
ReferenceField.props = {
...standardFieldProps,
...Many2OneField.props,
hideModelSelector: { type: Boolean, optional: true },
modelFieldValue: { type: String, optional: true },
value: [
Boolean,
{
type: Object,
shape: {
resModel: String,
resId: Number,
displayName: String,
},
},
],
getContext: { type: Function, optional: true },
getDomain: { type: Function, optional: true },
modelSelection: { type: Array, optional: true },
string: { type: String, optional: true },
};
ReferenceField.defaultProps = {
getContext: () => ({}),
getDomain: () => new Domain(),
...Many2OneField.defaultProps,
};
ReferenceField.displayName = _lt("Reference");
ReferenceField.supportedTypes = ["reference", "char"];
ReferenceField.extractProps = (fieldName, record, attrs) => {
let props = {};
const preloadedData = record.preloadedData[fieldName];
if (record.fields[fieldName].type === "char") {
props = {
value: {
resModel: preloadedData.model,
resId: preloadedData.data.id,
displayName: preloadedData.data.display_name,
},
};
} else if (attrs.options["model_field"]) {
props = {
hideModelSelector: true,
modelFieldValue: preloadedData && preloadedData.modelName,
};
} else {
props = {
modelSelection: record.fields[fieldName].selection,
};
}
ReferenceField.extractProps = ({ attrs, field }) => {
return {
...props,
getContext: () => record.getFieldContext(fieldName),
getDomain: () => record.getFieldDomain(fieldName),
string: attrs.string || record.fields[fieldName].string,
...Many2OneField.extractProps({ attrs, field }),
hideModelSelector: !!attrs.options["model_field"],
};
};
@@ -9,7 +9,7 @@
t-on-change="(ev) => this.updateModel(ev.target.value || false)"
>
<option />
<t t-foreach="props.modelSelection" t-as="option" t-key="option[0]">
<t t-foreach="selection" t-as="option" t-key="option[0]">
<option t-att-value="option[0]" t-att-selected="option[0] === relation" t-esc="option[1]" />
</t>
</select>
@@ -7,13 +7,23 @@ import { standardFieldProps } from "../standard_field_props";
const { Component } = owl;
export class SelectionField extends Component {
get options() {
switch (this.props.record.fields[this.props.name].type) {
case "many2one":
return this.props.record.preloadedData[this.props.name];
case "selection":
return this.props.record.fields[this.props.name].selection;
default:
return [];
}
}
get string() {
switch (this.props.type) {
case "many2one":
return this.props.value ? this.props.value[1] : "";
case "selection":
return this.props.value !== false
? this.props.options.find((o) => o[0] === this.props.value)[1]
? this.options.find((o) => o[0] === this.props.value)[1]
: "";
default:
return "";
@@ -23,6 +33,9 @@ export class SelectionField extends Component {
const rawValue = this.props.value;
return this.props.type === "many2one" && rawValue ? rawValue[0] : rawValue;
}
get isRequired() {
return this.props.record.isRequired(this.props.name);
}
stringify(value) {
return JSON.stringify(value);
@@ -38,7 +51,7 @@ export class SelectionField extends Component {
if (value === false) {
this.props.update(false);
} else {
this.props.update(this.props.options.find((option) => option[0] === value));
this.props.update(this.options.find((option) => option[0] === value));
}
break;
case "selection":
@@ -51,30 +64,16 @@ export class SelectionField extends Component {
SelectionField.template = "web.SelectionField";
SelectionField.props = {
...standardFieldProps,
options: Object,
placeholder: { type: String, optional: true },
required: { type: Boolean, optional: true },
};
SelectionField.displayName = _lt("Selection");
SelectionField.supportedTypes = ["many2one", "selection"];
SelectionField.isEmpty = (record, fieldName) => record.data[fieldName] === false;
SelectionField.extractProps = (fieldName, record, attrs) => {
const getOptions = () => {
switch (record.fields[fieldName].type) {
case "many2one":
return record.preloadedData[fieldName];
case "selection":
return record.fields[fieldName].selection;
default:
return [];
}
};
SelectionField.extractProps = ({ attrs }) => {
return {
options: getOptions(),
placeholder: attrs.placeholder,
required: record.isRequired(fieldName),
};
};
@@ -7,14 +7,14 @@
</t>
<t t-else="">
<select class="o_input" t-on-change="onChange">
<t t-if="!props.required">
<t t-if="!isRequired">
<option
value="false"
t-att-selected="!props.value"
t-esc="props.placeholder"
/>
</t>
<t t-foreach="props.options" t-as="option" t-key="option[0]">
<t t-foreach="options" t-as="option" t-key="option[0]">
<option
t-att-selected="option[0] === value"
t-att-value="stringify(option[0])"
@@ -13,29 +13,30 @@ export class StatInfoField extends Component {
const formatter = formatters.get(this.props.type);
return formatter(this.props.value || 0, { digits: this.props.digits });
}
get label() {
return this.props.labelField
? this.props.record.data[this.props.labelField]
: this.props.record.activeFields[this.props.name].string;
}
}
StatInfoField.template = "web.StatInfoField";
StatInfoField.props = {
...standardFieldProps,
label: { type: String, optional: true },
labelField: { type: String, optional: true },
noLabel: { type: Boolean, optional: true },
digits: { type: Array, optional: true },
};
StatInfoField.label = _lt("Stat Info");
StatInfoField.displayName = _lt("Stat Info");
StatInfoField.supportedTypes = ["float", "integer", "monetary"];
StatInfoField.isEmpty = () => false;
StatInfoField.extractProps = (fieldName, record, attrs) => {
StatInfoField.extractProps = ({ attrs, field }) => {
return {
label: attrs.options.label_field
? record.data[attrs.options.label_field]
: record.activeFields[fieldName].string,
labelField: attrs.options.label_field,
noLabel: archParseBoolean(attrs.nolabel),
digits:
(attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) ||
record.fields[fieldName].digits,
digits: (attrs.digits ? JSON.parse(attrs.digits) : attrs.options.digits) || field.digits,
};
};
@@ -3,7 +3,7 @@
<t t-name="web.StatInfoField" owl="1">
<span class="o_stat_info o_stat_value" t-esc="formattedValue"/>
<span t-if="!props.noLabel" class="o_stat_text" t-esc="props.label"/>
<span t-if="!props.noLabel" class="o_stat_text" t-esc="label"/>
</t>
</templates>
@@ -12,7 +12,7 @@ const { Component } = owl;
export class StateSelectionField extends Component {
setup() {
if (this.props.addCommand) {
if (this.props.record.activeFields[this.props.name].viewType === "form") {
this.initiateCommand();
}
this.excludedValues = [];
@@ -22,21 +22,33 @@ export class StateSelectionField extends Component {
done: "green",
};
}
get options() {
return Array.from(this.props.record.fields[this.props.name].selection);
}
get availableOptions() {
return this.props.options.filter((o) => !this.excludedValues.includes(o[0]));
return this.options.filter((o) => !this.excludedValues.includes(o[0]));
}
get currentValue() {
return this.props.value || this.props.options[0][0];
return this.props.value || this.options[0][0];
}
get label() {
return formatSelection(this.currentValue, { selection: this.props.options });
return formatSelection(this.currentValue, { selection: this.options });
}
get showLabel() {
return (
this.props.record.activeFields[this.props.name].viewType === "list" &&
!this.props.hideLabel
);
}
get isReadonly() {
return this.props.record.isReadonly(this.props.name);
}
initiateCommand() {
try {
const commandService = useService("command");
const provide = () => {
return this.props.options.map((value) => ({
return this.options.map((value) => ({
name: value[1],
action: () => {
this.props.update(value[0]);
@@ -71,24 +83,18 @@ StateSelectionField.components = {
};
StateSelectionField.props = {
...standardFieldProps,
addCommand: { type: Boolean, optional: true },
showLabel: { type: Boolean, optional: true },
options: Object,
hideLabel: { type: Boolean, optional: true },
};
StateSelectionField.defaultProps = {
addCommand: false,
showLabel: false,
hideLabel: false,
};
StateSelectionField.displayName = _lt("Label Selection");
StateSelectionField.supportedTypes = ["selection"];
StateSelectionField.extractProps = (fieldName, record, attrs) => {
StateSelectionField.extractProps = ({ attrs }) => {
return {
addCommand: record.activeFields[fieldName].viewType === "form",
showLabel: record.activeFields[fieldName].viewType === "list" && !attrs.options.hide_label,
options: Array.from(record.fields[fieldName].selection),
readonly: record.isReadonly(fieldName),
hideLabel: !!attrs.options.hide_label,
};
};
@@ -2,10 +2,10 @@
<templates xml:space="preserve">
<t t-name="web.StateSelectionField" owl="1">
<t t-if="props.readonly">
<t t-if="isReadonly">
<button class="d-flex align-items-center btn btn-link disabled">
<span t-attf-class="{{ 'mx-2 o_status ' + statusColor(currentValue) }} "/>
<span t-if="props.showLabel" class="o_status_label" t-esc="label"/>
<span t-if="showLabel" class="o_status_label" t-esc="label"/>
</button>
</t>
<t t-else="">
@@ -13,7 +13,7 @@
<t t-set-slot="toggler">
<div class="d-flex align-items-center">
<span t-attf-class="{{ 'mx-2 o_status ' + statusColor(currentValue) }} "/>
<span t-if="props.showLabel" class="o_status_label" t-esc="label"/>
<span t-if="showLabel" class="o_status_label" t-esc="label"/>
</div>
</t>
<t t-foreach="availableOptions" t-as="option" t-key="option[0]">
@@ -14,15 +14,25 @@ const { Component } = owl;
export class StatusBarField extends Component {
setup() {
if (this.props.addCommand) {
if (this.props.record.activeFields[this.props.name].viewType === "form") {
this.initiateCommand();
}
}
get currentName() {
const item = this.props.options.find((item) => item.isSelected);
const item = this.options.find((item) => item.isSelected);
return item ? item.name : "";
}
get options() {
switch (this.props.record.fields[this.props.name].type) {
case "many2one":
return this.props.record.preloadedData[this.props.name];
case "selection":
return this.props.record.fields[this.props.name].selection;
default:
return [];
}
}
getDropdownItemClassNames(item) {
const classNames = [
@@ -37,7 +47,7 @@ export class StatusBarField extends Component {
}
getVisibleMany2Ones() {
let items = this.props.options;
let items = this.options;
// FIXME: do this somewhere else
items = items.map((i) => {
return {
@@ -53,7 +63,7 @@ export class StatusBarField extends Component {
}
getVisibleSelection() {
let selection = this.props.options;
let selection = this.options;
if (this.props.visibleSelection.length) {
selection = selection.filter(
(item) =>
@@ -140,13 +150,11 @@ StatusBarField.defaultProps = {
};
StatusBarField.props = {
...standardFieldProps,
addCommand: { type: Boolean, optional: true },
canCreate: { type: Boolean, optional: true },
canWrite: { type: Boolean, optional: true },
displayName: { type: String, optional: true },
isDisabled: { type: Boolean, optional: true },
visibleSelection: { type: Array, optional: true },
options: Array,
};
StatusBarField.components = {
Dropdown,
@@ -159,23 +167,11 @@ StatusBarField.supportedTypes = ["many2one", "selection"];
StatusBarField.isEmpty = (record, fieldName) => {
return record.model.env.isSmall ? !record.data[fieldName] : false;
};
StatusBarField.extractProps = (fieldName, record, attrs) => {
const getOptions = () => {
switch (record.fields[fieldName].type) {
case "many2one":
return record.preloadedData[fieldName];
case "selection":
return record.fields[fieldName].selection;
default:
return [];
}
};
StatusBarField.extractProps = ({ attrs, field }) => {
return {
options: getOptions(),
addCommand: record.activeFields[fieldName].viewType === "form",
canCreate: Boolean(attrs.can_create),
canWrite: Boolean(attrs.can_write),
displayName: record.fields[fieldName].string,
displayName: field.string,
isDisabled: !attrs.options.clickable,
visibleSelection:
attrs.statusbar_visible && attrs.statusbar_visible.trim().split(/\s*,\s*/g),
@@ -66,19 +66,15 @@ TextField.props = {
...standardFieldProps,
isTranslatable: { type: Boolean, optional: true },
placeholder: { type: String, optional: true },
resId: { type: [Number, Boolean], optional: true },
resModel: { type: String, optional: true },
};
TextField.displayName = _lt("Multiline Text");
TextField.supportedTypes = ["html", "text"];
TextField.extractProps = (fieldName, record, attrs) => {
TextField.extractProps = ({ attrs, field }) => {
return {
isTranslatable: record.fields[fieldName].translate,
isTranslatable: field.translate,
placeholder: attrs.placeholder,
resId: record.resId,
resModel: record.resModel,
};
};
@@ -93,28 +89,4 @@ class ListTextField extends TextField {
}
}
ListTextField.template = "web.TextField";
ListTextField.components = {
TranslationButton,
};
ListTextField.props = {
...standardFieldProps,
isTranslatable: { type: Boolean, optional: true },
placeholder: { type: String, optional: true },
resId: { type: [Number, Boolean], optional: true },
resModel: { type: String, optional: true },
};
ListTextField.displayName = _lt("Multiline Text");
ListTextField.supportedTypes = ["html", "text"];
ListTextField.extractProps = (fieldName, record, attrs) => {
return {
isTranslatable: record.fields[fieldName].translate,
placeholder: attrs.placeholder,
resId: record.resId,
resModel: record.resModel,
};
};
registry.category("fields").add("list.text", ListTextField);
@@ -18,8 +18,8 @@
<t t-if="props.isTranslatable">
<TranslationButton
fieldName="props.name"
resId="props.resId"
resModel="props.resModel"
resId="props.record.resId"
resModel="props.record.resModel"
value="props.value"
updateField="props.update"
/>
@@ -37,7 +37,7 @@ UrlField.props = {
UrlField.displayName = _lt("URL");
UrlField.supportedTypes = ["char"];
UrlField.extractProps = (fieldName, record, attrs) => {
UrlField.extractProps = ({ attrs }) => {
return {
text: attrs.text,
websitePath: attrs.options.website_path,
@@ -254,7 +254,7 @@ X2ManyField.props = {
X2ManyField.supportedTypes = ["one2many"];
X2ManyField.template = "web.X2ManyField";
X2ManyField.useSubView = true;
X2ManyField.extractProps = (fieldName, record, attrs) => {
X2ManyField.extractProps = ({ attrs }) => {
return {
addLabel: attrs["add-label"],
};
+5 -7
View File
@@ -123,14 +123,12 @@ export function copyAttributes(el, compiled) {
}
}
for (const attName of ["style", "placeholder"]) {
let att = el.getAttribute(attName);
if (att) {
if (isComponent) {
att = toStringExpression(att);
}
compiled.setAttribute(attName, att);
let att = el.getAttribute("style");
if (att) {
if (isComponent) {
att = toStringExpression(att);
}
compiled.setAttribute("style", att);
}
}
@@ -202,4 +202,48 @@ QUnit.module("Fields", (hooks) => {
await click(target, ".o_boolean_toggle");
assert.containsOnce(target, ".o_boolean_toggle input:checked");
});
QUnit.test(
"BooleanToggleField is disabled if readonly in editable list",
async function (assert) {
serverData.models.partner.fields.bar.readonly = true;
await makeView({
type: "list",
serverData,
resModel: "partner",
arch: `
<list editable="bottom">
<field name="bar" widget="boolean_toggle" />
</list>
`,
});
assert.containsOnce(
target,
".o_boolean_toggle input:disabled",
"field should be readonly"
);
assert.containsOnce(target, ".o_boolean_toggle input:not(:checked)");
await click(target, ".o_boolean_toggle");
assert.containsOnce(
target,
".o_boolean_toggle input:disabled",
"field should still be readonly"
);
assert.containsOnce(
target,
".o_boolean_toggle input:not(:checked)",
"should keep unchecked on cell click"
);
await click(target, ".o_boolean_toggle");
assert.containsOnce(
target,
".o_boolean_toggle input:not(:checked)",
"should keep unchecked on click"
);
}
);
});
@@ -390,7 +390,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_progressbar_value").textContent,
"99 / 0",
"99 / 100",
"Initial value should be correct"
);
@@ -406,7 +406,7 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_progressbar_value.o_input", "69");
assert.strictEqual(
target.querySelector(".o_progressbar_value").textContent,
"69 / 0",
"69 / 100",
"New value should be different than initial after click"
);
});
@@ -472,7 +472,7 @@ QUnit.module("Fields", (hooks) => {
assert.strictEqual(
target.querySelector(".o_field_progressbar[name='int_field3'] .o_progressbar_value")
.textContent,
"69 / 0",
"69 / 100",
"New value should be different than initial after click"
);
}
@@ -106,14 +106,14 @@ QUnit.module("Form Compiler", () => {
assert.areContentEquivalent(compileTemplate(arch), expected);
});
QUnit.test("properly compile field with placeholder", async (assert) => {
QUnit.test("properly compile field without placeholder", async (assert) => {
const arch = /*xml*/ `
<form>
<field name="display_name" placeholder="e.g. Contact's Name or //someinfo..."/>
</form>`;
const expected = /*xml*/ `
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']" placeholder="\`e.g. Contact's Name or //someinfo...\`"/>
<Field id="'display_name'" name="'display_name'" record="props.record" fieldInfo="props.archInfo.fieldNodes['display_name']"/>
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@@ -254,6 +254,7 @@ QUnit.module("Form Renderer", (hooks) => {
}
}
CharField.template = owl.xml`<div/>`;
CharField.extractProps = ({ attrs }) => ({ placeholder: attrs.placeholder });
registry.category("fields").add("char", CharField, { force: true });
@@ -2741,7 +2741,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(this.props.horizontal, true);
}
}
MyField.extractProps = function (fieldName, record, attrs) {
MyField.extractProps = function ({ attrs }) {
assert.deepEqual(attrs.options, { horizontal: true });
return { horizontal: attrs.options.horizontal };
};
@@ -784,21 +784,23 @@ QUnit.module("Views", (hooks) => {
QUnit.test("field with widget and attributes in kanban", async (assert) => {
assert.expect(1);
class MyField extends Component {}
MyField.template = xml`<span/>`;
MyField.extractProps = (fieldName, record, attrs) => {
if (record.resId === 1) {
assert.deepEqual(attrs, {
field_id: "int_field",
name: "int_field",
widget: "my_field",
str: "some string",
bool: "true",
num: "4.5",
options: {},
});
class MyField extends Component {
setup() {
if (this.props.record.resId === 1) {
assert.deepEqual(this.props.attrs, {
name: "int_field",
widget: "my_field",
str: "some string",
bool: "true",
num: "4.5",
options: {},
field_id: "int_field",
});
}
}
};
}
MyField.template = xml`<span/>`;
MyField.extractProps = ({ attrs }) => ({ attrs });
registry.category("fields").add("my_field", MyField);
await makeView({
@@ -826,7 +828,7 @@ QUnit.module("Views", (hooks) => {
QUnit.test("field with widget and dynamic attributes in kanban", async (assert) => {
class MyField extends Component {}
MyField.template = xml`<span/>`;
MyField.extractProps = (fieldName, record, attrs) => {
MyField.extractProps = ({ attrs }) => {
assert.step(
`${attrs["dyn-bool"]}/${attrs["interp-str"]}/${attrs["interp-str2"]}/${attrs["interp-str3"]}`
);