[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:
@@ -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"
|
||||
>
|
||||
​ <!-- 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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+1
-2
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+4
-6
@@ -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);
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+4
-27
@@ -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
-2
@@ -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]}&model=${props.relation}` : '#'"
|
||||
t-att-href="props.value ? `#id=${props.value[0]}&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"],
|
||||
};
|
||||
|
||||
@@ -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"]}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user