[REF] base,all: Update modifier syntax: adapt JavaScript

closes odoo/odoo#104741

Related: odoo/enterprise#35255
Related: odoo/documentation#3523
Related: odoo/upgrade#4884
Signed-off-by: Rémy Voet (ryv) <ryv@odoo.com>
This commit is contained in:
Gorash
2023-08-18 09:49:16 +02:00
parent ecd8547672
commit 0d99f4b9d9
76 changed files with 712 additions and 606 deletions
@@ -73,7 +73,7 @@ QUnit.module('section_and_note', (hooks) => {
<field name="invoice_line_ids" widget="section_and_note_one2many">
<tree editable="bottom">
<field name="sequence" widget="handle"/>
<field name="display_type" invisible="1"/>
<field name="display_type" column_invisible="1"/>
<field name="name" widget="section_and_note_text"/>
<field name="price"/>
</tree>
@@ -96,7 +96,7 @@ QUnit.test(
resId: 1,
arch: `
<form>
<field name="description" onchange_on_keydown="True" attrs="{'invisible': [('display_name','=','yop')]}"/>
<field name="description" onchange_on_keydown="True" invisible="display_name == 'yop'"/>
<field name="display_name"/>
</form>`,
mockRPC(route, params) {
@@ -459,7 +459,7 @@ QUnit.module('favorite filter widget', (hooks) => {
<field name="display_name"/>
<field name="subject"/>
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="mailing_model_id" readonly="state != 'draft'"/>
<field name="mailing_filter_count" />
<field name="mailing_filter_id" widget="mailing_filter" options="{'no_create': '1', 'no_open': '1', 'domain_field': 'mailing_domain', 'model': 'mailing_model_id'}"/>
<field name="state" widget="statusbar" options="{'clickable' : '1'}"/>
@@ -109,7 +109,7 @@ QUnit.module('field html', (hooks) => {
arch: `
<form>
<field name="display_name"/>
<field name="body_arch" widget="mass_mailing_html" attrs="{'invisible': [['display_name', '=', 'hide']]}"/>
<field name="body_arch" widget="mass_mailing_html" invisible="display_name == 'hide'"/>
</form>`,
});
@@ -1,6 +1,6 @@
/** @odoo-module */
import { evalDomain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { ListRenderer } from "@web/views/list/list_renderer";
const { onWillUpdateProps } = owl;
@@ -19,14 +19,8 @@ export class ProjectSharingListRenderer extends ListRenderer {
const allColumns = [];
const firstRecord = this.props.list.records[0];
for (const column of columns) {
if (
column.modifiers.column_invisible &&
column.modifiers.column_invisible instanceof Array
) {
const result = evalDomain(column.modifiers.column_invisible, firstRecord.evalContext);
if (result) {
continue;
}
if (evaluateBooleanExpr(column.column_invisible, firstRecord.evalContext)) {
continue;
}
allColumns.push(column);
}
@@ -16,8 +16,7 @@ export class ProjectTaskKanbanRenderer extends KanbanRenderer {
let canMoveRecords = super.canMoveRecords;
if (!canMoveRecords && this.canResequenceRecords && this.props.list.isGroupedByPersonalStages) {
const { groupByField } = this.props.list;
const { modifiers } = groupByField;
canMoveRecords = !(modifiers && modifiers.readonly);
canMoveRecords = groupByField.readonly !== "True";
}
return canMoveRecords;
}
@@ -25,9 +24,8 @@ export class ProjectTaskKanbanRenderer extends KanbanRenderer {
get canResequenceGroups() {
let canResequenceGroups = super.canResequenceGroups;
if (!canResequenceGroups && this.props.list.isGroupedByPersonalStages) {
const { modifiers } = this.props.list.groupByField;
const { groupsDraggable } = this.props.archInfo;
canResequenceGroups = groupsDraggable && !(modifiers && modifiers.readonly);
canResequenceGroups = groupsDraggable && groupsDraggable.readonly !== "True";
}
return canResequenceGroups;
}
@@ -58,7 +58,7 @@ QUnit.module("SectionOneToManyField", (hooks) => {
<form>
<field name="lines" widget="section_one2many">
<tree>
<field name="display_type" invisible="1" />
<field name="display_type" column_invisible="1" />
<field name="title" />
<field name="int" />
</tree>
@@ -93,6 +93,10 @@ registry.category("web_tour.tours").add('sale_timesheet_tour', {
}, {
trigger: 'div[name="timesheet_ids"] td.o_field_x2many_list_row_add a[role="button"]',
content: 'Click on Add a line to create a new timesheet into the task.',
}, {
trigger: '.o_field_x2many div[name="name"] input',
content: 'Enter a description for this timesheet',
run: 'text work',
}, {
trigger: 'div[name="unit_amount"] input',
content: 'Enter one hour for this timesheet',
@@ -76,7 +76,7 @@ QUnit.module("QuestionPageListRenderer", (hooks) => {
<field name="sequence" widget="handle"/>
<field name="title" widget="survey_description_page"/>
<field name="question_type" />
<field name="is_page" invisible="1"/>
<field name="is_page" column_invisible="1"/>
</tree>
</field>
</form>
@@ -108,7 +108,7 @@ QUnit.module("QuestionPageListRenderer", (hooks) => {
<field name="sequence" widget="handle"/>
<field name="title" widget="survey_description_page"/>
<field name="question_type" />
<field name="is_page" invisible="1"/>
<field name="is_page" column_invisible="1"/>
<field name="random_questions_count"/>
</tree>
</field>
@@ -147,7 +147,7 @@ QUnit.module("QuestionPageListRenderer", (hooks) => {
<field name="sequence" widget="handle"/>
<field name="question_type" />
<field name="title" widget="survey_description_page"/>
<field name="is_page" invisible="1"/>
<field name="is_page" column_invisible="1"/>
<field name="random_questions_count"/>
</tree>
</field>
@@ -184,7 +184,7 @@ QUnit.module("QuestionPageListRenderer", (hooks) => {
<field name="random_questions_count"/>
<field name="question_type" />
<field name="title" widget="survey_description_page"/>
<field name="is_page" invisible="1"/>
<field name="is_page" column_invisible="1"/>
</tree>
</field>
</form>
+28 -2
View File
@@ -33,6 +33,32 @@ export function parseExpr(expr) {
* @returns {any}
*/
export function evaluateExpr(expr, context = {}) {
const ast = parseExpr(expr);
return evaluate(ast, context);
let ast;
try {
ast = parseExpr(expr);
} catch (error) {
throw new EvalError(`Can not parse python expression: (${expr})\nError: ${error.message}`);
}
try {
return evaluate(ast, context);
} catch (error) {
throw new EvalError(`Can not evaluate python expression: (${expr})\nError: ${error.message}`);
}
}
/**
* Evaluates a python expression to return a boolean.
*
* @param {string} expr
* @param {Object} [context]
* @returns {any}
*/
export function evaluateBooleanExpr(expr, context = {}) {
if (!expr || expr === 'False' || expr === '0') {
return false;
}
if (expr === 'True' || expr === '1') {
return true;
}
return evaluateExpr(`bool(${expr})`, context);
}
@@ -16,12 +16,15 @@ export const BUILTINS = {
case "boolean":
return value;
case "object":
if (value === null) {
if (value === null || value === undefined) {
return false;
}
if (value.isTrue) {
return value.isTrue();
}
if (value instanceof Array) {
return !!value.length;
}
return true;
}
return true;
@@ -136,6 +136,9 @@ function isIn(left, right) {
if (typeof right === "string" && typeof left === "string") {
return right.includes(left);
}
if (typeof right === "object") {
return left in right;
}
return false;
}
@@ -261,6 +264,15 @@ const DICT = {
},
};
const STRING = {
lower(str) {
return () => str.toLowerCase();
},
upper(str) {
return () => str.toUpperCase();
},
};
// -----------------------------------------------------------------------------
// Evaluate function
// -----------------------------------------------------------------------------
@@ -274,14 +286,16 @@ export function evaluate(ast, context = {}) {
const dicts = new Set();
let pyContext;
const evalContext = Object.create(context);
Object.defineProperty(evalContext, "context", {
get() {
if (!pyContext) {
pyContext = toPyDict(context);
}
return pyContext;
},
});
if (!evalContext.context) {
Object.defineProperty(evalContext, "context", {
get() {
if (!pyContext) {
pyContext = toPyDict(context);
}
return pyContext;
},
});
}
/**
* @param {AST} ast
@@ -362,6 +376,12 @@ export function evaluate(ast, context = {}) {
// this is a dictionary => need to apply dict methods
return DICT[ast.key](left);
}
if (typeof left === 'string') {
return STRING[ast.key](left);
}
if (ast.key == 'get' && typeof left === 'object') {
return DICT[ast.key](toPyDict(left));
}
const result = left[ast.key];
if (typeof result === "function" && !isConstructor(result)) {
return result.bind(left);
@@ -2,7 +2,8 @@
import { markRaw, markup, toRaw } from "@odoo/owl";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { Domain, evalDomain } from "@web/core/domain";
import { Domain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { serializeDate, serializeDateTime } from "@web/core/l10n/dates";
import { _t } from "@web/core/l10n/translation";
import { x2ManyCommands } from "@web/core/orm_service";
@@ -541,17 +542,17 @@ export class Record extends DataPoint {
_isInvisible(fieldName) {
const invisible = this.activeFields[fieldName].invisible;
return invisible ? evalDomain(invisible, this.evalContext) : false;
return invisible ? evaluateBooleanExpr(invisible, this.evalContext) : false;
}
_isReadonly(fieldName) {
const readonly = this.activeFields[fieldName].readonly;
return readonly ? evalDomain(readonly, this.evalContext) : false;
return readonly ? evaluateBooleanExpr(readonly, this.evalContext) : false;
}
_isRequired(fieldName) {
const required = this.activeFields[fieldName].required;
return required ? evalDomain(required, this.evalContext) : false;
return required ? evaluateBooleanExpr(required, this.evalContext) : false;
}
async _load(nextConfig = {}) {
@@ -502,7 +502,8 @@ export class StaticList extends DataPoint {
const changes = {};
for (const fieldName in command[2]) {
if (["one2many", "many2many"].includes(this.fields[fieldName].type)) {
if (record.activeFields[fieldName].invisible) {
const invisible = record.activeFields[fieldName].invisible;
if (invisible === "True" || invisible === "1") {
if (!(command[1] in this._unknownRecordCommands)) {
this._unknownRecordCommands[command[1]] = [];
}
@@ -3,7 +3,6 @@
import { markup, onWillDestroy, onWillStart, onWillUpdateProps, useComponent } from "@odoo/owl";
import { evalPartialContext, makeContext } from "@web/core/context";
import { deserializeDate, deserializeDateTime } from "@web/core/l10n/dates";
import { Domain } from "@web/core/domain";
import { x2ManyCommands } from "@web/core/orm_service";
import { Deferred } from "@web/core/utils/concurrency";
import { omit } from "@web/core/utils/objects";
@@ -22,9 +21,9 @@ export function makeActiveField({
} = {}) {
return {
context: context || "{}",
invisible: invisible || false,
readonly: readonly || false,
required: required || false,
invisible: invisible || "False",
readonly: readonly || "False",
required: required || "False",
onChange: onChange || false,
forceSave: forceSave || false,
isHandle: isHandle || false,
@@ -74,7 +73,7 @@ export function completeActiveFields(activeFields, extraActiveFields) {
for (const fieldName in extraActiveFields) {
const extraActiveField = {
...extraActiveFields[fieldName],
invisible: true,
invisible: 'True',
};
if (fieldName in activeFields) {
completeActiveField(activeFields[fieldName], extraActiveField);
@@ -103,29 +102,29 @@ export function createPropertyActiveField(property) {
return activeField;
}
function combineModifiers(mod1, mod2, operator) {
export function combineModifiers(mod1, mod2, operator) {
if (operator === "AND") {
if (mod1 === false || mod2 === false) {
return false;
if (!mod1 || mod1 === "False" || !mod2 || mod2 === "False") {
return "False";
}
if (mod1 === true) {
if (mod1 === "True") {
return mod2;
}
if (mod2 === true) {
if (mod2 === "True") {
return mod1;
}
return Domain.and([mod1, mod2]).toString();
return "(" + mod1 + ") and (" + mod2 + ")";
} else if (operator === "OR") {
if (mod1 === true || mod2 === true) {
return true;
if (mod1 === "True" || mod2 === "True") {
return "True";
}
if (mod1 === false) {
if (!mod1 || mod1 === "False") {
return mod2;
}
if (mod2 === false) {
if (!mod2 || mod2 === "False") {
return mod1;
}
return Domain.or([mod1, mod2]).toString();
return "(" + mod1 + ") or (" + mod2 + ")";
}
throw new Error(
`Operator provided to "combineModifiers" must be "AND" or "OR", received ${operator}`
@@ -166,12 +165,11 @@ export function extractFieldsFromArchInfo({ fieldNodes, widgetNodes }, fields) {
const activeFields = {};
for (const fieldNode of Object.values(fieldNodes)) {
const fieldName = fieldNode.name;
const modifiers = fieldNode.modifiers || {};
const activeField = makeActiveField({
context: fieldNode.context,
invisible: modifiers.invisible || modifiers.column_invisible,
readonly: modifiers.readonly,
required: modifiers.required,
invisible: combineModifiers(fieldNode.invisible, fieldNode.column_invisible, "OR"),
readonly: fieldNode.readonly,
required: fieldNode.required,
onChange: fieldNode.onChange,
forceSave: fieldNode.forceSave,
isHandle: fieldNode.isHandle,
@@ -205,7 +203,7 @@ export function extractFieldsFromArchInfo({ fieldNodes, widgetNodes }, fields) {
}
}
if (fieldNode.field?.useSubView) {
activeField.required = false;
activeField.required = 'False';
}
}
@@ -272,7 +270,7 @@ export function getFieldsSpec(
const { related, limit, defaultOrderBy, invisible } = activeFields[fieldName];
fieldsSpec[fieldName] = {};
// X2M
if (related && (invisible !== true || withInvisible)) {
if (related && ((invisible !== "True" && invisible !== "1") || withInvisible)) {
fieldsSpec[fieldName].fields = getFieldsSpec(
related.activeFields,
related.fields,
@@ -291,7 +289,7 @@ export function getFieldsSpec(
// M2O
if (fields[fieldName].type === "many2one") {
fieldsSpec[fieldName].fields = {};
if (invisible !== true) {
if (invisible !== "True" && invisible !== "1") {
fieldsSpec[fieldName].fields.display_name = {};
}
}
@@ -2,7 +2,7 @@
import { makeContext } from "@web/core/context";
import { _t } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
import { XMLParser } from "@web/core/utils/xml";
import { DEFAULT_INTERVAL, DEFAULT_PERIOD } from "@web/search/utils/dates";
@@ -109,9 +109,8 @@ export class SearchArchParser extends XMLParser {
visitField(node) {
this.pushGroup("field");
const preField = { type: "field" };
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
if (modifiers.invisible === true) {
preField.invisible = true;
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
preField.invisible = "True";
}
if (node.hasAttribute("domain")) {
preField.domain = node.getAttribute("domain");
@@ -221,9 +220,8 @@ export class SearchArchParser extends XMLParser {
preSearchItem.domain = stringRepr;
}
}
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
if (modifiers.invisible === true) {
preSearchItem.invisible = true;
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
preSearchItem.invisible = "True";
const fieldName = preSearchItem.fieldName;
if (fieldName && !this.fields[fieldName]) {
// In some case when a field is limited to specific groups
@@ -290,8 +288,7 @@ export class SearchArchParser extends XMLParser {
if (node.nodeType !== 1 || node.tagName !== "field") {
continue;
}
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
if (modifiers.invisible === true) {
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
continue;
}
const attrs = {};
@@ -303,8 +300,8 @@ export class SearchArchParser extends XMLParser {
const section = {
color: attrs.color || null,
description: attrs.string || this.fields[attrs.name].string,
enableCounters: Boolean(evaluateExpr(attrs.enable_counters || "0")),
expand: Boolean(evaluateExpr(attrs.expand || "0")),
enableCounters: evaluateBooleanExpr(attrs.enable_counters),
expand: evaluateBooleanExpr(attrs.expand),
fieldName: attrs.name,
icon: attrs.icon || null,
id: nextSectionId++,
@@ -315,7 +312,7 @@ export class SearchArchParser extends XMLParser {
if (type === "category") {
section.activeValueId = this.searchPanelDefaults[attrs.name];
section.icon = section.icon || "fa-folder";
section.hierarchize = Boolean(evaluateExpr(attrs.hierarchize || "1"));
section.hierarchize = evaluateBooleanExpr(attrs.hierarchize || "1");
section.values.set(false, {
childrenIds: [],
display_name: ALL.toString(),
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { Dialog } from "@web/core/dialog/dialog";
import { evalDomain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { is24HourFormat } from "@web/core/l10n/dates";
import { Field } from "@web/views/fields/field";
import { Record } from "@web/views/record";
@@ -33,7 +33,7 @@ export class CalendarCommonPopover extends Component {
}
isInvisible(fieldNode, record) {
return evalDomain(fieldNode.modifiers.invisible, record.evalContext);
return evaluateBooleanExpr(fieldNode.invisible, record.evalContext);
}
computeDateTimeAndDuration() {
+6 -3
View File
@@ -2,7 +2,7 @@
import { _t } from "@web/core/l10n/translation";
import { Dialog } from "@web/core/dialog/dialog";
import { evalDomain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { editModelDebug } from "@web/core/debug/debug_utils";
import { formatDateTime, deserializeDateTime } from "@web/core/l10n/dates";
import { registry } from "@web/core/registry";
@@ -69,6 +69,9 @@ export function editView({ accessRights, component, env }) {
type = component.props.viewInfo.type;
type = type === "tree" ? "list" : type;
}
if (!type) {
return;
}
const displayName = type[0].toUpperCase() + type.slice(1);
const description = env._t("Edit View: ") + displayName;
return {
@@ -221,8 +224,8 @@ class SetDefaultDialog extends Component {
// ignore fields which are empty, invisible, readonly, o2m or m2m
if (
!value ||
evalDomain(this.activeFields[fieldName].invisible, evalContext) ||
evalDomain(this.activeFields[fieldName].readonly, evalContext) ||
evaluateBooleanExpr(this.activeFields[fieldName].invisible, evalContext) ||
evaluateBooleanExpr(this.activeFields[fieldName].readonly, evalContext) ||
fieldInfo.type === "one2many" ||
fieldInfo.type === "many2many" ||
fieldInfo.type === "binary" ||
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
@@ -28,7 +28,7 @@ export class BadgeField extends Component {
get classFromDecoration() {
const evalContext = this.props.record.evalContext;
for (const decorationName in this.props.decorations) {
if (evaluateExpr(this.props.decorations[decorationName], evalContext)) {
if (evaluateBooleanExpr(this.props.decorations[decorationName], evalContext)) {
return `text-bg-${decorationName}`;
}
}
@@ -1,8 +1,7 @@
/** @odoo-module **/
import { evalDomain } from "@web/core/domain";
import { _t } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { omit } from "@web/core/utils/objects";
@@ -37,12 +36,7 @@ class CopyClipboardField extends Component {
return this.props.record.fields[this.props.name].type;
}
get disabled() {
const context = this.props.record.evalContext;
const evaluated = this.props.disabledExpr ? evaluateExpr(this.props.disabledExpr) : false;
if (evaluated instanceof Array) {
return evalDomain(evaluated, context);
}
return Boolean(evaluated);
return this.props.disabledExpr ? evaluateBooleanExpr(this.props.disabledExpr, this.props.record.evalContext) : false;
}
}
@@ -24,7 +24,7 @@ import { standardFieldProps } from "../standard_field_props";
* maxDate?: string;
* minDate?: string;
* placeholder?: string;
* required?: boolean;
* required?: string | boolean;
* rounding?: number;
* startDateField?: string;
* warnFuture?: boolean;
@@ -41,7 +41,7 @@ export class DateTimeField extends Component {
maxDate: { type: String, optional: true },
minDate: { type: String, optional: true },
placeholder: { type: String, optional: true },
required: { type: Boolean, optional: true },
required: { type: [Boolean, String], optional: true },
rounding: { type: Number, optional: true },
startDateField: { type: String, optional: true },
warnFuture: { type: Boolean, optional: true },
@@ -267,27 +267,27 @@ export const dateField = {
},
],
supportedTypes: ["date"],
extractProps: ({ attrs, modifiers, options }) => ({
extractProps: ({ attrs, options }, dynamicInfo) => ({
endDateField: options[END_DATE_FIELD_OPTION],
maxDate: options.max_date,
minDate: options.min_date,
placeholder: attrs.placeholder,
required: Boolean(modifiers.required),
required: 'required' in attrs ? attrs.required : dynamicInfo.required,
rounding: options.rounding && parseInt(options.rounding, 10),
startDateField: options[START_DATE_FIELD_OPTION],
warnFuture: archParseBoolean(options.warn_future),
}),
fieldDependencies: ({ type, modifiers, options }) => {
fieldDependencies: ({ type, attrs, options }) => {
const deps = [];
if (options[START_DATE_FIELD_OPTION]) {
deps.push({ name: options[START_DATE_FIELD_OPTION], type, modifiers });
deps.push({ name: options[START_DATE_FIELD_OPTION], type, attrs });
if (options[END_DATE_FIELD_OPTION]) {
console.warn(
`A field cannot have both ${START_DATE_FIELD_OPTION} and ${END_DATE_FIELD_OPTION} options at the same time`
);
}
} else if (options[END_DATE_FIELD_OPTION]) {
deps.push({ name: options[END_DATE_FIELD_OPTION], type, modifiers });
deps.push({ name: options[END_DATE_FIELD_OPTION], type, attrs });
}
return deps;
},
+38 -35
View File
@@ -1,11 +1,15 @@
/** @odoo-module **/
import { Domain, evalDomain } from "@web/core/domain";
import { evaluateExpr } from "@web/core/py_js/py";
import { Domain } from "@web/core/domain";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { utils } from "@web/core/ui/ui_service";
import { getFieldContext } from "@web/model/relational_model/utils";
import { archParseBoolean, getClassNameFromDecoration, X2M_TYPES } from "@web/views/utils";
import {
archParseBoolean,
getClassNameFromDecoration,
X2M_TYPES,
} from "@web/views/utils";
import { getTooltipInfo } from "./field_tooltip";
import { Component, xml } from "@odoo/owl";
@@ -39,8 +43,8 @@ export function getFieldFromRegistry(fieldType, widget, viewType, jsClass) {
}
export function fieldVisualFeedback(field, record, fieldName, fieldInfo) {
const modifiers = fieldInfo.modifiers || {};
const readonly = evalDomain(modifiers.readonly, record.evalContext);
const readonly = evaluateBooleanExpr(fieldInfo.readonly, record.evalContext);
const required = evaluateBooleanExpr(fieldInfo.required, record.evalContext);
const inEdit = record.isInEdition;
let empty = !record.isNew;
@@ -52,7 +56,7 @@ export function fieldVisualFeedback(field, record, fieldName, fieldInfo) {
empty = inEdit ? empty && readonly : empty;
return {
readonly,
required: evalDomain(modifiers.required, record.evalContext),
required,
invalid: record.isFieldInvalid(fieldName),
empty,
};
@@ -67,7 +71,10 @@ export function getPropertyFieldInfo(propertyField) {
type,
widget: type,
options: {},
modifiers: {},
column_invisible: "False",
invisible: "False",
readonly: "False",
required: "False",
attrs: {},
relatedPropertyField,
@@ -76,7 +83,6 @@ export function getPropertyFieldInfo(propertyField) {
help: undefined,
onChange: false,
forceSave: false,
alwaysInvisible: false,
decorations: {},
// ???
};
@@ -150,7 +156,7 @@ export class Field extends Component {
const { decorations } = fieldInfo;
const evalContext = record.evalContext;
for (const decoName in decorations) {
const value = evaluateExpr(decorations[decoName], evalContext);
const value = evaluateBooleanExpr(decorations[decoName], evalContext);
classNames[getClassNameFromDecoration(decoName)] = value;
}
}
@@ -164,16 +170,13 @@ export class Field extends Component {
get fieldComponentProps() {
const record = this.props.record;
const evalContext = record.evalContext;
const readonly = this.props.readonly === true;
let readonly = this.props.readonly || false;
let readonlyFromModifiers = false;
let propsFromNode = {};
if (this.props.fieldInfo) {
const evalContext = record.getEvalContext?.(false) || record.evalContext;
let fieldInfo = this.props.fieldInfo;
const modifiers = fieldInfo.modifiers || {};
readonlyFromModifiers = evalDomain(modifiers.readonly, evalContext);
readonly = readonly || evaluateBooleanExpr(fieldInfo.readonly, evalContext);
if (this.field.extractProps) {
if (this.props.attrs) {
@@ -182,14 +185,13 @@ export class Field extends Component {
attrs: { ...fieldInfo.attrs, ...this.props.attrs },
};
}
const dynamicInfo = {
get context() {
return getFieldContext(record, fieldInfo.name, fieldInfo.context);
},
domain() {
const evalContext = record.getEvalContext
? record.getEvalContext(true)
: record.evalContext;
const evalContext = record.getEvalContext?.(true) || record.evalContext;
if (fieldInfo.domain) {
return new Domain(evaluateExpr(fieldInfo.domain, evalContext)).toList();
}
@@ -198,14 +200,8 @@ export class Field extends Component {
? new Domain(evaluateExpr(domain, evalContext)).toList()
: domain || [];
},
readonly: readonly || readonlyFromModifiers,
get required() {
return (
fieldInfo.modifiers &&
fieldInfo.modifiers.required &&
evalDomain(fieldInfo.modifiers.required, record.evalContext)
);
},
required: evaluateBooleanExpr(fieldInfo.required, evalContext),
readonly: readonly,
};
propsFromNode = this.field.extractProps(fieldInfo, dynamicInfo);
}
@@ -221,7 +217,7 @@ export class Field extends Component {
delete props.readonly;
return {
readonly: readonly || !record.isInEdition || readonlyFromModifiers || false,
readonly: readonly || !record.isInEdition || false,
...propsFromNode,
...props,
};
@@ -246,13 +242,15 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
const name = node.getAttribute("name");
const widget = node.getAttribute("widget");
const fields = models[modelName];
if (!fields[name]) {
throw new Error(`"${modelName}"."${name}" field is undefined.`);
}
const field = getFieldFromRegistry(fields[name].type, widget, viewType, jsClass);
const fieldInfo = {
name,
type: fields[name].type,
viewType,
widget,
modifiers: {},
field,
context: "{}",
string: fields[name].string,
@@ -260,12 +258,22 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
onChange: false,
forceSave: false,
options: {},
alwaysInvisible: false,
decorations: {},
attrs: {},
domain: undefined,
};
for (const attr of ["invisible", "column_invisible", "readonly", "required"]) {
fieldInfo[attr] = node.getAttribute(attr);
if (fieldInfo[attr] === "True") {
if (attr === "column_invisible") {
fieldInfo.invisible = "True";
}
} else if (fieldInfo[attr] === null && fields[name][attr]) {
fieldInfo[attr] = "True";
}
}
for (const { name, value } of node.attributes) {
if (["name", "widget"].includes(name)) {
// avoid adding name and widget to attrs
@@ -273,11 +281,6 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
}
if (["context", "string", "help", "domain"].includes(name)) {
fieldInfo[name] = value;
} else if (name === "modifiers") {
fieldInfo.modifiers = JSON.parse(value);
fieldInfo.alwaysInvisible =
fieldInfo.modifiers.invisible === true ||
fieldInfo.modifiers.column_invisible === true;
} else if (name === "on_change") {
fieldInfo.onChange = archParseBoolean(value);
} else if (name === "options") {
@@ -293,7 +296,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
}
}
if (name === "id") {
fieldInfo.modifiers.readonly = true;
fieldInfo.readonly = 'True';
}
if (widget === "handle") {
@@ -18,7 +18,10 @@ export function getTooltipInfo(params) {
widgetDescription,
context: params.fieldInfo.context,
domain: params.field.domain,
modifiers: JSON.stringify(params.fieldInfo.modifiers),
invisible: params.fieldInfo.invisible,
column_invisible: params.fieldInfo.column_invisible,
readonly: params.fieldInfo.readonly,
required: params.fieldInfo.required,
changeDefault: params.field.change_default,
relation: params.field.relation,
selection: params.field.selection,
@@ -40,9 +40,21 @@
<span class="o-tooltip--technical--title">Domain:</span>
<t t-esc="field.domain"/>
</li>
<li t-if="field.modifiers" data-item="modifiers">
<span class="o-tooltip--technical--title">Modifiers:</span>
<t t-esc="field.modifiers"/>
<li t-if="'invisible' in field" data-item="invisible">
<span class="o-tooltip--technical--title">Invisible:</span>
<t t-esc="field.invisible"/>
</li>
<li t-if="'column_invisible' in field" data-item="column_invisible">
<span class="o-tooltip--technical--title">Column invisible:</span>
<t t-esc="field.column_invisible"/>
</li>
<li t-if="'required' in field" data-item="required">
<span class="o-tooltip--technical--title">Required:</span>
<t t-esc="field.required"/>
</li>
<li t-if="'readonly' in field" data-item="readonly">
<span class="o-tooltip--technical--title">Readonly:</span>
<t t-esc="field.readonly"/>
</li>
<li t-if="field.default" data-item="default">
<span class="o-tooltip--technical--title">Default:</span>
@@ -5,7 +5,7 @@ import { isMobileOS } from "@web/core/browser/feature_detection";
import { makeContext } from "@web/core/context";
import { Dialog } from "@web/core/dialog/dialog";
import { _t } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { useChildRef, useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { Many2XAutocomplete, useOpenMany2XRecord } from "@web/views/fields/relational_utils";
@@ -168,7 +168,7 @@ export class Many2OneField extends Component {
get classFromDecoration() {
const evalContext = this.props.record.evalContext;
for (const decorationName in this.props.decorations) {
if (evaluateExpr(this.props.decorations[decorationName], evalContext)) {
if (evaluateBooleanExpr(this.props.decorations[decorationName], evalContext)) {
return `text-${decorationName}`;
}
}
@@ -217,8 +217,11 @@ export class Many2OneField extends Component {
};
}
getDomain() {
const domain = this.props.domain;
return typeof domain === "function" ? domain() : domain;
let domain = this.props.domain;
if (typeof domain === "function") {
domain = domain();
}
return domain;
}
async openAction() {
const action = await this.orm.call(this.relation, "get_formview_action", [[this.resId]], {
@@ -3,7 +3,7 @@
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { makeContext } from "@web/core/context";
import { Dialog } from "@web/core/dialog/dialog";
import { evalDomain } from "@web/core/domain";
import { Domain } from "@web/core/domain";
import { RPCError } from "@web/core/network/rpc_service";
import { Cache } from "@web/core/utils/cache";
import {
@@ -121,7 +121,7 @@ export function useActiveActions({
let evalFn = () => actionName !== "write";
if (!isNull(crudOptions[actionName])) {
const action = crudOptions[actionName];
evalFn = (evalContext) => evalDomain(action, evalContext);
evalFn = (evalContext) => Boolean(action && new Domain(action).contains(evalContext));
}
if (actionName in subViewActiveActions) {
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { makeContext } from "@web/core/context";
import { evalDomain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { _t } from "@web/core/l10n/translation";
import { Pager } from "@web/core/pager/pager";
import { registry } from "@web/core/registry";
@@ -191,6 +191,9 @@ export class X2ManyField extends Component {
archInfo,
list: this.list,
openRecord: this.openRecord.bind(this),
evalViewModifier: (modifier) => {
return evaluateBooleanExpr(modifier, this.list.evalContext);
},
};
if (this.props.viewMode === "kanban") {
@@ -207,32 +210,9 @@ export class X2ManyField extends Component {
return props;
}
// handle column_invisible modifiers
const columns = archInfo.columns
.map((col) => {
// first remove (column_)invisible buttons in button_groups
if (col.type === "button_group") {
const buttons = col.buttons.filter((button) => {
return !this.evalColumnInvisibleModifier(button.modifiers);
});
return { ...col, buttons };
}
return col;
})
.filter((col) => {
// filter out (column_)invisible fields and empty button_groups
if (col.type === "field") {
return !this.evalColumnInvisibleModifier(col.modifiers);
} else if (col.type === "button_group") {
return col.buttons.length > 0;
}
return true;
});
const editable =
(this.archInfo.activeActions.edit && archInfo.editable) || this.props.editable;
props.activeActions = this.activeActions;
props.archInfo = { ...archInfo, columns };
props.cycleOnTab = false;
props.editable = !this.props.readonly && editable;
props.nestedKeyOptionalFieldsData = this.nestedKeyOptionalFieldsData;
@@ -244,13 +224,6 @@ export class X2ManyField extends Component {
return props;
}
evalColumnInvisibleModifier(modifiers) {
if ("column_invisible" in modifiers) {
return evalDomain(modifiers.column_invisible, this.list.evalContext);
}
return false;
}
async onAdd({ context, editable } = {}) {
const domain =
typeof this.props.domain === "function" ? this.props.domain() : this.props.domain;
@@ -134,17 +134,19 @@ export class FormCompiler extends ViewCompiler {
let hasContent = false;
for (const child of el.children) {
const invisible = getModifier(child, "invisible");
if (this.isAlwaysInvisible(invisible, params)) {
if (!params.compileInvisibleNodes && (invisible === "True" || invisible === "1")) {
continue;
}
hasContent = true;
let isVisibleExpr;
if (typeof invisible === "boolean") {
isVisibleExpr = `${invisible ? false : true}`;
if (!invisible || invisible === "False" || invisible === "0") {
isVisibleExpr = "true";
} else if (invisible === "True" || invisible === "1") {
isVisibleExpr = "false";
} else {
isVisibleExpr = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(
invisible
)})`;
)},__comp__.props.record.evalContext)`;
}
const mainSlot = createElement("t", {
"t-set-slot": `slot_${slotId++}`,
@@ -277,7 +279,7 @@ export class FormCompiler extends ViewCompiler {
}
const invisible = getModifier(child, "invisible");
if (this.isAlwaysInvisible(invisible, params)) {
if (!params.compileInvisibleNodes && (invisible === "True" || invisible === "1")) {
continue;
}
@@ -340,12 +342,14 @@ export class FormCompiler extends ViewCompiler {
if (slotContent && !isTextNode(slotContent)) {
let isVisibleExpr;
if (typeof invisible === "boolean") {
isVisibleExpr = `${invisible ? false : true}`;
if (!invisible || invisible === "False" || invisible === "0") {
isVisibleExpr = "true";
} else if (invisible === "True" || invisible === "1") {
isVisibleExpr = "false";
} else {
isVisibleExpr = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(
invisible
)})`;
)},__comp__.props.record.evalContext)`;
}
mainSlot.setAttribute("isVisible", isVisibleExpr);
if (itemSpan > 0) {
@@ -499,7 +503,7 @@ export class FormCompiler extends ViewCompiler {
continue;
}
const invisible = getModifier(child, "invisible");
if (this.isAlwaysInvisible(invisible, params)) {
if (!params.compileInvisibleNodes && (invisible === "True" || invisible === "1")) {
continue;
}
@@ -537,15 +541,17 @@ export class FormCompiler extends ViewCompiler {
};
}
let isVisible;
if (typeof invisible === "boolean") {
isVisible = `${!invisible}`;
let isVisibleExpr;
if (!invisible || invisible === "False" || invisible === "0") {
isVisibleExpr = "true";
} else if (invisible === "True" || invisible === "1") {
isVisibleExpr = "false";
} else {
isVisible = `!__comp__.evalDomainFromRecord(__comp__.props.record,${JSON.stringify(
isVisibleExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(
invisible
)})`;
)},__comp__.props.record.evalContext)`;
}
pageSlot.setAttribute("isVisible", isVisible);
pageSlot.setAttribute("isVisible", isVisibleExpr);
for (const contents of child.children) {
append(pageSlot, this.compileNode(contents, { ...params, currentSlot: pageSlot }));
@@ -3,13 +3,13 @@
import { hasTouch } from "@web/core/browser/feature_detection";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { makeContext } from "@web/core/context";
import { evalDomain } from "@web/core/domain";
import { useDebugCategory } from "@web/core/debug/debug_context";
import { registry } from "@web/core/registry";
import { SIZES } from "@web/core/ui/ui_service";
import { useBus, useService } from "@web/core/utils/hooks";
import { omit } from "@web/core/utils/objects";
import { createElement } from "@web/core/utils/xml";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
import { standardViewProps } from "@web/views/standard_view_props";
@@ -48,7 +48,7 @@ export async function loadSubViews(
if (!isX2Many(field)) {
continue; // what follows only concerns x2many fields
}
if (fieldInfo.alwaysInvisible) {
if (fieldInfo.invisible === "True" || fieldInfo.invisible === "1") {
continue; // no need to fetch the sub view if the field is always invisible
}
if (!fieldInfo.field.useSubView) {
@@ -110,6 +110,7 @@ export async function loadSubViews(
export class FormController extends Component {
setup() {
this.evaluateBooleanExpr = evaluateBooleanExpr;
this.dialogService = useService("dialog");
this.router = useService("router");
this.user = useService("user");
@@ -559,10 +560,6 @@ export class FormController extends Component {
result["o_field_highlight"] = size < SIZES.SM || hasTouch();
return result;
}
evalDomainFromRecord(record, expr) {
return evalDomain(expr, record.evalContext);
}
}
FormController.template = `web.FormView`;
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { evalDomain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { Notebook } from "@web/core/notebook/notebook";
import { Setting } from "./setting/setting";
import { Field } from "@web/views/fields/field";
@@ -30,6 +30,7 @@ import {
export class FormRenderer extends Component {
setup() {
this.evaluateBooleanExpr = evaluateBooleanExpr;
const { archInfo, Compiler, record } = this.props;
const { arch, xmlDoc } = archInfo;
const templates = { FormRenderer: xmlDoc };
@@ -79,10 +80,6 @@ export class FormRenderer extends Component {
get shouldAutoFocus() {
return !this.props.archInfo.disableAutofocus;
}
evalDomainFromRecord(record, expr) {
return evalDomain(expr, record.evalContext);
}
}
FormRenderer.template = xml`<t t-call="{{ templates.FormRenderer }}" t-call-context="{ __comp__: Object.assign(Object.create(this), { this: this }) }" />`;
@@ -9,10 +9,7 @@ export class Setting extends Component {
setup() {
if (this.props.fieldName) {
this.fieldType = this.props.record.fields[this.props.fieldName].type;
if (
typeof this.props.fieldInfo.modifiers.readonly === "boolean" &&
this.props.fieldInfo.modifiers.readonly === true
) {
if (this.props.fieldInfo.readonly === "True") {
this.notMuttedLabel = true;
}
}
@@ -55,8 +55,7 @@ export class GraphArchParser extends XMLParser {
}
archInfo.fieldAttrs[fieldName].string = string;
}
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
if (modifiers.invisible === true) {
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
if (!archInfo.fieldAttrs[fieldName]) {
archInfo.fieldAttrs[fieldName] = {};
}
@@ -73,7 +73,7 @@ export class KanbanArchParser extends XMLParser {
type: "button",
id: button_id++,
}))
.filter((button) => button.modifiers.invisible !== true);
.filter((button) => button.invisible !== "True" && button.invisible !== "1");
return false;
} else if (node.tagName === "control") {
for (const childNode of node.children) {
@@ -4,6 +4,7 @@ import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_d
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
import { CogMenu } from "@web/search/cog_menu/cog_menu";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
import { SearchBar } from "@web/search/search_bar/search_bar";
@@ -206,6 +207,10 @@ export class KanbanController extends Component {
});
}
evalViewModifier(modifier) {
return evaluateBooleanExpr(modifier, {context: this.props.context});
};
async openRecord(record, mode) {
const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);
this.props.selectRecord(record.resId, { activeIds, mode });
@@ -51,6 +51,7 @@
canQuickCreate="canQuickCreate"
quickCreateState="quickCreateState"
progressBarState="progressBarState"
evalViewModifier.bind="evalViewModifier"
/>
</Layout>
</div>
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { ColorList } from "@web/core/colorlist/colorlist";
import { evalDomain } from "@web/core/domain";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { registry } from "@web/core/registry";
@@ -179,6 +179,7 @@ export function isHtmlEmpty(innerHTML = "") {
export class KanbanRecord extends Component {
setup() {
this.evaluateBooleanExpr = evaluateBooleanExpr;
this.action = useService("action");
this.dialog = useService("dialog");
this.notification = useService("notification");
@@ -243,10 +244,6 @@ export class KanbanRecord extends Component {
};
}
evalDomainFromRecord(record, expr) {
return evalDomain(expr, record.evalContext);
}
getRecordClasses() {
const { archInfo, canResequence, forceGlobalClick, record, progressBarState } = this.props;
const classes = ["o_kanban_record d-flex"];
@@ -21,7 +21,7 @@ const DEFAULT_QUICK_CREATE_VIEW = {
// note: the required modifier is written in the format returned by the server
arch: /* xml */ `
<form>
<field name="display_name" placeholder="Title" modifiers='{"required": true}' />
<field name="display_name" placeholder="Title" required="True" />
</form>`,
};
const DEFAULT_QUICK_CREATE_FIELDS = {
@@ -51,6 +51,7 @@ export class KanbanRenderer extends Component {
"deleteRecord",
"openRecord",
"readonly",
"evalViewModifier",
"forceGlobalClick?",
"noContentHelp?",
"scrollTop?",
@@ -221,14 +222,13 @@ export class KanbanRenderer extends Component {
const fieldNodes = Object.values(this.props.archInfo.fieldNodes).filter(
(fieldNode) => fieldNode.name === groupByField.name
);
let isReadonly;
let isReadonly = this.props.list.fields[groupByField.name].readonly;
if (
!isReadonly &&
fieldNodes.length &&
fieldNodes.some((fieldNode) => "readonly" in fieldNode.modifiers)
fieldNodes.some((fieldNode) => "readonly" in fieldNode)
) {
isReadonly = fieldNodes.every((fieldNode) => fieldNode.modifiers.readonly === true);
} else {
isReadonly = this.props.list.fields[groupByField.name].readonly;
isReadonly = fieldNodes.every((fieldNode) => fieldNode.readonly === 'True');
}
return !isReadonly && this.isMovableField(groupByField);
}
@@ -1,10 +1,11 @@
/** @odoo-module */
import { Field } from "@web/views/fields/field";
import { XMLParser } from "@web/core/utils/xml";
import { stringToOrderBy } from "@web/search/utils/order_by";
import { Field } from "@web/views/fields/field";
import { archParseBoolean, getActiveActions, getDecoration, processButton } from "@web/views/utils";
import { encodeObjectForTemplate } from "@web/views/view_compiler";
import { combineModifiers } from "@web/model/relational_model/utils";
import { Widget } from "@web/views/widgets/widget";
export class GroupListArchParser extends XMLParser {
@@ -36,10 +37,6 @@ export class GroupListArchParser extends XMLParser {
}
export class ListArchParser extends XMLParser {
isColumnVisible(columnInvisibleModifier) {
return columnInvisibleModifier !== true;
}
parseFieldNode(node, models, modelName) {
return Field.parseFieldNode(node, models, modelName, "list");
}
@@ -77,25 +74,24 @@ export class ListArchParser extends XMLParser {
buttonGroup = undefined;
}
if (node.tagName === "button") {
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
if (this.isColumnVisible(modifiers.column_invisible)) {
const button = {
...this.processButton(node),
defaultRank: "btn-link",
type: "button",
id: buttonId++,
const button = {
...this.processButton(node),
defaultRank: "btn-link",
type: "button",
id: buttonId++,
};
if (buttonGroup) {
buttonGroup.buttons.push(button);
buttonGroup.column_invisible = combineModifiers(buttonGroup.column_invisible, node.getAttribute('column_invisible'), "AND");
} else {
buttonGroup = {
id: `column_${nextId++}`,
type: "button_group",
buttons: [button],
hasLabel: false,
column_invisible: node.getAttribute('column_invisible'),
};
if (buttonGroup) {
buttonGroup.buttons.push(button);
} else {
buttonGroup = {
id: `column_${nextId++}`,
type: "button_group",
buttons: [button],
hasLabel: false,
};
columns.push(buttonGroup);
}
columns.push(buttonGroup);
}
} else if (node.tagName === "field") {
const fieldInfo = this.parseFieldNode(node, models, modelName);
@@ -108,20 +104,18 @@ export class ListArchParser extends XMLParser {
if (fieldInfo.isHandle) {
handleField = fieldInfo.name;
}
if (this.isColumnVisible(fieldInfo.modifiers.column_invisible)) {
const label = fieldInfo.field.label;
columns.push({
...fieldInfo,
id: `column_${nextId++}`,
className: node.getAttribute("class"), // for oe_edit_only and oe_read_only
optional: node.getAttribute("optional") || false,
type: "field",
hasLabel: !(
archParseBoolean(fieldInfo.attrs.nolabel) || fieldInfo.field.noLabel
),
label: (fieldInfo.widget && label && label.toString()) || fieldInfo.string,
});
}
const label = fieldInfo.field.label;
columns.push({
...fieldInfo,
id: `column_${nextId++}`,
className: node.getAttribute("class"), // for oe_edit_only and oe_read_only
optional: node.getAttribute("optional") || false,
type: "field",
hasLabel: !(
archParseBoolean(fieldInfo.attrs.nolabel) || fieldInfo.field.noLabel
),
label: (fieldInfo.widget && label && label.toString()) || fieldInfo.string,
});
return false;
} else if (node.tagName === "widget") {
const widgetInfo = this.parseWidgetNode(node);
@@ -155,7 +149,7 @@ export class ListArchParser extends XMLParser {
};
return false;
} else if (node.tagName === "header") {
// AAB: not sure we need to handle invisible="1" button as the usecase seems way
// AAB: not sure we need to handle invisible="True" button as the usecase seems way
// less relevant than for fields (so for buttons, relying on the modifiers logic
// that applies later on could be enough, even if the value is always true)
headerButtons = [...node.children]
@@ -163,8 +157,7 @@ export class ListArchParser extends XMLParser {
...this.processButton(node),
type: "button",
id: buttonId++,
}))
.filter((button) => button.modifiers.invisible !== true);
}));
return false;
} else if (node.tagName === "control") {
for (const childNode of node.children) {
@@ -2,7 +2,7 @@
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { download } from "@web/core/network/download";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
import { unique } from "@web/core/utils/arrays";
import { useService, useBus } from "@web/core/utils/hooks";
import { omit } from "@web/core/utils/objects";
@@ -380,6 +380,10 @@ export class ListController extends Component {
this.model.root.selectDomain(false);
}
evalViewModifier(modifier) {
return evaluateBooleanExpr(modifier, this.model.root.evalContext);
}
get className() {
return this.props.className;
}
@@ -44,7 +44,7 @@
<t t-set-slot="control-panel-selection-actions">
<div t-if="nbSelected" class="d-flex gap-1">
<t t-call="web.ListView.Selection"/>
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id">
<t t-foreach="archInfo.headerButtons" t-as="button" t-key="button.id" t-if="!evalViewModifier(button.invisible)">
<MultiRecordViewButton
t-if="button.display !== 'always'"
list="model.root"
@@ -69,7 +69,18 @@
</t>
</div>
</t>
<t t-component="props.Renderer" list="model.root" activeActions="activeActions" archInfo="archInfo" allowSelectors="props.allowSelectors" editable="editable" openRecord.bind="openRecord" noContentHelp="props.info.noContentHelp" onAdd.bind="createRecord" onOptionalFieldsChanged.bind="onOptionalFieldsChanged"/>
<t t-component="props.Renderer"
list="model.root"
activeActions="activeActions"
archInfo="archInfo"
allowSelectors="props.allowSelectors"
editable="editable"
openRecord.bind="openRecord"
noContentHelp="props.info.noContentHelp"
onAdd.bind="createRecord"
onOptionalFieldsChanged.bind="onOptionalFieldsChanged"
evalViewModifier.bind="evalViewModifier"
/>
</Layout>
</div>
</t>
@@ -2,12 +2,11 @@
import { browser } from "@web/core/browser/browser";
import { CheckBox } from "@web/core/checkbox/checkbox";
import { Domain, evalDomain } from "@web/core/domain";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { Pager } from "@web/core/pager/pager";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { useBus, useService } from "@web/core/utils/hooks";
import { useSortable } from "@web/core/utils/sortable";
@@ -15,6 +14,7 @@ import { getTabableElements } from "@web/core/utils/ui";
import { Field, getPropertyFieldInfo } from "@web/views/fields/field";
import { getTooltipInfo } from "@web/views/fields/field_tooltip";
import { getClassNameFromDecoration } from "@web/views/utils";
import { combineModifiers } from "@web/model/relational_model/utils";
import { ViewButton } from "@web/views/view_button/view_button";
import { useBounceButton } from "@web/views/view_hook";
import { Widget } from "@web/views/widgets/widget";
@@ -228,7 +228,13 @@ export class ListRenderer extends Component {
if (list.isGrouped && col.widget === "handle") {
return false; // no handle column if the list is grouped
}
return !col.optional || this.optionalActiveFields[col.name];
if (col.optional && !this.optionalActiveFields[col.name]) {
return false;
}
if (this.evalColumnInvisible(col.column_invisible)) {
return false;
}
return true;
});
}
@@ -263,6 +269,7 @@ export class ListRenderer extends Component {
return {
...getPropertyFieldInfo(propertyField),
id: `${column.id}_${propertyField.name}`,
column_invisible: combineModifiers(propertyField.column_invisible, column.column_invisible, 'OR'),
classNames: column.classNames,
optional: "hide",
type: "field",
@@ -274,12 +281,9 @@ export class ListRenderer extends Component {
}
getFieldProps(record, column) {
if (this.isCellReadonly(column, record) || this.isRecordReadonly(record)) {
return {
readonly: true,
};
}
return {};
return {
readonly: this.isCellReadonly(column, record) || this.isRecordReadonly(record),
};
}
// The following code manipulates the DOM directly to avoid having to wait for a
@@ -565,7 +569,9 @@ export class ListRenderer extends Component {
get optionalFieldGroups() {
const propertyGroups = {};
const optionalFields = [];
for (const col of this.allColumns.filter((col) => col.optional)) {
const optionalColumns = this.allColumns.filter((col) =>
col.optional && !this.evalColumnInvisible(col.column_invisible));
for (const col of optionalColumns) {
const optionalField = {
label: col.label,
name: col.name,
@@ -589,7 +595,8 @@ export class ListRenderer extends Component {
}
get hasOptionalFields() {
return this.allColumns.some((c) => c.optional);
return this.allColumns.some((col) =>
col.optional && !this.evalColumnInvisible(col.column_invisible));
}
get displayOptionalFields() {
@@ -796,7 +803,7 @@ export class ListRenderer extends Component {
getRowClass(record) {
// classnames coming from decorations
const classNames = this.props.archInfo.decorations
.filter((decoration) => evaluateExpr(decoration.condition, record.evalContext))
.filter((decoration) => evaluateBooleanExpr(decoration.condition, record.evalContext))
.map((decoration) => decoration.class);
if (record.selected) {
classNames.push("table-info");
@@ -840,8 +847,7 @@ export class ListRenderer extends Component {
}
const classNames = [...this.cellClassByColumn[column.id]];
if (column.type === "field") {
const { required } = column.modifiers;
if (required && evalDomain(required, record.evalContext)) {
if (evaluateBooleanExpr(column.required, record.evalContext)) {
classNames.push("o_required_modifier");
}
if (record.isFieldInvalid(column.name)) {
@@ -856,7 +862,7 @@ export class ListRenderer extends Component {
// only handle the text-decoration.
const { decorations } = column;
for (const decoName in decorations) {
if (evaluateExpr(decorations[decoName], record.evalContext)) {
if (evaluateBooleanExpr(decorations[decoName], record.evalContext)) {
classNames.push(getClassNameFromDecoration(decoName));
}
}
@@ -875,10 +881,10 @@ export class ListRenderer extends Component {
}
isCellReadonly(column, record) {
return (
return !!(
this.isRecordReadonly(record) ||
(column.relatedPropertyField && record.selected && record.model.multiEdit) ||
evalDomain(column.modifiers.readonly, record.evalContext)
evaluateBooleanExpr(column.readonly, record.evalContext)
);
}
@@ -910,8 +916,12 @@ export class ListRenderer extends Component {
return getFormattedValue(record, fieldName, column.attrs);
}
evalModifier(modifier, record) {
return !!(modifier && new Domain(modifier).contains(record.evalContext));
evalInvisible(invisible, record) {
return evaluateBooleanExpr(invisible, record.evalContext);
}
evalColumnInvisible(columnInvisible) {
return this.props.evalViewModifier(columnInvisible);
}
getGroupDisplayName(group) {
@@ -2106,6 +2116,7 @@ ListRenderer.props = [
"list",
"archInfo",
"openRecord",
"evalViewModifier",
"onAdd?",
"cycleOnTab?",
"allowSelectors?",
@@ -176,7 +176,7 @@
<t t-esc="getGroupDisplayName(group)"/> (<t t-esc="group.count"/>)
<div t-if="(groupByButtons[group.groupByField.name] and !group.isFolded)" class="o_group_buttons">
<t t-foreach="groupByButtons[group.groupByField.name]" t-as="button" t-key="button.id">
<t t-if="!evalModifier(button.modifiers.invisible, group.record)">
<t t-if="!evalInvisible(button.invisible, group.record)">
<t t-if="button.clickParams.type === 'edit'">
<button t-att-title="button.title" class="btn" t-on-click="() => this.editGroupRecord(group)" tabindex="-1">
<i t-attf-class="fa fa-fw {{button.icon}} o_button_icon"/>
@@ -226,7 +226,7 @@
</td>
<t t-foreach="getColumns(record)" t-as="column" t-key="column.id">
<t t-if="column.type === 'field'">
<t t-set="isInvisible" t-value="evalModifier(column.modifiers.invisible, record) or !(column.name in record.data)"/>
<t t-set="isInvisible" t-value="evalInvisible(column.invisible, record) or !(column.name in record.data)"/>
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)"
class="o_data_cell cursor-pointer"
t-att-class="getCellClass(column, record)"
@@ -244,7 +244,7 @@
<t t-if="column.type === 'button_group'">
<td t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, group, record)" class="o_data_cell cursor-pointer" t-att-class="getCellClass(column, record)" t-on-click="(ev) => this.onButtonCellClicked(record, column, ev)" tabindex="-1">
<t t-foreach="column.buttons" t-as="button" t-key="button.id">
<ViewButton t-if="!evalModifier(button.modifiers.invisible, record)"
<ViewButton t-if="!evalInvisible(button.invisible, record)"
className="button.className"
clickParams="button.clickParams"
defaultRank="button.defaultRank"
@@ -42,8 +42,7 @@ export class PivotArchParser extends XMLParser {
if (node.hasAttribute("string")) {
archInfo.fieldAttrs[fieldName].string = node.getAttribute("string");
}
const modifiers = JSON.parse(node.getAttribute("modifiers") || "{}");
if (modifiers.invisible === true) {
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
archInfo.fieldAttrs[fieldName].isInvisible = true;
break;
}
+6 -3
View File
@@ -2,6 +2,7 @@
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { combineModifiers } from "@web/model/relational_model/utils";
export const X2M_TYPES = ["one2many", "many2many"];
const NUMERIC_TYPES = ["integer", "float", "monetary"];
@@ -27,7 +28,6 @@ export const BUTTON_CLICK_PARAMS = [
"special",
"effect",
"help",
"modifiers",
// WOWL SAD: is adding the support for debounce attribute here justified or should we
// just override compileButton in kanban compiler to add the debounce?
"debounce",
@@ -61,7 +61,7 @@ export function addDependencies(deps, activeFields, fields) {
attrs: {},
options: {},
...dependency,
modifiers: { invisible: true, ...dependency.modifiers },
invisible: "True",
};
}
if (!(name in fields)) {
@@ -268,9 +268,12 @@ export function processButton(node) {
title: node.getAttribute("title") || undefined,
string: node.getAttribute("string") || undefined,
options: JSON.parse(node.getAttribute("options") || "{}"),
modifiers: JSON.parse(node.getAttribute("modifiers") || "{}"),
display: node.getAttribute("display") || "selection",
clickParams,
column_invisible: node.getAttribute("column_invisible"),
invisible: combineModifiers(node.getAttribute("column_invisible"), node.getAttribute("invisible"), "OR"),
readonly: node.getAttribute("readonly"),
required: node.getAttribute("required"),
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { KeepLast } from "@web/core/utils/concurrency";
import { useService } from "@web/core/utils/hooks";
@@ -334,7 +334,7 @@ export class View extends Component {
if ("useSampleModel" in props) {
viewProps.useSampleModel = props.useSampleModel;
} else if (sample) {
viewProps.useSampleModel = Boolean(evaluateExpr(sample));
viewProps.useSampleModel = evaluateBooleanExpr(sample);
}
for (const key in props) {
@@ -51,7 +51,10 @@ export class ViewButton extends Component {
string: this.props.string,
help: this.clickParams.help,
context: this.clickParams.context,
modifiers: this.clickParams.modifiers,
invisible: this.props.attrs?.invisible,
column_invisible: this.props.attrs?.column_invisible,
readonly: this.props.attrs?.readonly,
required: this.props.attrs?.required,
special: this.clickParams.special,
type: this.clickParams.type,
name: this.clickParams.name,
@@ -48,9 +48,21 @@
<span class="o-tooltip--technical--title">Context:</span>
<t t-esc="button.context || context"/>
</li>
<li t-if="button.modifiers" data-item="modifiers">
<span class="o-tooltip--technical--title">Modifiers:</span>
<t t-esc="button.modifiers"/>
<li t-if="'invisible' in button" data-item="invisible">
<span class="o-tooltip--technical--title">Invisible:</span>
<t t-esc="button.invisible"/>
</li>
<li t-if="'column_invisible' in button" data-item="column_invisible">
<span class="o-tooltip--technical--title">Column invisible:</span>
<t t-esc="button.column_invisible"/>
</li>
<li t-if="'required' in button" data-item="required">
<span class="o-tooltip--technical--title">Required:</span>
<t t-esc="button.required"/>
</li>
<li t-if="'readonly' in button" data-item="readonly">
<span class="o-tooltip--technical--title">Readonly:</span>
<t t-esc="button.readonly"/>
</li>
<li t-if="button.special" data-item="special">
<span class="o-tooltip--technical--title">Special:</span>
+7 -20
View File
@@ -140,11 +140,7 @@ export function encodeObjectForTemplate(obj) {
* @returns {boolean | boolean[]}
*/
export function getModifier(el, modifierName) {
// cf python side def transfer_node_to_modifiers
// modifiers' string are evaluated to their boolean or array form
const modifiers = JSON.parse(el.getAttribute("modifiers") || "{}");
const mod = modifierName in modifiers ? modifiers[modifierName] : false;
return typeof mod !== "boolean" ? mod : !!mod;
return el.getAttribute(modifierName);
}
/**
@@ -229,16 +225,16 @@ export class ViewCompiler {
* @returns {Element}
*/
applyInvisible(invisible, compiled, params) {
if (!invisible) {
if (!invisible || invisible === "False") {
return compiled;
}
if (typeof invisible === "boolean") {
if (invisible === "True" || invisible === "1") {
return;
}
const recordExpr = params.recordExpr || "__comp__.props.record";
let isVisileExpr = `!__comp__.evalDomainFromRecord(${recordExpr},${JSON.stringify(
let isVisileExpr = `!__comp__.evaluateBooleanExpr(${JSON.stringify(
invisible
)})`;
)},${recordExpr}.evalContext)`;
if (compiled.hasAttribute("t-if")) {
const formerTif = compiled.getAttribute("t-if");
isVisileExpr = `( ${formerTif} ) and ${isVisileExpr}`;
@@ -275,7 +271,7 @@ export class ViewCompiler {
let invisible;
if (evalInvisible) {
invisible = getModifier(node, "invisible");
if (this.isAlwaysInvisible(invisible, params)) {
if (!params.compileInvisibleNodes && (invisible === "True" || invisible === "1")) {
return;
}
}
@@ -394,7 +390,7 @@ export class ViewCompiler {
*/
compileGenericNode(el, params) {
const compiled = createElement(el.nodeName.toLowerCase());
const metaAttrs = ["modifiers", "attrs", "invisible", "readonly"];
const metaAttrs = ["column_invisible", "invisible", "readonly", "required"];
for (const attr of el.attributes) {
if (metaAttrs.includes(attr.name)) {
continue;
@@ -429,15 +425,6 @@ export class ViewCompiler {
return assignOwlDirectives(widget, el);
}
/**
* @param {any} invisibleModifer
* @param {{ enableInvisible?: boolean }} params
* @returns {boolean}
*/
isAlwaysInvisible(invisibleModifer, params) {
return !params.enableInvisible && typeof invisibleModifer === "boolean" && invisibleModifer;
}
validateNode(node) {
// detect attributes not in whitelist, starting with t-
const attributes = Object.values(node.attributes).map((attr) => attr.name);
@@ -1,7 +1,6 @@
/* @odoo-module */
import { evalDomain } from "@web/core/domain";
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { Component, xml } from "@odoo/owl";
@@ -45,8 +44,7 @@ export class Widget extends Component {
let propsFromNode = {};
if (this.props.widgetInfo) {
const widgetInfo = this.props.widgetInfo;
const modifiers = widgetInfo.modifiers || {};
readonlyFromModifiers = evalDomain(modifiers.readonly, evalContext);
readonlyFromModifiers = evaluateBooleanExpr(widgetInfo.attrs.readonly, evalContext);
propsFromNode = this.widget.extractProps ? this.widget.extractProps(widgetInfo) : {};
}
@@ -67,7 +65,6 @@ Widget.parseWidgetNode = function (node) {
const widget = viewWidgetRegistry.get(name);
const widgetInfo = {
name,
modifiers: {},
widget,
options: {},
attrs: {},
@@ -78,9 +75,7 @@ Widget.parseWidgetNode = function (node) {
// avoid adding name and widget to attrs
continue;
}
if (name === "modifiers") {
widgetInfo.modifiers = JSON.parse(value);
} else if (name === "options") {
if (name === "options") {
widgetInfo.options = evaluateExpr(value);
} else if (!name.startsWith("t-att")) {
// all other (non dynamic) attributes
@@ -333,7 +333,7 @@ QUnit.module("py", {}, () => {
QUnit.test("throws when period negative", (assert) => {
const matcher = (errorMessage) => {
return function match(err) {
return err.message === errorMessage;
return err.message.includes(errorMessage);
};
};
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { evaluateExpr } from "@web/core/py_js/py";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
QUnit.module("py", {}, () => {
QUnit.module("interpreter", () => {
@@ -29,6 +29,8 @@ QUnit.module("py", {}, () => {
assert.strictEqual(evaluateExpr('""'), "");
assert.strictEqual(evaluateExpr('"foo"'), "foo");
assert.strictEqual(evaluateExpr("'foo'"), "foo");
assert.strictEqual(evaluateExpr("'FOO'.lower()"), "foo");
assert.strictEqual(evaluateExpr("'foo'.upper()"), "FOO");
});
QUnit.test("boolean", (assert) => {
@@ -96,6 +98,7 @@ QUnit.module("py", {}, () => {
assert.strictEqual(evaluateExpr("not False"), true);
assert.strictEqual(evaluateExpr("not foo", { foo: false }), true);
assert.strictEqual(evaluateExpr("not None"), true);
assert.strictEqual(evaluateExpr("not []"), true);
assert.strictEqual(evaluateExpr("True == False or True == True"), true);
assert.strictEqual(evaluateExpr("False == True and False"), false);
});
@@ -123,6 +126,7 @@ QUnit.module("py", {}, () => {
assert.strictEqual(evaluateExpr('None or "bar"'), "bar");
assert.strictEqual(evaluateExpr("False or None"), null);
assert.strictEqual(evaluateExpr("0 or 1"), 1);
assert.strictEqual(evaluateExpr("[] or False"), false);
});
QUnit.module("values from context");
@@ -321,6 +325,22 @@ QUnit.module("py", {}, () => {
assert.strictEqual(evaluateExpr("{'a': 1}.get('b', 54)"), 54);
});
QUnit.test("can get values from values 'context'", (assert) => {
assert.strictEqual(evaluateExpr("context.get('a')", { context: { a: 123 } }), 123);
const values = { context: { a: { b: { c: 321 } } } };
assert.strictEqual(evaluateExpr("context.get('a').b.c", values), 321);
assert.strictEqual(evaluateExpr("context.get('a', {'e': 5}).b.c", values), 321);
assert.strictEqual(evaluateExpr("context.get('d', 3)", values), 3);
assert.strictEqual(evaluateExpr("context.get('d', {'e': 5})['e']", values), 5);
});
QUnit.test("can check if a key is in the 'context'", (assert) => {
assert.strictEqual(evaluateExpr("'a' in context", { context: { a: 123 } }), true);
assert.strictEqual(evaluateExpr("'a' in context", { context: { b: 123 } }), false);
assert.strictEqual(evaluateExpr("'a' not in context", { context: { a: 123 } }), false);
assert.strictEqual(evaluateExpr("'a' not in context", { context: { b: 123 } }), true);
});
QUnit.module("objects");
QUnit.test("can read values from object", (assert) => {
@@ -350,5 +370,42 @@ QUnit.module("py", {}, () => {
QUnit.test("tuple in list", (assert) => {
assert.deepEqual(evaluateExpr("[(1 + 2,'foo', True)]"), [[3, "foo", true]]);
});
QUnit.module("evaluate to boolean");
QUnit.test("simple expression", (assert) => {
assert.strictEqual(evaluateBooleanExpr("12"), true);
assert.strictEqual(evaluateBooleanExpr("0"), false);
assert.strictEqual(evaluateBooleanExpr("0 + 3 - 1"), true);
assert.strictEqual(evaluateBooleanExpr("0 + 3 - 1 - 2"), false);
assert.strictEqual(evaluateBooleanExpr('"foo"'), true);
assert.strictEqual(evaluateBooleanExpr("[1]"), true);
assert.strictEqual(evaluateBooleanExpr("[]"), false);
});
QUnit.test("use contextual values", (assert) => {
assert.strictEqual(evaluateBooleanExpr("a", {a: 12}), true);
assert.strictEqual(evaluateBooleanExpr("a", {a: 0}), false);
assert.strictEqual(evaluateBooleanExpr("0 + 3 - a", {a: 1}), true);
assert.strictEqual(evaluateBooleanExpr("0 + 3 - a - 2", {a: 1}), false);
assert.strictEqual(evaluateBooleanExpr("0 + 3 - a - b", {a: 1, b: 2}), false);
assert.strictEqual(evaluateBooleanExpr('a', {a: "foo"}), true);
assert.strictEqual(evaluateBooleanExpr("a", {a: [1]}), true);
assert.strictEqual(evaluateBooleanExpr("a", {a: []}), false);
});
QUnit.test("throw if has missing value", (assert) => {
assert.throws(() => evaluateBooleanExpr("a", {b: 0}));
assert.strictEqual(evaluateBooleanExpr("1 or a"), true); // do not throw (lazy value)
assert.throws(() => evaluateBooleanExpr("0 or a"));
assert.throws(() => evaluateBooleanExpr("a or b", {b: true}));
assert.throws(() => evaluateBooleanExpr("a and b", {b: true}));
assert.throws(() => evaluateBooleanExpr("a()"));
assert.throws(() => evaluateBooleanExpr("a[0]"));
assert.throws(() => evaluateBooleanExpr("a.b"));
assert.throws(() => evaluateBooleanExpr("0 + 3 - a", {b: 1}));
assert.throws(() => evaluateBooleanExpr("0 + 3 - a - 2", {b: 1}));
assert.throws(() => evaluateBooleanExpr("0 + 3 - a - b", {b: 2}));
});
});
});
+9 -91
View File
@@ -10,7 +10,6 @@ import {
serializeDate,
serializeDateTime,
} from "@web/core/l10n/dates";
import { evaluateExpr } from "@web/core/py_js/py";
import { registry } from "@web/core/registry";
import { intersection, unique } from "@web/core/utils/arrays";
import { deepCopy, pick } from "@web/core/utils/objects";
@@ -319,7 +318,6 @@ export class MockServer {
function isNodeProcessed(node) {
return processedNodes.findIndex((n) => n.isSameNode(node)) > -1;
}
const modifiersNames = ["invisible", "readonly", "required"];
const onchanges = params.models[modelName].onchanges || {};
const fieldNodes = {};
const groupbyNodes = {};
@@ -332,8 +330,6 @@ export class MockServer {
}
const editableView = editable && this._editableNode(doc, modelName);
const onchangeAbleView = this._onchangeAbleView(doc);
const modifiersFromModel = this._modifiersFromModel(doc);
const inTreeView = ["tree", "list"].includes(doc.tagName);
const inFormView = doc.tagName === "form";
// mock _postprocess_access_rights
const isBaseModel = !context.base_model_name || modelName === context.base_model_name;
@@ -350,90 +346,30 @@ export class MockServer {
if (node.nodeType === Node.TEXT_NODE) {
return false;
}
const modifiers = {};
['required', 'readonly', 'invisible', 'column_invisible'].forEach((attr) => {
const value = node.getAttribute(attr);
if (value === '1' || value === 'true') {
node.setAttribute(attr, 'True');
}
});
const isField = node.tagName === "field";
const isGroupby = node.tagName === "groupby";
if (isField) {
const fieldName = node.getAttribute("name");
fieldNodes[fieldName] = {
node,
isInvisible: node.getAttribute("invisible"),
isInvisible: node.getAttribute("invisible") === "True",
isEditable: editableView && this._editableNode(node, modelName),
};
// 'transfer_field_to_modifiers' simulation
const field = fields[fieldName];
if (!field) {
throw new Error("Field " + fieldName + " does not exist");
}
const defaultValues = {};
const stateExceptions = {}; // what is this ?
modifiersFromModel.forEach((attr) => {
stateExceptions[attr] = [];
defaultValues[attr] = !!field[attr];
});
// LPE: what is this ?
/* for (const [state, modifs] of Object.entries(field.states || {})) {
modifs.forEach((modif) => {
if (defaultValues[modif[0]] !== modif[1]) {
stateExceptions[modif[0]].append(state);
}
});
});*/
Object.entries(defaultValues).forEach(([attr, defaultValue]) => {
if (stateExceptions[attr].length) {
modifiers[attr] = [
["state", defaultValue ? "not in" : "in", stateExceptions[attr]],
];
} else {
modifiers[attr] = defaultValue;
}
});
} else if (isGroupby && !isNodeProcessed(node)) {
const groupbyName = node.getAttribute("name");
fieldNodes[groupbyName] = { node };
groupbyNodes[groupbyName] = node;
}
// 'transfer_node_to_modifiers' simulation
let attrs = node.getAttribute("attrs");
node.removeAttribute("attrs");
if (attrs) {
attrs = evaluateExpr(attrs);
Object.assign(modifiers, attrs);
}
const states = node.getAttribute("states");
node.removeAttribute("states");
if (states) {
if (!modifiers.invisible) {
modifiers.invisible = [];
}
modifiers.invisible.push(["state", "not in", states.split(",")]);
}
const inListHeader = inTreeView && node.closest("header");
modifiersNames.forEach((attr) => {
const mod = node.getAttribute(attr);
node.removeAttribute(attr);
if (mod) {
const v = evaluateExpr(mod, context) ? true : false;
if (inTreeView && !inListHeader && attr === "invisible") {
modifiers.column_invisible = v;
} else if (v || !(attr in modifiers) || !Array.isArray(modifiers[attr])) {
modifiers[attr] = v;
}
}
});
modifiersNames.forEach((attr) => {
if (
attr in modifiers &&
(!!modifiers[attr] === false ||
(Array.isArray(modifiers[attr]) && !modifiers[attr].length))
) {
delete modifiers[attr];
}
});
if (Object.keys(modifiers).length) {
node.setAttribute("modifiers", JSON.stringify(modifiers));
}
if (isGroupby && !isNodeProcessed(node)) {
return false;
}
@@ -480,8 +416,7 @@ export class MockServer {
Array.from(node.children).forEach((childNode) => {
if (childNode.tagName) {
relFields = Object.assign({}, params.models[relModel].fields);
// this is hackhish, but _getView modifies the subview document in place,
// especially to generate the "modifiers" attribute
// this is hackhish, but _getView modifies the subview document in place
const { models } = this._getView({
models: params.models,
arch: childNode,
@@ -553,16 +488,7 @@ export class MockServer {
case "field": {
const fname = node.getAttribute("name");
const field = this.models[modelName].fields[fname];
return (
(!field.readonly ||
(field.states &&
Object.values(field.states).some((item) =>
item.includes("readonly")
))) &&
(!["1", "True"].includes(node.getAttribute("readonly")) ||
Object.keys(evaluateExpr(node.getAttribute("attrs") || "{}") || {}).length >
0)
);
return !field.readonly && node.getAttribute("readonly") !== "True" && node.getAttribute("readonly") !== "1";
}
default:
return false;
@@ -579,14 +505,6 @@ export class MockServer {
}
}
_modifiersFromModel(node) {
const modifiersNames = ["invisible"];
if (["kanban", "tree", "form"].includes(node.tagName)) {
modifiersNames.push(...["readonly", "required"]);
}
return modifiersNames;
}
/**
* Converts an Object representing a record to actual return Object of the
* python `onchange` method.
@@ -104,8 +104,8 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
arch: `
<form>
<header>
<button string="Confirm" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Do it" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Confirm" invisible="display_name == 'first record'" />
<button string="Do it" invisible="display_name == 'first record'" />
</header>
<sheet>
<group>
@@ -145,7 +145,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
arch: `
<form>
<header>
<button string="Hola" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Hola" invisible="display_name == 'first record'" />
<button string="Ciao" />
</header>
<sheet>
@@ -197,7 +197,7 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
<form>
<header>
<widget name="attach_document" string="Attach document" />
<button string="Ciao" attrs="{'invisible': [['display_name', '=', 'first record']]}" />
<button string="Ciao" invisible="display_name == 'first record'" />
</header>
<sheet>
<group>
@@ -251,8 +251,8 @@ QUnit.module("Mobile Views", ({ beforeEach }) => {
<form>
<header>
<button string="Just more than one" />
<button string="Confirm" attrs="{'invisible': [['display_name', '=', '']]}" />
<button string="Do it" attrs="{'invisible': [['display_name', '!=', '']]}" />
<button string="Confirm" invisible="display_name == ''" />
<button string="Do it" invisible="display_name != ''" />
</header>
<sheet>
<field name="display_name" />
+2 -2
View File
@@ -1377,14 +1377,14 @@ QUnit.module("MockServer", (hooks) => {
data.views = {
"bar,10001,list": `
<tree>
<field name="bool" invisible="1"/>
<field name="bool" column_invisible="1"/>
<field name="foo"/>
</tree>
`,
"bar,10001,search": `<search></search>`,
};
const expectedList = `<tree>
<field name="bool" modifiers="{&quot;column_invisible&quot;:true}"/>
<field name="bool" column_invisible="True"/>
<field name="foo"/>
</tree>`;
const mockServer = new MockServer(data);
@@ -288,11 +288,11 @@ QUnit.module("Views", ({ beforeEach }) => {
<form>
<field name="name" />
<field name="allday" />
<group attrs="{'invisible': [['allday', '=', True]]}">
<group invisible="allday">
<field name="start" />
<field name="stop" />
</group>
<group attrs="{'invisible': [['allday', '=', False]]}">
<group invisible="not allday">
<field name="start_date" />
<field name="stop_date" />
</group>
@@ -301,8 +301,8 @@ QUnit.module("Views", ({ beforeEach }) => {
"event,1,form": `
<form>
<field name="allday" invisible="1" />
<field name="start" attrs="{'invisible': [['allday', '=', False]]}" />
<field name="stop" attrs="{'invisible': [['allday', '=', True]]}" />
<field name="start" invisible="not allday" />
<field name="stop" invisible="allday" />
</form>
`,
},
@@ -1370,8 +1370,8 @@ QUnit.module("Views", ({ beforeEach }) => {
<calendar date_start="start" date_stop="stop" all_day="allday" mode="week">
<field name="priority" widget="priority" readonly="1" />
<field name="is_hatched" invisible="1" />
<field name="partner_id" attrs="{'invisible': [['is_hatched', '=', False]]}" />
<field name="start" attrs="{'invisible': [['is_hatched', '=', True]]}" />
<field name="partner_id" invisible="not is_hatched" />
<field name="start" invisible="is_hatched" />
</calendar>
`,
});
@@ -4692,7 +4692,8 @@ QUnit.module("Views", ({ beforeEach }) => {
serverData,
arch: `
<calendar date_start="start" date_stop="stop" all_day="allday" mode="month">
<field name="name" attrs="{'readonly': [['unknown_field', '=', 42]]}" />
<field name="delay" invisible="True" />
<field name="name" readonly="delay == 42" />
</calendar>
`,
});
@@ -4709,7 +4710,8 @@ QUnit.module("Views", ({ beforeEach }) => {
serverData,
arch: `
<calendar date_start="start" date_stop="stop" all_day="allday" mode="month" scales="month,year">
<field name="name" attrs="{'readonly': [['unknown_field', '=', 42]]}" />
<field name="delay" invisible="True" />
<field name="name" readonly="delay == 42" />
</calendar>
`,
});
@@ -429,7 +429,7 @@ QUnit.module("Fields", (hooks) => {
<sheet>
<group>
<field name="int_field" />
<field name="foo" attrs="{'invisible': [('int_field', '==', 9)]}"/>
<field name="foo" invisible="int_field == 9"/>
</group>
</sheet>
</form>`,
@@ -294,7 +294,7 @@ Ho-ho-hoooo Merry Christmas`,
<sheet>
<div>
<field name="text_field" disabled="1" widget="CopyClipboardButton"/>
<field name="char_field" disabled="[('char_field', '=', 'yop')]" widget="CopyClipboardButton"/>
<field name="char_field" disabled="char_field == 'yop'" widget="CopyClipboardButton"/>
<field name="char_field" widget="char"/>
</div>
</sheet>
@@ -871,7 +871,7 @@ QUnit.module("Fields", (hooks) => {
serverData,
arch: /* xml */ `
<form>
<field name="datetime" widget="daterange" options="{'end_date_field': 'datetime_end'}" attrs="{'required':[('datetime', '!=', False), ('datetime_end', '!=', False)]}"/>
<field name="datetime" widget="daterange" options="{'end_date_field': 'datetime_end'}" required="datetime and datetime_end"/>
</form>`,
resId: 1,
});
@@ -104,7 +104,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<group>
<field name="timmy" widget="many2many_checkboxes" attrs="{'readonly': true}" />
<field name="timmy" widget="many2many_checkboxes" readonly="True" />
</group>
</form>`,
});
@@ -400,7 +400,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="float_field" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@@ -495,7 +495,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<tree editable="bottom">
<field name="monetary_field"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@@ -436,7 +436,7 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test("O2M with parented m2o and domain on parent.m2o", async function (assert) {
assert.expect(4);
assert.expect(7);
// Records in an o2m can have a m2o pointing to themselves.
// In that case, a domain evaluation on that field followed by name_search
@@ -475,6 +475,7 @@ QUnit.module("Fields", (hooks) => {
mockRPC(route, { kwargs }) {
if (route === "/web/dataset/call_kw/turtle/name_search") {
assert.deepEqual(kwargs.args, [["id", "in", []]]);
assert.deepEqual(JSON.stringify(kwargs.args), '[["id","in",[]]]');
}
},
});
@@ -648,8 +649,8 @@ QUnit.module("Fields", (hooks) => {
<field name="p">
<tree editable="bottom">
<field name="turtles" invisible="1"/>
<field name="foo" attrs="{&quot;readonly&quot; : [(&quot;turtles&quot;, &quot;!=&quot;, [])] }"/>
<field name="qux" attrs="{&quot;readonly&quot; : [(&quot;turtles&quot;, &quot;!=&quot;, [])] }"/>
<field name="foo" readonly="turtles"/>
<field name="qux" readonly="turtles"/>
</tree>
</field>
</form>`,
@@ -1502,7 +1503,7 @@ QUnit.module("Fields", (hooks) => {
<tree editable="bottom" default_order="turtle_int" limit="10">
<field name="turtle_int" widget="handle"/>
<field name="turtle_foo"/>
<field name="turtle_qux" attrs="{'readonly': [('turtle_foo', '=', False)]}"/>
<field name="turtle_qux" readonly="not turtle_foo"/>
</tree>
</field>
</form>`,
@@ -2310,7 +2311,7 @@ QUnit.module("Fields", (hooks) => {
serverData.views = {
"turtle,false,form": `
<form>
<field name="display_name" attrs="{'invisible': [('turtle_foo', '==', 'yop')]}"/>
<field name="display_name" invisible="turtle_foo == 'yop'"/>
<field name="turtle_foo"/>
</form>`,
};
@@ -5258,7 +5259,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="turtles">
<tree editable="bottom">
<field name="turtle_foo" widget="char" attrs="{'readonly': [('turtle_int', '==', 11111)]}"/>
<field name="turtle_foo" widget="char" readonly="turtle_int == 11111"/>
<field name="turtle_int"/>
</tree>
</field>
@@ -5322,7 +5323,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<field name="p">
<tree editable="top">
<field name="display_name" attrs='{"readonly": [["product_id", "=", false]]}'/>
<field name="display_name" readonly="not product_id"/>
<field name="product_id"/>
</tree>
</field>
@@ -6520,7 +6521,7 @@ QUnit.module("Fields", (hooks) => {
<field name="turtles">
<tree editable="top" limit="2">
<field name="turtle_foo"/>
<field name="partner_ids" widget="many2many_tags" attrs="{'readonly': [('turtle_foo', '=', 'a')]}"/>
<field name="partner_ids" widget="many2many_tags" readonly="turtle_foo == 'a'"/>
</tree>
</field>
</form>`,
@@ -7856,7 +7857,7 @@ QUnit.module("Fields", (hooks) => {
<field name="turtles">
<tree editable="top">
<field name="turtle_int"/>
<field name="turtle_foo" attrs='{"required": [["turtle_int", "=", 0]]}'/>
<field name="turtle_foo" required='turtle_int == 0'/>
</tree>
</field>
</form>`,
@@ -8343,7 +8344,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="name"/>
<field name="p" attrs="{'readonly': [['name', '=', 'readonly']]}">
<field name="p" readonly="name == 'readonly'">
<tree><field name="display_name"/></tree>
<form>
<field name="display_name"/>
@@ -10615,8 +10616,8 @@ QUnit.module("Fields", (hooks) => {
<field name="bar"/>
<field name="p">
<tree>
<field name="foo" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
<field name="bar" attrs="{'column_invisible': [('parent.bar', '=', False)]}"/>
<field name="foo" column_invisible="parent.product_id"/>
<field name="bar" column_invisible="not parent.bar"/>
</tree>
</field>
</page>
@@ -10667,7 +10668,7 @@ QUnit.module("Fields", (hooks) => {
<field name="p">
<tree>
<field name="foo"/>
<button name="abc" string="Do it" class="some_button" attrs="{'column_invisible': [('parent.product_id', '=', False)]}"/>
<button name="abc" string="Do it" class="some_button" column_invisible="not parent.product_id"/>
</tree>
</field>
</form>`,
@@ -10702,10 +10703,10 @@ QUnit.module("Fields", (hooks) => {
<field name="p">
<tree>
<button name="abc1" string="Do it 1" class="some_button1"/>
<button name="abc2" string="Do it 2" class="some_button2" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
<button name="abc2" string="Do it 2" class="some_button2" column_invisible="parent.product_id"/>
<field name="foo"/>
<button name="abc3" string="Do it 3" class="some_button3" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
<button name="abc4" string="Do it 4" class="some_button4" attrs="{'column_invisible': [('parent.trululu', '!=', False)]}"/>
<button name="abc3" string="Do it 3" class="some_button3" column_invisible="parent.product_id"/>
<button name="abc4" string="Do it 4" class="some_button4" column_invisible="parent.trululu"/>
</tree>
</field>
</form>`,
@@ -12012,7 +12013,7 @@ QUnit.module("Fields", (hooks) => {
<field name="p">
<tree editable="bottom">
<field name="foo"/>
<field name="int_field" attrs="{'column_invisible': [('parent.bar', '=', False)]}"/>
<field name="int_field" column_invisible="not parent.bar"/>
</tree>
</field>
</form>`,
@@ -12038,8 +12039,8 @@ QUnit.module("Fields", (hooks) => {
serverData.views = {
"partner,false,list": `
<tree>
<field name="foo" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
<field name="bar" attrs="{'column_invisible': [('parent.bar', '=', False)]}"/>
<field name="foo" column_invisible="parent.product_id"/>
<field name="bar" column_invisible="not parent.bar"/>
</tree>`,
};
@@ -1818,7 +1818,7 @@ QUnit.module("Fields", (hooks) => {
<field name="company_id"/>
<field name="display_name"/>
<field name="properties" widget="properties"/>
<div class="o_test_properties_not_empty" attrs="{'invisible': [('properties', '=', [])]}">
<div class="o_test_properties_not_empty" invisible="not properties">
Properties not empty
</div>
</group>
@@ -1872,7 +1872,7 @@ QUnit.module("Fields", (hooks) => {
<field name="company_id"/>
<field name="display_name"/>
<field name="properties" widget="properties"/>
<div class="o_test_properties_not_empty" attrs="{'invisible': [('properties', '=', [])]}">
<div class="o_test_properties_not_empty" invisible="not properties">
Properties not empty
</div>
</group>
@@ -276,7 +276,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
<field name="color" required="feedback_value == 'bad'" />
</form>`,
});
@@ -329,7 +329,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="feedback_value" />
<field name="color" attrs="{'required': [('feedback_value', '=', 'bad')]}" />
<field name="color" required="feedback_value == 'bad'" />
</form>`,
});
@@ -161,7 +161,7 @@ QUnit.module("Fields", (hooks) => {
arch: `
<form>
<field name="bar" />
<field name="txt" attrs="{'invisible': [('bar', '=', True)]}" />
<field name="txt" invisible="bar" />
</form>`,
});
@@ -55,7 +55,7 @@ QUnit.module("Fields", (hooks) => {
resId: 1,
arch: /*xml*/ `
<tree string="Colors" editable="top">
<field name="tz_offset" invisible="True"/>
<field name="tz_offset" column_invisible="True"/>
<field name="color" widget="timezone_mismatch" />
</tree>
`,
@@ -276,7 +276,7 @@ QUnit.module("Fields", (hooks) => {
<form>
<sheet>
<group>
<field name="foo" widget="url" attrs="{'readonly': True}" />
<field name="foo" widget="url" readonly="True" />
<field name="foo2" />
</group>
</sheet>
@@ -228,18 +228,18 @@ QUnit.module("Form Compiler", (hooks) => {
// ```<form>
// <field name="display_name" invisible="1" />
// <div class="visible3" invisible="0"/>
// <div modifiers="{'invisible': [['display_name', '=', 'take']]}"/>
// <div invisible="display_name == 'take'"/>
// </form>````
const arch = /*xml*/ `
<form>
<field field_id="display_name" name="display_name" modifiers="{&quot;invisible&quot;: true}" />
<div class="visible3" modifiers="{&quot;invisible&quot;: false}"/>
<div modifiers="{&quot;invisible&quot;: [[&quot;display_name&quot;, &quot;=&quot;, &quot;take&quot;]]}"/>
<field field_id="display_name" name="display_name" invisible="True" />
<div class="visible3" invisible="False"/>
<div invisible="display_name == &quot;take&quot;"/>
</form>`;
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,[[&quot;display_name&quot;,&quot;=&quot;,&quot;take&quot;]])" />
<div t-if="!__comp__.evaluateBooleanExpr(&quot;display_name == \\&quot;take\\&quot;&quot;,__comp__.props.record.evalContext)" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
@@ -248,14 +248,14 @@ QUnit.module("Form Compiler", (hooks) => {
QUnit.test("compile invisible containing string as domain", async (assert) => {
const arch = /*xml*/ `
<form>
<field name="display_name" modifiers="{&quot;invisible&quot;: true}" />
<div class="visible3" modifiers="{&quot;invisible&quot;: false}"/>
<div modifiers="{&quot;invisible&quot;: &quot;[['display_name', '=', 'take']]&quot;}"/>
<field name="display_name" invisible="True" />
<div class="visible3" invisible="False"/>
<div invisible="display_name == 'take'"/>
</form>`;
const expected = /*xml*/ `
<div class="visible3" />
<div t-if="!__comp__.evalDomainFromRecord(__comp__.props.record,&quot;[['display_name','=','take']]&quot;)" />
<div t-if="!__comp__.evaluateBooleanExpr(&quot;display_name == 'take'&quot;,__comp__.props.record.evalContext)" />
`;
assert.areContentEquivalent(compileTemplate(arch), expected);
});
@@ -350,14 +350,14 @@ QUnit.module("Form Renderer", (hooks) => {
};
});
QUnit.test("compile form with modifiers and attrs - string as domain", async (assert) => {
QUnit.test("compile form with modifiers", async (assert) => {
serverData.views = {
"partner,1,form": /*xml*/ `
<form>
<div modifiers="{&quot;invisible&quot;: &quot;[['display_name', '=', uid]]&quot;}">
<div invisible="display_name == uid">
<field name="charfield"/>
</div>
<field name="display_name" attrs="{'readonly': &quot;[['display_name', '=', uid]]&quot;}"/>
<field name="display_name" readonly="display_name == uid"/>
</form>`,
};
@@ -379,7 +379,7 @@ QUnit.module("Form Renderer", (hooks) => {
<form>
<sheet>
<notebook>
<page name="p1" attrs="{'invisible': [['display_name', '=', 'lol']]}"><field name="charfield"/></page>
<page name="p1" invisible="display_name == 'lol'"><field name="charfield"/></page>
<page name="p2"><field name="display_name"/></page>
</notebook>
</sheet>
@@ -482,9 +482,9 @@ QUnit.module("Form Renderer", (hooks) => {
},
});
const arch = `<myNode modifiers="{&quot;invisible&quot;: [[&quot;field&quot;, &quot;=&quot;, &quot;value&quot;]]}" />`;
const arch = `<myNode invisible="field == 'value'" />`;
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evalDomainFromRecord(__comp__.props.record,[[&quot;field&quot;,&quot;=&quot;,&quot;value&quot;]])" t-ref="compiled_view_root"/></t>`;
const expected = `<t><div class="myNode" t-if="( myCondition or myOtherCondition ) and !__comp__.evaluateBooleanExpr(&quot;field == 'value'&quot;,__comp__.props.record.evalContext)" t-ref="compiled_view_root"/></t>`;
assert.areEquivalent(compileTemplate(arch), expected);
});
});
@@ -497,11 +497,11 @@ QUnit.module("Views", (hooks) => {
<form>
<group>
<group>
<field name="foo" class="foo_1" attrs="{'invisible': [('bar','=',False)]}"/>
<field name="foo" class="foo_2" attrs="{'invisible': [('bar','=',True)]}"/>
<field name="foo" class="foo_1" invisible="not bar"/>
<field name="foo" class="foo_2" invisible="bar"/>
<field name="foo" class="foo_3"/>
<field name="int_field" class="int_field_1" attrs="{'readonly': [('bar','=',True)]}"/>
<field name="int_field" class="int_field_2" attrs="{'readonly': [('bar','=',False)]}"/>
<field name="int_field" class="int_field_1" readonly="bar"/>
<field name="int_field" class="int_field_2" readonly="not bar"/>
<field name="bar"/>
</group>
</group>
@@ -1215,8 +1215,8 @@ QUnit.module("Views", (hooks) => {
<group>
<field name="int_field"/>
<field name="timmy" invisible="1"/>
<field name="foo" attrs='{"invisible": [("int_field", "=", 10)]}'/>
<field name="bar" attrs='{"invisible": [("bar", "=", False), ("timmy", "=", [])]}'/>
<field name="foo" invisible="int_field == 10"/>
<field name="bar" invisible="not bar and not timmy"/>
</group>
</sheet>
</form>`,
@@ -1249,10 +1249,12 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<form>
<div class="a" attrs='{"invisible": [("foo", "=", False)]}'>a</div>
<div class="b" attrs='{"invisible": [("foo", "=", "")]}'>b</div>
<div class="c" attrs='{"invisible": [("display_name", "=", False)]}'>c</div>
<div class="d" attrs='{"invisible": [("display_name", "=", "")]}'>d</div>
<div class="a" invisible="foo == False">b</div>
<div class="b" invisible="foo == ''">b</div>
<div class="c" invisible="not display_name">c</div>
<div class="d" invisible="display_name == ''">d</div>
<div class="e" invisible="not foo">e</div>
<div class="f" invisible="display_name == False">f</div>
<field name="foo" invisible="1"/>
</form>`,
resId: 1,
@@ -1262,6 +1264,8 @@ QUnit.module("Views", (hooks) => {
assert.containsNone(target, "div.b");
assert.containsNone(target, "div.c");
assert.containsOnce(target, "div.d");
assert.containsNone(target, "div.e");
assert.containsNone(target, "div.f");
});
QUnit.test(
@@ -1275,7 +1279,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<field name="int_field"/>
<notebook name="test_name" class="new_class" attrs='{"invisible": [["int_field", "=", 10]]}'>
<notebook name="test_name" class="new_class" invisible="int_field == 10">
<page string="Foo">
<field name="foo"/>
</page>
@@ -1367,10 +1371,10 @@ QUnit.module("Views", (hooks) => {
<sheet>
<field name="bar"/>
<notebook>
<page string="A" attrs='{"invisible": [["bar", "=", false]]}'>
<page string="A" invisible='not bar'>
<field name="foo"/>
</page>
<page string="B" attrs='{"invisible": [["bar", "=", true]]}'>
<page string="B" invisible='bar'>
<field name="int_field"/>
</page>
<page string="C">
@@ -1405,7 +1409,7 @@ QUnit.module("Views", (hooks) => {
<sheet>
<field name="int_field"/>
<notebook>
<page string="Foo" attrs='{"invisible": [["int_field", "=", 44]]}'>
<page string="Foo" invisible='int_field == 44'>
<field name="foo"/>
</page>
<page string="Bar">
@@ -1438,7 +1442,7 @@ QUnit.module("Views", (hooks) => {
<sheet>
<field name="bar"/>
<notebook>
<page string="Foo" attrs='{"invisible": [["bar", "!=", false]]}'>
<page string="Foo" invisible='bar'>
<field name="foo"/>
</page>
</notebook>
@@ -1493,13 +1497,13 @@ QUnit.module("Views", (hooks) => {
<sheet>
<field name="bar"/>
<notebook class="new_class">
<page string="Foo" attrs="{'invisible': [['bar', '=', true]]}">
<page string="Foo" invisible="bar">
<field name="foo"/>
</page>
<page string="Qux" invisible="1">
<field name="qux"/>
</page>
<page string="IntField" attrs="{'invisible': [['bar', '=', true]]}">
<page string="IntField" invisible="bar">
<field name="int_field"/>
</page>
</notebook>
@@ -1670,7 +1674,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<field name="bar"/>
<group attrs='{"invisible": [["bar", "!=", true]]}'>
<group invisible='not bar'>
<group>
<field name="foo"/>
</group>
@@ -1699,7 +1703,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<field name="foo"/>
<group attrs='{"invisible": [["int_field", "=", 0.0]]}'>
<group invisible='int_field == 0.0'>
<div class="hello">this should be invisible</div>
<field name="int_field"/>
</group>
@@ -1861,7 +1865,7 @@ QUnit.module("Views", (hooks) => {
<button class="oe_stat_button" type="action" name="someaction">
<field name="int_field"/>
</button>
<button class="oe_stat_button" name="some_action" type="action" attrs='{"invisible": [["bar", "=", true]]}'>
<button class="oe_stat_button" name="some_action" type="action" invisible='bar'>
<field name="bar"/>
</button>
</div>
@@ -1917,7 +1921,7 @@ QUnit.module("Views", (hooks) => {
<button class="oe_stat_button">
<field name="int_field"/>
</button>
<button class="oe_stat_button" name="some_action" type="action" attrs='{"invisible": [["bar", "=", true]]}'>
<button class="oe_stat_button" name="some_action" type="action" invisible='bar'>
<field name="bar"/>
</button>
</div>
@@ -2099,8 +2103,8 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<group>
<label for="bar" string="label1" attrs='{"invisible": [["bar", "=", True]]}'/>
<label for="bar" string="label2" attrs='{"invisible": [["bar", "=", False]]}'/>
<label for="bar" string="label1" invisible='bar'/>
<label for="bar" string="label2" invisible='not bar'/>
<field name="bar" nolabel="1"/>
</group>
</sheet>
@@ -2129,7 +2133,7 @@ QUnit.module("Views", (hooks) => {
<sheet>
<group>
<div>
<label for="bar" attrs='{"invisible": [["bar", "=", True]]}'/>
<label for="bar" invisible='bar'/>
<field name="bar" />
</div>
</group>
@@ -2356,8 +2360,8 @@ QUnit.module("Views", (hooks) => {
<group>
<field name="bar"/>
<field name="int_field"/>
<field name="foo" attrs="{'required': [['bar', '=', false]]}"/>
<field name="foo" attrs="{'required': [['int_field', '=', 44]]}"/>
<field name="foo" required="not bar"/>
<field name="foo" required="int_field == 44"/>
</group>
</form>`,
mockRPC(route, args) {
@@ -2412,8 +2416,8 @@ QUnit.module("Views", (hooks) => {
<group>
<field name="bar"/>
<field name="int_field"/>
<field name="foo" attrs="{'readonly': [['bar', '=', false]]}"/>
<field name="foo" attrs="{'readonly': [['int_field', '=', 0]]}"/>
<field name="foo" readonly="not bar"/>
<field name="foo" readonly="int_field == 0"/>
</group>
</form>`,
mockRPC(route, args) {
@@ -2466,8 +2470,8 @@ QUnit.module("Views", (hooks) => {
<group>
<field name="bar"/>
<field name="int_field"/>
<field name="foo" required="1" attrs="{'invisible': [['bar', '=', false]]}"/>
<field name="foo" attrs="{'invisible': [['int_field', '=', 0]]}"/>
<field name="foo" required="1" invisible="not bar"/>
<field name="foo" invisible="int_field == 0"/>
</group>
</form>`,
mockRPC(route, args) {
@@ -2627,7 +2631,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<group>
<field name="foo" attrs="{'readonly': [['bar', '=', True]]}"/>
<field name="foo" readonly="bar"/>
<field name="bar"/>
</group>
</sheet>
@@ -2668,7 +2672,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<form>
<field name="int_field" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="int_field" readonly="id"/>
</form>`,
});
@@ -2722,7 +2726,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<form>
<field name="trululu"/>
<field name="product_ids" attrs="{'readonly': [['trululu', '=', False]]}">
<field name="product_ids" readonly="not trululu">
<tree editable="top"><field name="int_field" widget="handle" /><field name="name"/></tree>
</field>
</form>
@@ -2770,7 +2774,7 @@ QUnit.module("Views", (hooks) => {
<sheet>
<group>
<field name="foo"/>
<field name="trululu" attrs="{'readonly': [['foo', '=', False]]}"/>
<field name="trululu" readonly="not foo"/>
<field name="int_field"/>
</group>
</sheet>
@@ -2831,7 +2835,7 @@ QUnit.module("Views", (hooks) => {
<form>
<group>
<field name="foo"/>
<field name="display_name" attrs="{'readonly': [['foo', '=', 'readonly']]}"/>
<field name="display_name" readonly="foo == 'readonly'"/>
</group>
</form>`,
resId: 2,
@@ -2912,9 +2916,9 @@ QUnit.module("Views", (hooks) => {
<sheet>
<label for="foo" string="Foo"/>
<field name="foo"/>
<label for="trululu" string="Trululu" attrs="{'readonly': [['foo', '=', False]]}"/>
<field name="trululu" attrs="{'readonly': [['foo', '=', False]]}"/>
<label for="int_field" string="IntField" attrs="{'readonly': [['int_field', '=', False]]}"/>
<label for="trululu" string="Trululu" readonly="not foo"/>
<field name="trululu" readonly="not foo"/>
<label for="int_field" string="IntField" readonly="not int_field"/>
<field name="int_field"/>
</sheet>
</form>`,
@@ -2947,7 +2951,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<group>
<field name="foo" attrs="{'required': [['bar', '=', True]]}"/>
<field name="foo" required="bar"/>
<field name="bar"/>
</group>
</sheet>
@@ -3063,7 +3067,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<group>
<separator string="Geolocation" attrs='{"invisible": [["bar", "=", True]]}'/>
<separator string="Geolocation" invisible='bar'/>
<field name="bar"/>
</group>
</sheet>
@@ -3252,7 +3256,7 @@ QUnit.module("Views", (hooks) => {
<header>
<button name="post" class="p" string="Confirm" type="object"/>
<button name="some_method" class="s" string="Do it" type="object"/>
<button name="some_other_method" states="ab,ef" string="Do not" type="object"/>
<button name="some_other_method" invisible="state not in ['ab', 'ef']" string="Do not" type="object"/>
</header>
<sheet>
<group>
@@ -5899,7 +5903,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<group>
<field name="foo" class="foo_field" attrs='{"invisible": [["bar", "=", True]]}'/>
<field name="foo" class="foo_field" invisible='bar'/>
<field name="bar"/>
</group>
</sheet>
@@ -6339,7 +6343,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<field name="p">
<tree editable="top">
<field name="display_name" attrs="{'readonly': [('timmy', '=', false)]}"/>
<field name="display_name" readonly="not timmy"/>
<field name="timmy"/>
</tree>
</field>
@@ -7580,7 +7584,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<form>
<field name="foo" attrs="{'readonly': [('bar','=',False)]}"/>
<field name="foo" readonly="not bar"/>
<field name="bar"/>
</form>`,
resId: 1,
@@ -7664,7 +7668,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<form>
<field name="bar"/>
<field name="timmy" widget="many2many_tags" attrs="{'readonly': [('bar','=',True)]}"/>
<field name="timmy" widget="many2many_tags" readonly="bar"/>
</form>`,
resId: 5,
});
@@ -7714,11 +7718,11 @@ QUnit.module("Views", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<footer attrs="{'invisible': [('bar','=',False)]}">
<footer invisible="not bar">
<button>Hello</button>
<button>World</button>
</footer>
<footer attrs="{'invisible': [('bar','!=',False)]}">
<footer invisible="bar">
<button>Foo</button>
</footer>
</form>`,
@@ -8531,7 +8535,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<notebook>
<page string="Foo" attrs='{"invisible": [["id", "=", 2]]}'>
<page string="Foo" invisible='id == 2'>
<field name="foo"/>
</page>
<page string="Bar">
@@ -9036,8 +9040,8 @@ QUnit.module("Views", (hooks) => {
<form>
<field name="foo" />
<group>
<div class="cell1" attrs='{"invisible": [["foo", "=", "1"]]}' />
<div class="cell2" attrs='{"invisible": [["foo", "=", "2"]]}' />
<div class="cell1" invisible='foo == "1"' />
<div class="cell2" invisible='foo == "2"' />
</group>
</form>`,
});
@@ -9953,7 +9957,7 @@ QUnit.module("Views", (hooks) => {
<tree><field name="display_name"/></tree>
<form>
<field name="display_name"/>
<field name="foo" attrs="{'readonly': [['display_name', '=', 'readonly']]}"/>
<field name="foo" readonly="display_name == 'readonly'"/>
</form>
</field>
</form>`,
@@ -10029,7 +10033,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<form>
<field name="id"/>
<field name="foo" attrs="{'readonly': [['id', '=', False]]}"/>
<field name="foo" readonly="not id"/>
</form>`,
});
@@ -10150,7 +10154,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<form>
<header>
<button name="some_method" class="oe_highlight" string="Button" type="object" title="This is title" attrs="{'readonly': [['display_name', '=', 'readonly']]}"/>
<button name="some_method" class="oe_highlight" string="Button" type="object" title="This is title" readonly="display_name == 'readonly'"/>
<button name="empty_method" string="Empty Button" type="object"/>
</header>
<button name="other_method" class="oe_highlight" string="Button2" type="object" help="help Button2"/>
@@ -10168,7 +10172,7 @@ QUnit.module("Views", (hooks) => {
await nextTick();
assert.strictEqual(
target.querySelector(".o-tooltip").textContent,
` Button : ButtonThis is titleObject:partnerModifiers:{"readonly":[["display_name","=","readonly"]]}Button Type:objectMethod:some_method`
` Button : ButtonThis is titleObject:partnerReadonly:display_name == 'readonly'Button Type:objectMethod:some_method`
);
await mouseEnter(target.querySelector("button[name='other_method']"));
@@ -10251,7 +10255,8 @@ QUnit.module("Views", (hooks) => {
<field name="p">
<tree>
<field name="foo"/>
<field name="bar" invisible="context.get('hide_bar', False)"/>
<field name="bar" column_invisible="context.get('hide_bar', False)"/>
<field name="int_field" column_invisible="True"/>
</tree>
</field>
</form>`,
@@ -10272,7 +10277,7 @@ QUnit.module("Views", (hooks) => {
"partner,false,list": `
<tree>
<field name="foo"/>
<field name="bar" invisible="context.get('hide_bar', False)"/>
<field name="bar" column_invisible="context.get('hide_bar', False)"/>
</tree>`,
};
await makeView({
@@ -10480,7 +10485,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<form>
<sheet>
<field name="product_id" context="{'lang': 'en_US'}" attrs='{"invisible": [["product_id", "=", 33]]}' widget="many2one"/>
<field name="product_id" context="{'lang': 'en_US'}" invisible="product_id == 33" widget="many2one"/>
</sheet>
</form>`,
});
@@ -10500,14 +10505,14 @@ QUnit.module("Views", (hooks) => {
);
assert.containsOnce(
target,
".o-tooltip--technical > li[data-item='modifiers']",
"modifiers should be present for this field"
".o-tooltip--technical > li[data-item='invisible']",
"invisible should be present for this field"
);
assert.strictEqual(
target.querySelector('.o-tooltip--technical > li[data-item="modifiers"]').lastChild
target.querySelector('.o-tooltip--technical > li[data-item="invisible"]').lastChild
.textContent,
'{"invisible":[["product_id","=",33]]}',
"modifiers should be properly stringified"
'product_id == 33',
"invisible should be properly stringified"
);
assert.containsOnce(
@@ -12507,7 +12512,7 @@ QUnit.module("Views", (hooks) => {
<form>
<field name="length"/>
<field name="display_name"/>
<field name="foo" attrs="{'readonly': [['display_name', '=', 'readonly']]}"/>
<field name="foo" readonly="display_name =='readonly'"/>
</form>
</field>
</form>`,
@@ -2416,8 +2416,8 @@ QUnit.module("Views", (hooks) => {
<graph type="pie">
<field name="revenue" type="measure"/>
<field name="date" interval="day"/>
<field name="foo" modifiers='{"invisible": false}'/>
<field name="bar" modifiers='{"invisible": true}' string="My invisible field"/>
<field name="foo" invisible="False"/>
<field name="bar" invisible="True" string="My invisible field"/>
<field name="id"/>
<field name="fighters" string="FooFighters"/>
</graph>
@@ -2619,7 +2619,7 @@ QUnit.module("Views", (hooks) => {
serverData.views["partner,some_view_ref,form"] =
"<form>" +
'<field name="foo" required="1"/>' +
'<field name="int_field" attrs=\'{"invisible": [["foo", "=", false]]}\'/>' +
'<field name="int_field" invisible="not foo"/>' +
"</form>";
await makeView({
@@ -8047,11 +8047,8 @@ QUnit.module("Views", (hooks) => {
'<field name="bar"/>' +
'<field name="state"/>' +
'<templates><div t-name="kanban-box">' +
'<button class="o_btn_test_1" type="object" name="a1" ' +
"attrs=\"{'invisible': [['foo', '!=', 'yop']]}\"/>" +
'<button class="o_btn_test_2" type="object" name="a2" ' +
"attrs=\"{'invisible': [['bar', '=', True]]}\" " +
'states="abc,def"/>' +
'<button class="o_btn_test_1" type="object" name="a1" invisible="foo != \'yop\'"/>' +
'<button class="o_btn_test_2" type="object" name="a2" invisible="bar and state not in [\'abc\', \'def\']"/>' +
"</div></templates>" +
"</kanban>",
});
@@ -8221,7 +8218,7 @@ QUnit.module("Views", (hooks) => {
<templates>
<t t-name="kanban-box">
<div>
<field name="foo" attrs="{'invisible': [['id', '=', 1]]}"/>
<field name="foo" invisible="id == 1"/>
</div>
</t>
</templates>
@@ -8396,7 +8393,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<kanban>
<field name="bar" />
<field name="product_id" attrs="{'readonly': [('bar', '!=', True)]}"/>
<field name="product_id" readonly="not bar"/>
<templates>
<t t-name="kanban-box">
<div><field name="id"/></div>
@@ -8563,7 +8560,7 @@ QUnit.module("Views", (hooks) => {
<t t-name="kanban-box">
<div>
<field name="foo"/>
<button type="object" attrs="{'invisible':['|', ('bar','=',True), ('category_ids', '!=', [])]}" class="btn btn-primary float-end" name="arbitrary">Join</button>
<button type="object" invisible="bar or category_ids" class="btn btn-primary float-end" name="arbitrary">Join</button>
</div>
</t>
</templates>
+121 -45
View File
@@ -746,10 +746,10 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree>
<button name="a" type="object" icon="fa-car"/>
<field name="foo" invisible="1"/>
<!--Here the invisible=1 is used to simulate a group on the case that the user
<field name="foo" column_invisible="1"/>
<!--Here the column_invisible=1 is used to simulate a group on the case that the user
don't have the rights to see the button.-->
<button name="b" type="object" icon="fa-car" invisible="1"/>
<button name="b" type="object" icon="fa-car" column_invisible="1"/>
<button name="x" type="object" icon="fa-star"/>
<button name="y" type="object" icon="fa-refresh"/>
<button name="z" type="object" icon="fa-exclamation"/>
@@ -786,7 +786,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree>
<button name="a" type="object" icon="fa-car"/>
<field name="foo" attrs="{'invisible': [['foo', '=', 'blip']]}"/>
<field name="foo" invisible="foo == 'blip'"/>
<button name="x" type="object" icon="fa-star"/>
<button name="y" type="object" icon="fa-refresh"/>
<button name="z" type="object" icon="fa-exclamation"/>
@@ -839,9 +839,9 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree>
<field name="foo"/>
<button name="x" type="object" icon="fa-star" attrs="{'invisible': [['foo', '=', 'blip']]}"/>
<button name="y" type="object" icon="fa-refresh" attrs="{'invisible': [['foo', '=', 'yop']]}"/>
<button name="z" type="object" icon="fa-exclamation" attrs="{'invisible': [['foo', '=', 'gnap']]}"/>
<button name="x" type="object" icon="fa-star" invisible="foo == 'blip'"/>
<button name="y" type="object" icon="fa-refresh" invisible="foo == 'yop'"/>
<button name="z" type="object" icon="fa-exclamation" invisible="foo == 'gnap'"/>
</tree>`,
});
@@ -1405,7 +1405,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree>
<field name="foo"/>
<field name="bar" invisible="1"/>
<field name="bar" column_invisible="1"/>
</tree>`,
});
@@ -1422,8 +1422,9 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree>
<field name="foo" invisible="context.get('notInvisible')"/>
<field name="bar" invisible="context.get('invisible')"/>
<field name="date" column_invisible="True"/>
<field name="foo" column_invisible="context.get('notInvisible')"/>
<field name="bar" column_invisible="context.get('invisible')"/>
</tree>`,
context: {
invisible: true,
@@ -1437,6 +1438,81 @@ QUnit.module("Views", (hooks) => {
}
);
QUnit.test(
"invisible column based on the context are correctly displayed in o2m",
async function (assert) {
serverData.models.foo.fields.foo_o2m = {
string: "Foo O2M",
type: "one2many",
relation: "foo",
};
await makeView({
type: "form",
resModel: "foo",
serverData,
resId: 1,
arch: `
<form>
<sheet>
<field name="foo_o2m">
<tree>
<field name="foo" column_invisible="context.get('notInvisible')"/>
<field name="bar" column_invisible="context.get('invisible')"/>
</tree>
</field>
</sheet>
</form>`,
context: {
invisible: true,
notInvisible: false,
},
});
// 1 for 1 visible column (foo), 1 th for delete button
assert.containsN(target, "th", 2, "should have 2 th");
assert.strictEqual(target.querySelectorAll("th")[0].dataset.name, "foo");
}
);
QUnit.test(
"invisible column based on the parent are correctly displayed in o2m",
async function (assert) {
serverData.models.foo.fields.foo_o2m = {
string: "Foo O2M",
type: "one2many",
relation: "foo",
};
await makeView({
type: "form",
resModel: "foo",
serverData,
resId: 1,
arch: `
<form>
<sheet>
<field name="int_field"/>
<field name="m2m" invisible="True"/>
<field name="properties" invisible="True"/>
<field name="foo_o2m">
<tree>
<field name="date" column_invisible="True"/>
<field name="foo" column_invisible="parent.int_field == 3"/>
<field name="bar" column_invisible="parent.int_field == 10"/>
<field name="qux" column_invisible="parent.m2m"/>
<field name="amount" column_invisible="parent.properties"/>
</tree>
</field>
</sheet>
</form>`,
});
// 1 for 2 visible column (foo, properties), 1 th for delete button
assert.containsN(target, "th", 3, "should have 3 th");
assert.strictEqual(target.querySelectorAll("th")[0].dataset.name, "foo");
assert.strictEqual(target.querySelectorAll("th")[1].dataset.name, "amount");
}
);
QUnit.test("save a record with an invisible required field", async function (assert) {
serverData.models.foo.fields.foo.required = true;
@@ -1446,7 +1522,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="foo" invisible="1"/>
<field name="foo" column_invisible="1"/>
<field name="int_field"/>
</tree>`,
mockRPC(_, { args, method }) {
@@ -1625,7 +1701,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree>
<field name="foo"/>
<field name="bar" attrs="{'invisible': [('id','=', 1)]}"/>
<field name="bar" invisible="id == 1"/>
<field name="int_field"/>
</tree>`,
});
@@ -1642,7 +1718,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree>
<field name="foo" attrs="{'invisible': [('id','=', 1)]}"/>
<field name="foo" invisible="id == 1"/>
</tree>`,
});
@@ -1662,7 +1738,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree editable="top">
<field name="foo"/>
<field name="m2o" invisible="1"/>
<field name="m2o" column_invisible="1"/>
</tree>`,
mockRPC(route) {
assert.step(route.split("/").pop());
@@ -3993,8 +4069,8 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree>
<field name="company_currency_id" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="company_currency_id" column_invisible="1"/>
<field name="currency_id" column_invisible="1"/>
<field name="amount"/>
<field name="amount_currency"/>
</tree>`,
@@ -4905,7 +4981,7 @@ QUnit.module("Views", (hooks) => {
<form>
<sheet>
<field name="bar"/>
<field name="o2m" attrs="{'invisible': [('bar', '=', True)]}">
<field name="o2m" invisible="bar">
<tree editable="bottom">
<field name="titi"/>
<field name="grosminet"/>
@@ -5437,7 +5513,7 @@ QUnit.module("Views", (hooks) => {
<field name="date"/>
<field name="text"/>
<field name="amount"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
<field name="m2o"/>
<field name="m2m" widget="many2many_tags"/>
</tree>`,
@@ -7144,7 +7220,7 @@ QUnit.module("Views", (hooks) => {
resModel: "foo",
serverData,
arch: `
<tree decoration-info="datetime == '2017-02-27 12:51:35'" decoration-danger="datetime &gt; '2017-02-27 12:51:35' AND datetime &lt; '2017-02-27 10:51:35'">
<tree decoration-info="datetime == '2017-02-27 12:51:35'" decoration-danger="datetime &gt; '2017-02-27 12:51:35' and datetime &lt; '2017-02-27 10:51:35'">
<field name="datetime"/>
<field name="int_field"/>
</tree>`,
@@ -7871,7 +7947,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<groupby name="currency_id">
<field name="position"/>
<button string="Button 1" type="object" name="button_method" attrs='{"invisible": [("position", "=", "after")]}'/>
<button string="Button 1" type="object" name="button_method" invisible="position == 'after'"/>
</groupby>
</tree>`,
mockRPC(route, args) {
@@ -7918,7 +7994,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<groupby name="currency_id">
<field name="m2o"/>
<button string="Button 1" type="object" name="button_method" attrs='{"invisible": [("m2o", "=", false)]}'/>
<button string="Button 1" type="object" name="button_method" invisible="not m2o"/>
</groupby>
</tree>`,
mockRPC(route, args) {
@@ -7950,7 +8026,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<groupby name="currency_id">
<field name="position"/>
<button string="Button 1" type="object" name="button_method" attrs='{"invisible": [("position", "=", "after")]}'/>
<button string="Button 1" type="object" name="button_method" invisible="position == 'after'"/>
</groupby>
</tree>`,
groupBy: ["currency_id"],
@@ -7972,7 +8048,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<groupby name="currency_id">
<field name="position"/>
<button string="Button 1" type="object" name="button_method" attrs='{"invisible": [("position", "=", "after")]}'/>
<button string="Button 1" type="object" name="button_method" invisible="position == 'after'"/>
</groupby>
</tree>`,
groupBy: ["currency_id"],
@@ -8046,7 +8122,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<field name="currency_id"/>
<groupby name="currency_id">
<field name="position" invisible="1"/>
<field name="position" column_invisible="1"/>
</groupby>
</tree>`,
groupBy: ["currency_id"],
@@ -8422,10 +8498,10 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree editable="top">
<button string="a button" name="button_action" icon="fa-car" type="object" attrs="{'invisible': [('id','=', 1)]}"/>
<button string="a button" name="button_action" icon="fa-car" type="object" invisible="id == 1"/>
<field name="int_field"/>
<field name="qux"/>
<field name="foo" attrs="{'invisible': [('id','=', 1)]}"/>
<field name="foo" invisible="id == 1"/>
</tree>`,
});
@@ -8459,7 +8535,7 @@ QUnit.module("Views", (hooks) => {
<tree>
<field name="id"/>
<field name="amount"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
@@ -8874,7 +8950,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="foo" attrs="{'invisible': [['bar', '=', True]]}"/>
<field name="foo" invisible="bar"/>
<field name="bar"/>
</tree>`,
});
@@ -8927,7 +9003,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="foo" attrs="{'readonly': [['bar', '=', True]]}"/>
<field name="foo" readonly="bar"/>
<field name="bar"/>
</tree>`,
});
@@ -8984,7 +9060,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="foo" attrs="{'required': [['bar', '=', True]]}"/>
<field name="foo" required="bar"/>
<field name="bar"/>
</tree>`,
});
@@ -9059,7 +9135,7 @@ QUnit.module("Views", (hooks) => {
<form>
<field name="o2m">
<tree editable="top">
<field name="display_name" attrs="{'invisible': [('stage', '=', 'open')]}"/>
<field name="display_name" invisible="stage == 'open'"/>
<field name="stage"/>
</tree>
</field>
@@ -10096,7 +10172,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree editable="bottom">
<field name="foo"/>
<field name="bar" invisible="1"/>
<field name="bar" column_invisible="1"/>
<field name="int_field"/>
</tree>`,
resId: 1,
@@ -10260,7 +10336,7 @@ QUnit.module("Views", (hooks) => {
<field name="foo"/>
<field name="qux"/>
<field name="amount" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" column_invisible="1"/>
</tree>`,
});
patchWithCleanup(session, { currencies });
@@ -10962,7 +11038,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree expand="1">
<field name="foo" invisible="1"/>
<field name="foo" column_invisible="1"/>
<field name="reference"/>
</tree>`,
groupBy: ["foo"],
@@ -11039,7 +11115,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree expand="1" multi_edit="1">
<field name="foo" invisible="1"/>
<field name="foo" column_invisible="1"/>
<field name="bar" widget="boolean_toggle"/>
<field name="reference"/>
</tree>`,
@@ -12053,7 +12129,7 @@ QUnit.module("Views", (hooks) => {
<tree multi_edit="1">
<field name="id"/>
<field name="foo"/>
<field name="int_field" attrs='{"readonly": [("id", ">" , 2)]}'/>
<field name="int_field" readonly="id > 2"/>
</tree>`,
mockRPC(route, args) {
if (args.method === "write") {
@@ -12637,8 +12713,8 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree editable="top">
<field name="bar"/>
<field name="foo" attrs="{'readonly': [['bar','=',True]]}"/>
<field name="m2o" attrs="{'readonly': [['bar','=',False]]}"/>
<field name="foo" readonly="bar"/>
<field name="m2o" readonly="not bar"/>
<field name="int_field"/>
</tree>`,
});
@@ -13288,9 +13364,9 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<tree editable="top">
<field name="id" invisible="1"/>
<field name="foo" attrs="{'readonly': [['id','!=',False]]}"/>
<field name="int_field" attrs="{'invisible': [['id','!=',False]]}"/>
<field name="id" column_invisible="1"/>
<field name="foo" readonly="id"/>
<field name="int_field" invisible="id"/>
</tree>`,
});
@@ -13330,7 +13406,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree editable="bottom">
<field name="foo"/>
<field name="bar" attrs="{'readonly': [('foo', '!=', 'yop')]}"/>
<field name="bar" readonly="foo != 'yop'"/>
</tree>`,
});
@@ -17131,7 +17207,7 @@ QUnit.module("Views", (hooks) => {
<field name="o2m">
<tree editable="bottom">
<field name="foo"/>
<field name="date" attrs="{'readonly': [('foo', '!=', 'yop')]}"/>
<field name="date" readonly="foo != 'yop'"/>
<field name="int_field"/>
</tree>
</field>
@@ -17993,7 +18069,7 @@ QUnit.module("Views", (hooks) => {
serverData,
arch: `
<list>
<field name="bar" invisible="1"/>
<field name="bar" column_invisible="1"/>
<widget name="test_widget"/>
</list>
`,
@@ -19039,7 +19115,7 @@ QUnit.module("Views", (hooks) => {
arch: `
<tree editable="bottom">
<field name="m2o"/>
<field name="properties" invisible="1"/>
<field name="properties" column_invisible="1"/>
</tree>
`,
mockRPC(route, { method, args }) {
@@ -107,11 +107,11 @@ QUnit.module("ViewDialogs", (hooks) => {
"partner,false,form": `
<form>
<field name="bar"/>
<footer attrs="{'invisible': [('bar','=',False)]}">
<footer invisible="not bar">
<button>Hello</button>
<button>World</button>
</footer>
<footer attrs="{'invisible': [('bar','!=',False)]}">
<footer invisible="bar">
<button>Foo</button>
</footer>
</form>`,
+1 -1
View File
@@ -340,7 +340,7 @@ export function getActionManagerServerData() {
"partner,666,form": `<form>
<header></header>
<sheet>
<div class="oe_button_box" name="button_box" modifiers="{}">
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="action" name="1" icon="fa-star" context="{'default_partner': active_id}">
<field string="Partners" name="o2m" widget="statinfo"/>
</button>
@@ -97,7 +97,7 @@ QUnit.module("SettingsFormView", (hooks) => {
<div>This is a different setting</div>
</setting>
</block>
<block title="Hide group Foo" attrs="{'invisible': [('bar','=',False)]}">
<block title="Hide group Foo" invisible="not bar">
<setting string="Hide Foo" help="this is hide foo">
<field name="foo"/>
</setting>
@@ -59,7 +59,7 @@ QUnit.test("basic rendering", async (assert) => {
<form>
<field name="lines" widget="slide_category_one2many">
<tree>
<field name="is_category" invisible="1"/>
<field name="is_category" column_invisible="1"/>
<field name="name"/>
<field name="display_name"/>
<field name="int"/>
@@ -87,7 +87,7 @@ QUnit.test("click on section behaves as usual in readonly mode", async (assert)
<form>
<field name="lines" widget="slide_category_one2many">
<tree>
<field name="is_category" invisible="1"/>
<field name="is_category" column_invisible="1"/>
<field name="name"/>
<field name="int"/>
</tree>
@@ -109,7 +109,7 @@ QUnit.test("click on section edit the section in place", async (assert) => {
<form>
<field name="lines" widget="slide_category_one2many">
<tree>
<field name="is_category" invisible="1"/>
<field name="is_category" column_invisible="1"/>
<field name="name"/>
<field name="int"/>
</tree>
@@ -131,7 +131,7 @@ QUnit.test("click on real line opens a dialog", async (assert) => {
<form>
<field name="lines" widget="slide_category_one2many">
<tree>
<field name="is_category" invisible="1"/>
<field name="is_category" column_invisible="1"/>
<field name="name"/>
<field name="int"/>
</tree>
@@ -152,7 +152,7 @@ QUnit.test("can create section inline", async (assert) => {
<form>
<field name="lines" widget="slide_category_one2many">
<tree>
<field name="is_category" invisible="1"/>
<field name="is_category" column_invisible="1"/>
<field name="name"/>
<field name="int"/>
<control>
@@ -180,7 +180,7 @@ QUnit.test("creates real record in form dialog", async (assert) => {
<form>
<field name="lines" widget="slide_category_one2many">
<tree>
<field name="is_category" invisible="1"/>
<field name="is_category" column_invisible="1"/>
<field name="name"/>
<field name="int"/>
<control>