[IMP] web: support of properties in domain selector
Before this commit, users could not query properties with the domain selector. Now, all the properties are listed when selecting a properties field. task id: 3283212 closes odoo/odoo#115270 Signed-off-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
co-authored by
Mathieu Duckerts-Antoine
parent
187ad14840
commit
a79765ed33
@@ -164,7 +164,7 @@ export class DomainSelector extends Component {
|
||||
fieldDef = { type: "integer" };
|
||||
}
|
||||
node.field = { ...fieldDef, name: field };
|
||||
node.operator = getOperatorsInfo(fieldDef.type)[0];
|
||||
node.operator = getOperatorsInfo(fieldDef)[0];
|
||||
node.value = getDefaultFieldValue(fieldDef);
|
||||
this.notifyChanges();
|
||||
}
|
||||
@@ -205,11 +205,11 @@ export class DomainSelector extends Component {
|
||||
}
|
||||
|
||||
getEditorInfo(node) {
|
||||
return getEditorInfo(node.field.type, node.operator.key);
|
||||
return getEditorInfo(node.field, node.operator.key);
|
||||
}
|
||||
|
||||
getOperatorsInfo(node) {
|
||||
const operators = getOperatorsInfo(node.field.type);
|
||||
const operators = getOperatorsInfo(node.field);
|
||||
if (!operators.some((op) => op.key === node.operator.key)) {
|
||||
operators.push(node.operator);
|
||||
}
|
||||
|
||||
@@ -236,7 +236,32 @@ const SELECTION = {
|
||||
in: SELECTION_EDITOR_IN,
|
||||
not_in: SELECTION_EDITOR_IN,
|
||||
},
|
||||
defaultValue: (field) => field.selection[0][0],
|
||||
defaultValue: (field) => field.selection[0][0] ?? false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const PROPERTIES = {
|
||||
operators: ["set", "not_set"],
|
||||
defaultValue: () => false,
|
||||
};
|
||||
|
||||
const PROPERTIES_SELECTION = {
|
||||
operators: ["equal", "not_equal", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Select, ({ value, update, field }) => ({
|
||||
value,
|
||||
update,
|
||||
options: field.selection || [],
|
||||
})),
|
||||
},
|
||||
defaultValue: (field) => field.selection?.[0]?.[0] ?? false,
|
||||
};
|
||||
|
||||
const PROPERTIES_RELATIONAL = {
|
||||
operators: ["equal", "not_equal", "set", "not_set"],
|
||||
editors: { default: makeEditor(Input) },
|
||||
defaultValue: (field) => (field.type === "many2one" ? 1 : []),
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -253,24 +278,40 @@ export const FIELD_DESCRIPTIONS = {
|
||||
many2one: RELATIONAL,
|
||||
monetary: NUMBER,
|
||||
one2many: RELATIONAL,
|
||||
properties: PROPERTIES,
|
||||
properties_definition: PROPERTIES,
|
||||
selection: SELECTION,
|
||||
text: TEXT,
|
||||
};
|
||||
|
||||
export function getFieldInfo(fieldType) {
|
||||
return FIELD_DESCRIPTIONS[fieldType] || DEFAULT;
|
||||
const PROPERTIES_DESCRIPTIONS = {
|
||||
selection: PROPERTIES_SELECTION,
|
||||
many2many: PROPERTIES_RELATIONAL,
|
||||
many2one: PROPERTIES_RELATIONAL,
|
||||
one2many: PROPERTIES_RELATIONAL,
|
||||
tags: PROPERTIES_RELATIONAL,
|
||||
};
|
||||
|
||||
export function getFieldInfo(fieldDef) {
|
||||
if (fieldDef.is_property && PROPERTIES_DESCRIPTIONS[fieldDef.type]) {
|
||||
return PROPERTIES_DESCRIPTIONS[fieldDef.type];
|
||||
} else if (FIELD_DESCRIPTIONS[fieldDef.type]) {
|
||||
return FIELD_DESCRIPTIONS[fieldDef.type];
|
||||
} else {
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEditorInfo(fieldType, operator) {
|
||||
const fieldInfo = getFieldInfo(fieldType);
|
||||
export function getEditorInfo(fieldDef, operator) {
|
||||
const fieldInfo = getFieldInfo(fieldDef);
|
||||
return fieldInfo.editors[operator] || fieldInfo.editors.default;
|
||||
}
|
||||
|
||||
export function getOperatorsInfo(fieldType) {
|
||||
return selectOperators(getFieldInfo(fieldType).operators);
|
||||
export function getOperatorsInfo(fieldDef) {
|
||||
return selectOperators(getFieldInfo(fieldDef).operators);
|
||||
}
|
||||
|
||||
export function getDefaultFieldValue(field) {
|
||||
const desc = getFieldInfo(field.type);
|
||||
return desc.defaultValue(field);
|
||||
export function getDefaultFieldValue(fieldDef) {
|
||||
const desc = getFieldInfo(fieldDef);
|
||||
return desc.defaultValue(fieldDef);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Cache } from "@web/core/utils/cache";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/**
|
||||
@@ -40,16 +41,70 @@ export const fieldService = {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} resModel valid model name
|
||||
* @param {Object} fieldDefs
|
||||
* @param {string} name
|
||||
* @param {import("@web/core/domain").DomainListRepr} [domain=[]]
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function _loadPropertyDefinitions(fieldDefs, name, domain = []) {
|
||||
const {
|
||||
definition_record: definitionRecord,
|
||||
definition_record_field: definitionRecordField,
|
||||
} = fieldDefs[name];
|
||||
const definitionRecordModel = fieldDefs[definitionRecord].relation;
|
||||
|
||||
domain = Domain.and([[[definitionRecordField, "!=", false]], domain]).toList();
|
||||
|
||||
const records = await orm.searchRead(definitionRecordModel, domain, [
|
||||
"display_name",
|
||||
definitionRecordField,
|
||||
]);
|
||||
|
||||
const definitions = {};
|
||||
for (const record of records) {
|
||||
for (const definition of record[definitionRecordField]) {
|
||||
definitions[definition.name] = {
|
||||
is_property: true,
|
||||
// for now, all properties are searchable but their definitions don't contain that info
|
||||
searchable: true,
|
||||
// differentiate definitions with same name but on different parent
|
||||
record_name: record.display_name,
|
||||
...definition,
|
||||
};
|
||||
}
|
||||
}
|
||||
return definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} resModel
|
||||
* @param {string} fieldName
|
||||
* @param {import("@web/core/domain").DomainListRepr} [domain]
|
||||
* @returns {Promise<object[]>}
|
||||
*/
|
||||
async function loadPropertyDefinitions(resModel, fieldName, domain) {
|
||||
const fieldDefs = await loadFields(resModel);
|
||||
return _loadPropertyDefinitions(fieldDefs, fieldName, domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null} resModel valid model name or null (case virtual)
|
||||
* @param {Object|null} fieldDefs
|
||||
* @param {string[]} names
|
||||
*/
|
||||
async function _loadPath(resModel, fieldDefs, names) {
|
||||
if (!fieldDefs) {
|
||||
return { isInvalid: "path", names, modelsInfo: [] };
|
||||
}
|
||||
|
||||
const [name, ...remainingNames] = names;
|
||||
const modelsInfo = [{ resModel, fieldDefs }];
|
||||
if (resModel === "*" && remainingNames.length) {
|
||||
return { isInvalid: "path", names, modelsInfo };
|
||||
}
|
||||
|
||||
const fieldDef = fieldDefs[name];
|
||||
if (name !== "*" && !fieldDef) {
|
||||
if ((name !== "*" && !fieldDef) || (name === "*" && remainingNames.length)) {
|
||||
return { isInvalid: "path", names, modelsInfo };
|
||||
}
|
||||
|
||||
@@ -57,16 +112,22 @@ export const fieldService = {
|
||||
return { names, modelsInfo };
|
||||
}
|
||||
|
||||
if (name === "*") {
|
||||
return { isInvalid: "path", names, modelsInfo };
|
||||
}
|
||||
|
||||
let subResult;
|
||||
if (fieldDef.relation) {
|
||||
const subResult = await _loadPath(
|
||||
subResult = await _loadPath(
|
||||
fieldDef.relation,
|
||||
await loadFields(fieldDef.relation),
|
||||
remainingNames
|
||||
);
|
||||
} else if (fieldDef.type === "properties") {
|
||||
subResult = await _loadPath(
|
||||
"*",
|
||||
await _loadPropertyDefinitions(fieldDefs, name),
|
||||
remainingNames
|
||||
);
|
||||
}
|
||||
|
||||
if (subResult) {
|
||||
const result = {
|
||||
names,
|
||||
modelsInfo: [...modelsInfo, ...subResult.modelsInfo],
|
||||
@@ -95,7 +156,7 @@ export const fieldService = {
|
||||
return _loadPath(resModel, fieldDefs, path.split("."));
|
||||
}
|
||||
|
||||
return { loadFields, loadPath };
|
||||
return { loadFields, loadPath, loadPropertyDefinitions };
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -89,6 +89,13 @@ export class ModelFieldSelector extends Component {
|
||||
}
|
||||
const { isInvalid, modelsInfo, names } = await this.fieldService.loadPath(resModel, path);
|
||||
const result = { isInvalid: !!isInvalid, displayNames: [] };
|
||||
if (!isInvalid) {
|
||||
const lastFieldDef = modelsInfo.at(-1).fieldDefs[names.at(-1)];
|
||||
if (["properties", "properties_definition"].includes(lastFieldDef.type)) {
|
||||
// there is no known case where we want to select a 'properties' field directly
|
||||
result.isInvalid = true;
|
||||
}
|
||||
}
|
||||
for (let index = 0; index < names.length; index++) {
|
||||
const name = names[index];
|
||||
const fieldDef = modelsInfo[index]?.fieldDefs[name];
|
||||
|
||||
@@ -105,9 +105,7 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
setup() {
|
||||
this.fieldService = useService("field");
|
||||
this.state = useState({ page: null });
|
||||
|
||||
this.keepLast = new KeepLast();
|
||||
|
||||
this.debouncedSearchFields = debounce(this.searchFields.bind(this), 250);
|
||||
|
||||
onWillStart(async () => {
|
||||
@@ -201,6 +199,9 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
}
|
||||
|
||||
selectField(field) {
|
||||
if (field.type === "properties") {
|
||||
return this.followRelation(field);
|
||||
}
|
||||
this.keepLast.add(Promise.resolve());
|
||||
this.state.page.selectedName = field.name;
|
||||
this.props.update(this.state.page.path);
|
||||
@@ -245,7 +246,7 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
const focusedFieldName = this.state.page.focusedFieldName;
|
||||
if (focusedFieldName) {
|
||||
const fieldDef = this.state.page.fieldDefs[focusedFieldName];
|
||||
if (fieldDef.relation) {
|
||||
if (fieldDef.relation || fieldDef.type === "properties") {
|
||||
this.followRelation(fieldDef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,13 @@
|
||||
<li class="o_model_field_selector_popover_item d-flex" t-att-class="{ 'active': fieldName === state.page.focusedFieldName }" t-att-data-name="fieldName">
|
||||
<button class="o_model_field_selector_popover_item_name btn flex-fill text-start" t-on-click="() => this.selectField(fieldDef)">
|
||||
<t t-esc="fieldDef.string" />
|
||||
<t t-if="fieldDef.record_name"> (<t t-esc="fieldDef.record_name" />)</t>
|
||||
<div t-if="props.isDebugMode" class="text-muted o_model_field_selector_popover_item_title"><t t-esc="fieldName"/> (<t t-esc="fieldDef.type"/>)</div>
|
||||
</button>
|
||||
<t t-if="fieldDef.relation and props.followRelations">
|
||||
<t t-if="(!fieldDef.is_property and fieldDef.relation and props.followRelations) or fieldDef.type === 'properties'">
|
||||
<button class="o_model_field_selector_popover_item_relation btn btn-link" t-on-click.stop="() => this.followRelation(fieldDef)">
|
||||
<i class="fa fa-chevron-right o_model_field_selector_popover_relation_icon" role="img" aria-label="Relation to follow" title="Relation to follow" />
|
||||
</button>
|
||||
</button>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
@@ -799,4 +799,96 @@ QUnit.module("Components", (hooks) => {
|
||||
);
|
||||
assert.containsOnce(target, ".o_model_field_selector_warning");
|
||||
});
|
||||
|
||||
QUnit.test("support properties", async (assert) => {
|
||||
assert.expect(7);
|
||||
|
||||
serverData.models.partner.fields.properties = {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
searchable: true,
|
||||
};
|
||||
serverData.models.product.fields.definitions = {
|
||||
string: "Definitions",
|
||||
type: "properties_definition",
|
||||
};
|
||||
serverData.models.product.records[0].definitions = [
|
||||
{ name: "xphone_prop_1", string: "P1", type: "boolean" },
|
||||
{ name: "xphone_prop_2", string: "P2", type: "selection", selection: [] },
|
||||
];
|
||||
serverData.models.product.records[1].definitions = [
|
||||
{ name: "xpad_prop_1", string: "P3", type: "many2one", relation: "partner" },
|
||||
];
|
||||
|
||||
let expectedDomain = `[("id", "=", 1)]`;
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<DomainSelector
|
||||
resModel="'partner'"
|
||||
value="value"
|
||||
readonly="false"
|
||||
isDebugMode="true"
|
||||
update="(domain) => this.onUpdate(domain)"
|
||||
/>
|
||||
`;
|
||||
static components = { DomainSelector };
|
||||
setup() {
|
||||
this.value = expectedDomain;
|
||||
}
|
||||
onUpdate(domain) {
|
||||
assert.strictEqual(domain, expectedDomain);
|
||||
this.value = domain;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountComponent(Parent);
|
||||
await openModelFieldSelectorPopover(target);
|
||||
await click(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='properties'] .o_model_field_selector_popover_relation_icon"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").textContent,
|
||||
"Properties"
|
||||
);
|
||||
|
||||
expectedDomain = `[("properties.xphone_prop_2", "=", False)]`;
|
||||
await click(
|
||||
target.querySelector(
|
||||
".o_model_field_selector_popover_item[data-name='xphone_prop_2'] button"
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").textContent,
|
||||
"PropertiesP2"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_domain_leaf_operator_select option")].map(
|
||||
(e) => e.value
|
||||
),
|
||||
["equal", "not_equal", "set", "not_set"]
|
||||
);
|
||||
|
||||
await openModelFieldSelectorPopover(target);
|
||||
expectedDomain = `[("properties.xpad_prop_1", "=", 1)]`;
|
||||
await click(
|
||||
target.querySelector(
|
||||
".o_model_field_selector_popover_item[data-name='xpad_prop_1'] button"
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").textContent,
|
||||
"PropertiesP3"
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_domain_leaf_operator_select option")].map(
|
||||
(e) => e.value
|
||||
),
|
||||
["equal", "not_equal", "set", "not_set"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,22 @@ function getModelInfo(resModel) {
|
||||
};
|
||||
}
|
||||
|
||||
function getDefinitions() {
|
||||
const records = serverData.models.species.records;
|
||||
const fieldDefs = {};
|
||||
for (const record of records) {
|
||||
for (const definition of record.definitions) {
|
||||
fieldDefs[definition.name] = {
|
||||
is_property: true,
|
||||
searchable: true,
|
||||
record_name: record.display_name,
|
||||
...definition,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { resModel: "*", fieldDefs };
|
||||
}
|
||||
|
||||
let serverData;
|
||||
QUnit.module("Field Service", {
|
||||
async beforeEach() {
|
||||
@@ -26,6 +42,13 @@ QUnit.module("Field Service", {
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
age: { type: "integer", string: "Age" },
|
||||
location_id: { type: "many2one", string: "Location", relation: "location" },
|
||||
species: { type: "many2one", string: "Species", relation: "species" },
|
||||
property_field: {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
definition_record: "species",
|
||||
definition_record_field: "definitions",
|
||||
},
|
||||
},
|
||||
},
|
||||
location: {
|
||||
@@ -37,6 +60,42 @@ QUnit.module("Field Service", {
|
||||
tortoise_ids: { type: "one2many", string: "Turtles", relation: "tortoise" },
|
||||
},
|
||||
},
|
||||
species: {
|
||||
fields: {
|
||||
id: { string: "ID", type: "integer" },
|
||||
display_name: { string: "Display Name", type: "char" },
|
||||
name: { string: "Name", type: "char", default: "name" },
|
||||
write_date: { string: "Last Modified on", type: "datetime" },
|
||||
definitions: { string: "Definitions", type: "properties_definition" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: "Galápagos tortoise",
|
||||
definitions: [
|
||||
{
|
||||
name: "galapagos_lifespans",
|
||||
string: "Lifespans",
|
||||
type: "integer",
|
||||
},
|
||||
{
|
||||
name: "location_ids",
|
||||
string: "Locations",
|
||||
type: "many2many",
|
||||
relation: "location",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
display_name: "Aldabra giant tortoise",
|
||||
definitions: [
|
||||
{ name: "aldabra_lifespans", string: "Lifespans", type: "integer" },
|
||||
{ name: "color", string: "Color", type: "char" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("field", fieldService);
|
||||
@@ -118,6 +177,31 @@ QUnit.test("loadPath", async (assert) => {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "property_field",
|
||||
expectedResult: {
|
||||
names: ["property_field"],
|
||||
modelsInfo: [getModelInfo("tortoise")],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "property_field.galapagos_lifespans",
|
||||
expectedResult: {
|
||||
names: ["property_field", "galapagos_lifespans"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getDefinitions()],
|
||||
},
|
||||
},
|
||||
{
|
||||
resModel: "tortoise",
|
||||
path: "property_field.location_ids.tortoise_ids",
|
||||
expectedResult: {
|
||||
isInvalid: "path",
|
||||
names: ["property_field", "location_ids", "tortoise_ids"],
|
||||
modelsInfo: [getModelInfo("tortoise"), getDefinitions()],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const env = await makeTestEnv({ serverData });
|
||||
|
||||
@@ -63,6 +63,27 @@ function getFocusedFieldName(target) {
|
||||
return target.querySelector(".o_model_field_selector_popover_item.active").innerText;
|
||||
}
|
||||
|
||||
function addProperties() {
|
||||
serverData.models.partner.fields.properties = {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
searchable: true,
|
||||
};
|
||||
serverData.models.product.fields.definitions = {
|
||||
string: "Definitions",
|
||||
type: "properties_definition",
|
||||
};
|
||||
serverData.models.product.records[0].definitions = [
|
||||
{ name: "xphone_prop_1", string: "P1", type: "boolean" },
|
||||
{ name: "xphone_prop_2", string: "P2", type: "char" },
|
||||
];
|
||||
serverData.models.product.records[1].definitions = [
|
||||
{ name: "xpad_prop_1", string: "P1", type: "date" },
|
||||
];
|
||||
}
|
||||
|
||||
QUnit.module("Components", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
@@ -720,4 +741,91 @@ QUnit.module("Components", (hooks) => {
|
||||
target.querySelector(".o_model_field_selector_popover_search .o_input")
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("support properties", async (assert) => {
|
||||
addProperties();
|
||||
|
||||
class Parent extends Component {
|
||||
static components = { ModelFieldSelector };
|
||||
static template = xml`
|
||||
<ModelFieldSelector
|
||||
readonly="false"
|
||||
resModel="'partner'"
|
||||
path="path"
|
||||
isDebugMode="true"
|
||||
update="(path, fieldInfo) => this.onUpdate(path)"
|
||||
/>
|
||||
`;
|
||||
setup() {
|
||||
this.path = "foo";
|
||||
}
|
||||
onUpdate(path) {
|
||||
this.path = path;
|
||||
assert.step(path);
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
await mountComponent(Parent);
|
||||
await openModelFieldSelectorPopover(target);
|
||||
assert.strictEqual(getTitle(target), "");
|
||||
assert.containsOnce(target, ".o_model_field_selector_popover_item[data-name='properties']");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='properties'] .o_model_field_selector_popover_relation_icon"
|
||||
);
|
||||
assert.deepEqual(getModelFieldSelectorValues(target), ["Foo"]);
|
||||
assert.containsNone(target, ".o_model_field_selector_warning");
|
||||
|
||||
await click(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='properties'] .o_model_field_selector_popover_relation_icon",
|
||||
"click on the relation icon should open the properties page"
|
||||
);
|
||||
assert.deepEqual(getModelFieldSelectorValues(target), ["Properties"]);
|
||||
assert.containsOnce(target, ".o_model_field_selector_warning");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await clickPrev(target);
|
||||
assert.strictEqual(getTitle(target), "");
|
||||
await click(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='properties'] .o_model_field_selector_popover_item_name",
|
||||
"click on the name should open the properties page"
|
||||
);
|
||||
assert.strictEqual(getTitle(target), "Properties");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").textContent,
|
||||
"Properties"
|
||||
);
|
||||
assert.containsN(target, ".o_model_field_selector_popover_item", 3);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='xphone_prop_1']"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='xphone_prop_2']"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='xpad_prop_1']"
|
||||
);
|
||||
assert.deepEqual(getDisplayedFieldNames(target), [
|
||||
"P1 (xphone)xphone_prop_1 (boolean)",
|
||||
"P1 (xpad)xpad_prop_1 (date)",
|
||||
"P2 (xphone)xphone_prop_2 (char)",
|
||||
]);
|
||||
|
||||
await click(
|
||||
target,
|
||||
".o_model_field_selector_popover_item[data-name='xphone_prop_2'] .o_model_field_selector_popover_item_name"
|
||||
);
|
||||
assert.verifySteps(["properties.xphone_prop_2"]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").textContent,
|
||||
"PropertiesP2"
|
||||
);
|
||||
assert.containsNone(target, ".o_model_field_selector_warning");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user