diff --git a/addons/mail/static/tests/tours/dynamic_placeholder_tour.js b/addons/mail/static/tests/tours/dynamic_placeholder_tour.js index aefc45c330b..07c5878b164 100644 --- a/addons/mail/static/tests/tours/dynamic_placeholder_tour.js +++ b/addons/mail/static/tests/tours/dynamic_placeholder_tour.js @@ -91,22 +91,22 @@ registry.category("web_tour.tours").add("mail/static/tests/tours/dynamic_placeho }, { content: "Check if the dynamic placeholder popover is opened", - trigger: "div.o_field_selector_popover", + trigger: "div.o_model_field_selector_popover", run: function () {}, }, { content: "Click on the first entry of the dynamic placeholder", - trigger: "div.o_field_selector_popover li:first-child", + trigger: "div.o_model_field_selector_popover li:first-child", }, { content: "Enter a default value", trigger: - 'div.o_field_selector_popover .o_field_selector_default_value_input input[type="text"]', + 'div.o_model_field_selector_popover .o_model_field_selector_default_value_input input[type="text"]', run: "text defValue", }, { content: "Click on the the dynamic placeholder default value", - trigger: "div.o_field_selector_popover li:first-child", + trigger: "div.o_model_field_selector_popover li:first-child", }, { content: "Wait for the popover to disappear", diff --git a/addons/web/static/src/core/domain_selector/domain_selector.js b/addons/web/static/src/core/domain_selector/domain_selector.js index fe0349a4bca..34d493b8d62 100644 --- a/addons/web/static/src/core/domain_selector/domain_selector.js +++ b/addons/web/static/src/core/domain_selector/domain_selector.js @@ -13,7 +13,10 @@ import { } from "@web/core/domain_selector/domain_selector_fields"; import { BranchDomainNode } from "@web/core/domain_selector/domain_selector_nodes"; import { findOperator } from "@web/core/domain_selector/domain_selector_operators"; -import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector"; +import { + loadFieldInfo, + ModelFieldSelector, +} from "@web/core/model_field_selector/model_field_selector"; import { useService } from "@web/core/utils/hooks"; export class DomainSelector extends Component { @@ -113,16 +116,8 @@ export class DomainSelector extends Component { if ("01".includes(field.toString())) { return { type: "integer" }; } - if (typeof field !== "string" || !field) { - return null; - } - const { isInvalid, names, modelsInfo } = await this.fieldService.loadPath(resModel, field); - if (isInvalid) { - return null; - } - const name = names.at(-1); - const { fieldDefs } = modelsInfo.at(-1); - return fieldDefs[name]; + const { fieldDef } = await loadFieldInfo(this.fieldService, resModel, field); + return fieldDef; } createNewLeaf() { @@ -154,13 +149,20 @@ export class DomainSelector extends Component { this.notifyChanges(); } + resetDomain() { + this.notifyChanges(); + } + updateBranchOperator(node, operator) { node.operator = operator; this.notifyChanges(); } - async updateField(node, field) { - const fieldDef = await this.loadFieldDef(this.props.resModel, field); + updateField(node, field, { fieldDef }) { + if (!fieldDef) { + field = ""; + fieldDef = { type: "integer" }; + } node.field = { ...fieldDef, name: field }; node.operator = getOperatorsInfo(fieldDef.type)[0]; node.value = getDefaultFieldValue(fieldDef); diff --git a/addons/web/static/src/core/domain_selector/domain_selector.scss b/addons/web/static/src/core/domain_selector/domain_selector.scss index a86f27d1997..4f72f93961f 100644 --- a/addons/web/static/src/core/domain_selector/domain_selector.scss +++ b/addons/web/static/src/core/domain_selector/domain_selector.scss @@ -98,11 +98,11 @@ border: 1px solid darken($o-brand-lightsecondary, 10%); padding: 2px 4px; - > .o_field_selector { + > .o_model_field_selector { float: left; margin-right: 4px; - > .o_field_selector_value > .o_field_selector_chain_part { + > .o_model_field_selector_value > .o_model_field_selector_chain_part { border: none; font-weight: 700; } @@ -124,11 +124,11 @@ width: auto; background-color: white; - &.o_field_selector { + &.o_model_field_selector { flex: 0 1 40%; width: 40%; - > .o_field_selector_value > .o_field_selector_chain_part { + > .o_model_field_selector_value > .o_model_field_selector_chain_part { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/addons/web/static/src/core/domain_selector/domain_selector.xml b/addons/web/static/src/core/domain_selector/domain_selector.xml index c6770e9e199..f83e34dde21 100644 --- a/addons/web/static/src/core/domain_selector/domain_selector.xml +++ b/addons/web/static/src/core/domain_selector/domain_selector.xml @@ -3,8 +3,8 @@
- + Match all records @@ -27,7 +27,7 @@ This domain is not supported. - + @@ -138,7 +138,7 @@ - + is not set @@ -156,8 +156,7 @@ - - +
-
diff --git a/addons/web/static/src/core/model_field_selector/model_field_hook.js b/addons/web/static/src/core/model_field_selector/model_field_hook.js deleted file mode 100644 index ca31dfd4fb3..00000000000 --- a/addons/web/static/src/core/model_field_selector/model_field_hook.js +++ /dev/null @@ -1,38 +0,0 @@ -/** @odoo-module **/ - -import { useService } from "@web/core/utils/hooks"; -import { zipWith } from "@web/core/utils/arrays"; - -export function useModelField() { - const fieldService = useService("field"); - - const loadModelFields = (resModel) => { - return fieldService.loadFields(resModel); - }; - - const loadChain = async (resModel, path) => { - if ("01".includes(path.toString())) { - return [{ resModel, field: null }]; - } - if (typeof path !== "string" || !path) { - return [{ resModel, field: null }]; - } - const { isInvalid, names, modelsInfo } = await fieldService.loadPath(resModel, path); - if (isInvalid) { - return [{ resModel, field: null }]; - } - const chain = zipWith(names, modelsInfo, (name, { resModel, fieldDefs }) => { - return { resModel, field: fieldDefs[name] }; - }); - const lastField = chain.at(-1)?.field; - if (lastField.relation) { - chain.push({ resModel: lastField.relation, field: null }); - } - return chain; - }; - - return { - loadModelFields, - loadChain, - }; -} diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector.js b/addons/web/static/src/core/model_field_selector/model_field_selector.js index 55e2f9af2d4..88dcfb23d65 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector.js +++ b/addons/web/static/src/core/model_field_selector/model_field_selector.js @@ -1,99 +1,118 @@ /** @odoo-module **/ -import { useModelField } from "./model_field_hook"; -import { usePopover } from "@web/core/popover/popover_hook"; +import { Component, onWillStart, onWillUpdateProps, useState } from "@odoo/owl"; +import { KeepLast } from "@web/core/utils/concurrency"; import { ModelFieldSelectorPopover } from "./model_field_selector_popover"; +import { usePopover } from "@web/core/popover/popover_hook"; +import { useService } from "@web/core/utils/hooks"; -import { Component, onWillStart, onWillUpdateProps, toRaw, useState } from "@odoo/owl"; - -export class ModelFieldSelector extends Component { - setup() { - this.popover = usePopover(this.constructor.components.Popover, { - popoverClass: "o_popover_field_selector", - onClose: () => { - if (this.state.isDirty) { - this.props.update(this.state.fieldName, toRaw(this.state.chain)); - } - }, - }); - this.modelField = useModelField(); - this.state = useState({ - chain: [], - fieldName: this.props.fieldName, - isDirty: false, - }); - - onWillStart(async () => { - this.state.chain = await this.loadChain(this.props.resModel, this.props.fieldName); - }); - onWillUpdateProps(async (nextProps) => { - this.state.chain = await this.loadChain(nextProps.resModel, nextProps.fieldName); - this.state.fieldName = nextProps.fieldName; - this.state.isDirty = false; - }); +export async function loadFieldInfo(fieldService, resModel, path) { + if (typeof path !== "string" || !path) { + return { resModel, fieldDef: null }; } - - get fieldNameChain() { - return this.getFieldNameChain(this.state.fieldName ?? this.props.fieldName); - } - - getFieldNameChain(fieldName) { - return fieldName.length ? fieldName.split(".") : []; - } - - async loadChain(resModel, fieldName) { - if ("01".includes(fieldName)) { - return [{ resModel, field: { string: fieldName } }]; - } - return this.modelField.loadChain(resModel, fieldName); - } - - async update(fieldName, isFieldSelected) { - this.state.fieldName = fieldName; - this.state.isDirty = !isFieldSelected; - this.state.chain = await this.loadChain(this.props.resModel, fieldName); - if (isFieldSelected) { - await this.props.update(fieldName, toRaw(this.state.chain)); - } - } - - onFieldSelectorClick(ev) { - if (this.props.readonly) { - return; - } - this.popover.open(ev.currentTarget, { - chain: this.state.chain, - update: this.update.bind(this), - showSearchInput: this.props.showSearchInput, - isDebugMode: this.props.isDebugMode, - loadChain: this.loadChain.bind(this), - filter: this.props.filter, - followRelations: this.props.followRelations, - }); + const { isInvalid, names, modelsInfo } = await fieldService.loadPath(resModel, path); + if (isInvalid) { + return { resModel, fieldDef: null }; } + const name = names.at(-1); + const modelInfo = modelsInfo.at(-1); + return { resModel: modelInfo.resModel, fieldDef: modelInfo.fieldDefs[name] }; } -Object.assign(ModelFieldSelector, { - template: "web._ModelFieldSelector", - components: { +function makeString(value) { + return String(value ?? "-"); +} + +export class ModelFieldSelector extends Component { + static template = "web._ModelFieldSelector"; + static components = { Popover: ModelFieldSelectorPopover, - }, - props: { - fieldName: String, + }; + static props = { resModel: String, + path: { optional: true }, readonly: { type: Boolean, optional: true }, showSearchInput: { type: Boolean, optional: true }, isDebugMode: { type: Boolean, optional: true }, update: { type: Function, optional: true }, filter: { type: Function, optional: true }, followRelations: { type: Boolean, optional: true }, - }, - defaultProps: { + }; + static defaultProps = { readonly: true, isDebugMode: false, showSearchInput: true, update: () => {}, - filter: () => true, followRelations: true, - }, -}); + }; + + setup() { + this.popover = usePopover(this.constructor.components.Popover, { + popoverClass: "o_popover_field_selector", + onClose: async () => { + if (this.newPath) { + const fieldInfo = await loadFieldInfo( + this.fieldService, + this.props.resModel, + this.newPath + ); + this.props.update(this.newPath, fieldInfo); + } + }, + }); + this.keepLast = new KeepLast(); + this.fieldService = useService("field"); + this.state = useState({ + isInvalid: false, + displayNames: [], + }); + onWillStart(() => this.updatePath(this.props.resModel, this.props.path)); + onWillUpdateProps((nextProps) => this.updatePath(nextProps.resModel, nextProps.path)); + } + + async updatePath(resModel, path, isConcurrent) { + let prom = this.loadPath(resModel, path); + if (isConcurrent) { + prom = this.keepLast.add(prom); + } + const state = await prom; + Object.assign(this.state, state); + } + + async loadPath(resModel, path) { + // the model should be checked maybe + if ([0, 1].includes(path)) { + return { isInvalid: false, displayNames: [makeString(path)] }; + } + if (typeof path !== "string" || !path) { + return { isInvalid: true, displayNames: [makeString()] }; + } + const { isInvalid, modelsInfo, names } = await this.fieldService.loadPath(resModel, path); + const result = { isInvalid: !!isInvalid, displayNames: [] }; + for (let index = 0; index < names.length; index++) { + const name = names[index]; + const fieldDef = modelsInfo[index]?.fieldDefs[name]; + result.displayNames.push(fieldDef?.string || makeString(name)); + } + return result; + } + + openPopover(currentTarget) { + if (this.props.readonly) { + return; + } + this.newPath = null; + this.popover.open(currentTarget, { + resModel: this.props.resModel, + path: this.props.path, + update: (path) => { + this.newPath = path; + this.updatePath(this.props.resModel, path, true); + }, + showSearchInput: this.props.showSearchInput, + isDebugMode: this.props.isDebugMode, + filter: this.props.filter, + followRelations: this.props.followRelations, + }); + } +} diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector.scss b/addons/web/static/src/core/model_field_selector/model_field_selector.scss index b4088d32cf0..84c596382db 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector.scss +++ b/addons/web/static/src/core/model_field_selector/model_field_selector.scss @@ -1,42 +1,22 @@ -.o_field_selector:not(.o_legacy_field_selector) { +.o_model_field_selector { position: relative; - > .o_field_selector_value { - display: flex; - flex-flow: row wrap; - align-items: center; - height: 100%; + &.o_edit_mode { + cursor: pointer; + } + + > .o_model_field_selector_value { min-height: 20px; // needed when there is no value in it and used standalone &:active, &:focus, &:active:focus { outline: none; } - - > .o_field_selector_chain_part { - padding: 0px 1px; + > .o_model_field_selector_chain_part { + cursor: inherit; border: 1px solid darken($o-brand-lightsecondary, 10%); background: $o-brand-lightsecondary; - margin-bottom: 1px; } > i { - align-self: center; - margin: 0 2px; font-size: 10px; } } - > .o_field_selector_controls { - @include o-position-absolute(0, 0, 1px); - display: flex; - align-items: center; - cursor: pointer; - } - - &.o_edit_mode { - > .o_field_selector_controls::after { - @include o-caret-down; - } - - > .o_field_selector_popover { - @include o-position-absolute($top: 100%, $left: 0); - } - } } diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector.xml b/addons/web/static/src/core/model_field_selector/model_field_selector.xml index b12ab63dcaa..10f59b95726 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector.xml +++ b/addons/web/static/src/core/model_field_selector/model_field_selector.xml @@ -2,29 +2,20 @@ -
-
- - - - - - - - +
+
+ + + + + +
- -
- -
-
+
+ +
diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js index 49643a6fc6c..e824d1d9368 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js +++ b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js @@ -1,202 +1,274 @@ /** @odoo-module **/ -import { sortBy } from "../utils/arrays"; -import { useModelField } from "./model_field_hook"; - +import { Component, onWillStart, useEffect, useRef, useState } from "@odoo/owl"; +import { debounce } from "@web/core/utils/timing"; import { fuzzyLookup } from "@web/core/utils/search"; -import { useAutofocus } from "../utils/hooks"; +import { KeepLast } from "@web/core/utils/concurrency"; +import { sortBy } from "@web/core/utils/arrays"; +import { useService } from "@web/core/utils/hooks"; -import { Component, onWillStart } from "@odoo/owl"; - -export class ModelFieldSelectorPopover extends Component { - setup() { - this.chain = Array.from(this.props.chain); - this.modelField = useModelField(); - this.unfilteredFields = {}; - this.fields = {}; - this.fieldKeys = []; - this.currentActiveFieldId = 0; - this.searchValue = ""; - this.defaultValue = ""; - this.isDefaultValueVisible = false; - this.fullFieldName = this.fieldNameChain.join("."); - if (!this.env.isSmall) { - useAutofocus(); - useAutofocus({ refName: "autofocusDefaultValue", selectAll: true }); - } - - onWillStart(async () => { - await this.loadFields(); - }); +class Page { + constructor(resModel, fieldDefs, previousPage = null, selectedName = null) { + this.resModel = resModel; + this.fieldDefs = fieldDefs; + this.previousPage = previousPage; + this.selectedName = selectedName; + this.sortedFieldNames = sortBy(Object.keys(fieldDefs), (key) => fieldDefs[key].string); + this.fieldNames = this.sortedFieldNames; + this.query = ""; + this.focusedFieldName = null; + this.resetFocusedFieldName(); } - get currentActiveField() { - return this.fieldKeys[this.currentActiveFieldId]; - } - - get currentNode() { - return this.chain[this.chain.length - 1]; - } - get currentFieldName() { - const nodes = this.chain.filter((node) => node.field); - return nodes.length ? nodes[nodes.length - 1].field.string : ""; - } - - get fieldNameChain() { - return this.chain.filter((node) => node.field).map((node) => node.field.name); - } - - async loadFields() { - this.unfilteredFields = await this.modelField.loadModelFields(this.currentNode.resModel); - this.fields = { ...this.unfilteredFields }; - this.fieldKeys = this.sortedKeys(this.fields); - for (const key of this.fieldKeys) { - const field = this.fields[key]; - if (!field.searchable || !this.props.filter(field)) { - delete this.fields[key]; + get path() { + const previousPath = this.previousPage?.path || ""; + if (this.selectedName) { + if (previousPath) { + return `${previousPath}.${this.selectedName}`; + } else { + return this.selectedName; } } - this.fieldKeys = this.sortedKeys(this.fields); + return previousPath; } - sortedKeys(obj) { - const keys = Object.keys(obj); - return sortBy(keys, (key) => obj[key].string); + + get selectedField() { + return this.fieldDefs[this.selectedName]; } - async update(isSelected) { - const fieldNameChain = this.fieldNameChain.join("."); - this.fullFieldName = fieldNameChain; - await this.loadFields(); - await this.props.update(fieldNameChain, isSelected); - if (isSelected) { - this.props.close(); + + get title() { + const prefix = this.previousPage?.previousPage ? "... > " : ""; + const title = this.previousPage?.selectedField.string || ""; + return `${prefix}${title}`; + } + + focus(direction) { + if (!this.fieldNames.length) { + return; + } + const index = this.fieldNames.indexOf(this.focusedFieldName); + if (direction === "previous") { + if (index === 0) { + this.focusedFieldName = this.fieldNames[this.fieldNames.length - 1]; + } else { + this.focusedFieldName = this.fieldNames[index - 1]; + } } else { - this.render(); + if (index === this.fieldNames.length - 1) { + this.focusedFieldName = this.fieldNames[0]; + } else { + this.focusedFieldName = this.fieldNames[index + 1]; + } } } - async onInputKeydown(ev) { + resetFocusedFieldName() { + if (this.selectedName && this.fieldNames.includes(this.selectedName)) { + this.focusedFieldName = this.selectedName; + } else { + this.focusedFieldName = this.fieldNames.length ? this.fieldNames[0] : null; + } + } + + searchFields(query = "") { + this.query = query; + this.fieldNames = this.sortedFieldNames; + if (query) { + this.fieldNames = fuzzyLookup( + query, + this.fieldNames, + (key) => this.fieldDefs[key].string + ); + } + this.resetFocusedFieldName(); + } +} + +export class ModelFieldSelectorPopover extends Component { + static template = "web.ModelFieldSelectorPopover"; + static props = { + close: Function, + filter: { type: Function, optional: true }, + followRelations: { type: Boolean, optional: true }, + isDebugMode: { type: Boolean, optional: true }, + path: String, + resModel: String, + showSearchInput: { type: Boolean, optional: true }, + update: Function, + }; + static defaultProps = { + filter: (fieldDef) => fieldDef.searchable, + isDebugMode: false, + followRelations: true, + }; + + setup() { + this.fieldService = useService("field"); + this.state = useState({ page: null }); + + this.keepLast = new KeepLast(); + + this.debouncedSearchFields = debounce(this.searchFields.bind(this), 250); + + onWillStart(async () => { + this.state.page = await this.loadPages(this.props.resModel, this.props.path); + }); + + const rootRef = useRef("root"); + useEffect(() => { + const focusedElement = rootRef.el.querySelector( + ".o_model_field_selector_popover_item.active" + ); + if (focusedElement) { + // current page can be empty (e.g. after a search) + focusedElement.scrollIntoView({ block: "center" }); + } + }); + useEffect( + () => { + if (this.props.showSearchInput) { + const searchInput = rootRef.el.querySelector( + ".o_model_field_selector_popover_search .o_input" + ); + searchInput.focus(); + } + }, + () => [this.state.page] + ); + } + + filter(fieldDefs, path) { + const filteredKeys = Object.keys(fieldDefs).filter((k) => + this.props.filter(fieldDefs[k], path) + ); + return Object.fromEntries(filteredKeys.map((k) => [k, fieldDefs[k]])); + } + + async followRelation(fieldDef) { + const { modelsInfo } = await this.keepLast.add( + this.fieldService.loadPath(this.state.page.resModel, `${fieldDef.name}.*`) + ); + this.state.page.selectedName = fieldDef.name; + const { resModel, fieldDefs } = modelsInfo.at(-1); + this.openPage( + new Page(resModel, this.filter(fieldDefs, this.state.page.path), this.state.page) + ); + } + + goToPreviousPage() { + this.keepLast.add(Promise.resolve()); + this.openPage(this.state.page.previousPage); + } + + async loadNewPath(path) { + const newPage = await this.keepLast.add(this.loadPages(this.props.resModel, path)); + this.openPage(newPage); + } + + async loadPages(resModel, path) { + if (typeof path !== "string" || !path.length) { + const fieldDefs = await this.fieldService.loadFields(resModel); + return new Page(resModel, this.filter(fieldDefs, path)); + } + const { isInvalid, modelsInfo, names } = await this.fieldService.loadPath(resModel, path); + switch (isInvalid) { + case "model": + throw new Error(`Invalid model name: ${resModel}`); + case "path": { + const { resModel, fieldDefs } = modelsInfo[0]; + return new Page(resModel, this.filter(fieldDefs, path), null, path); + } + default: { + let page = null; + for (let index = 0; index < names.length; index++) { + const name = names[index]; + const { resModel, fieldDefs } = modelsInfo[index]; + page = new Page(resModel, this.filter(fieldDefs, path), page, name); + } + return page; + } + } + } + + openPage(page) { + this.state.page = page; + this.state.page.searchFields(); + this.props.update(page.path); + } + + searchFields(query) { + this.state.page.searchFields(query); + } + + selectField(field) { + this.keepLast.add(Promise.resolve()); + this.state.page.selectedName = field.name; + this.props.update(this.state.page.path); + this.props.close(); + } + + onDebugInputKeydown(ev) { switch (ev.key) { - case "ArrowUp": + case "Enter": { ev.preventDefault(); ev.stopPropagation(); - if (this.currentActiveFieldId > 0) { - this.currentActiveFieldId--; - await this.render(); + this.loadNewPath(ev.currentTarget.value); + break; + } + } + } + + // @TODO should rework/improve this and maybe use hotkeys + async onInputKeydown(ev) { + const { page } = this.state; + switch (ev.key) { + case "ArrowUp": { + if (ev.target.selectionStart === 0) { + page.focus("previous"); } break; - case "ArrowDown": - ev.preventDefault(); - ev.stopPropagation(); - if (this.currentActiveFieldId < this.fieldKeys.length - 1) { - this.currentActiveFieldId++; - await this.render(); + } + case "ArrowDown": { + if (ev.target.selectionStart === page.query.length) { + page.focus("next"); } break; - case "ArrowLeft": - ev.preventDefault(); - ev.stopPropagation(); - this.onPreviousBtnClick(); + } + case "ArrowLeft": { + if (ev.target.selectionStart === 0 && page.previousPage) { + this.goToPreviousPage(); + } break; - case "Escape": + } + case "ArrowRight": { + if (ev.target.selectionStart === page.query.length) { + const focusedFieldName = this.state.page.focusedFieldName; + if (focusedFieldName) { + const fieldDef = this.state.page.fieldDefs[focusedFieldName]; + if (fieldDef.relation) { + this.followRelation(fieldDef); + } + } + } + break; + } + case "Enter": { + const focusedFieldName = this.state.page.focusedFieldName; + if (focusedFieldName) { + const fieldDef = this.state.page.fieldDefs[focusedFieldName]; + this.selectField(fieldDef); + } else { + ev.preventDefault(); + ev.stopPropagation(); + } + break; + } + case "Escape": { ev.preventDefault(); ev.stopPropagation(); this.props.close(); break; - case "Enter": - case "ArrowRight": - ev.preventDefault(); - ev.stopPropagation(); - if (this.isDefaultValueVisible) { - this.selectDefaultValue(true); - } else { - const field = { - ...this.fields[this.currentActiveField], - name: this.currentActiveField, - }; - this.onFieldSelected(field); - } - break; - } - } - onSearch(ev) { - this.searchValue = ev.target.value; - let fieldKeys = this.sortedKeys(this.fields); - if (this.searchValue) { - fieldKeys = fuzzyLookup(this.searchValue, fieldKeys, (key) => this.fields[key].string); - } - this.fieldKeys = fieldKeys; - this.render(); - } - onDefaultValue(ev) { - this.defaultValue = ev.target.value; - this.render(); - } - onPreviousBtnClick() { - this.searchValue = ""; - if (this.currentNode.field === null) { - this.chain.pop(); - } - this.currentNode.field = null; - this.update(); - } - onFieldSelected(field) { - this.searchValue = ""; - this.currentActiveFieldId = 0; - this.currentNode.field = field; - if (field.relation && this.props.followRelations) { - this.chain.push({ - resModel: field.relation, - field: null, - }); - this.update(); - } else if (this.props.needDefaultValue) { - this.isDefaultValueVisible = true; - this.update(); - } else { - this.update(true); - this.props.validate(this.fieldNameChain, this.defaultValue); - } - } - selectDefaultValue(acceptDefaultValue) { - if (!acceptDefaultValue) { - this.defaultValue = ""; - } - this.update(true); - this.props.validate(this.fieldNameChain, this.defaultValue); - } - async onFieldNameChange(ev) { - this.fullFieldName = ev.target.value.replace(/\s+/g, ""); - const { resModel } = this.props.chain[0]; - try { - this.chain = await this.props.loadChain(resModel, this.fullFieldName); - this.update(); - } catch { - // WOWL TODO: rethrow error when not the expected type - this.chain = [{ resModel, field: null }]; - await this.props.update(""); - this.render(); + } } } } - -ModelFieldSelectorPopover.defaultProps = { - validate: () => {}, - needDefaultValue: false, - isDebugMode: false, - followRelations: true, -}; - -ModelFieldSelectorPopover.props = { - chain: Array, - update: Function, - showSearchInput: Boolean, - isDebugMode: { type: Boolean, optional: true }, - loadChain: Function, - filter: Function, - close: Function, - followRelations: { type: Boolean, optional: true }, - needDefaultValue: { type: Boolean, optional: true }, - validate: { type: Function, optional: true }, -}; - -ModelFieldSelectorPopover.template = "web.ModelFieldSelectorPopover"; diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.scss b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.scss index c9c2e1e6b83..5f74b038a04 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.scss +++ b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.scss @@ -2,7 +2,7 @@ border-bottom-color: map-get($theme-colors, 'primary'); } -.o_field_selector_popover:not(.o_legacy_field_selector_popover) { +.o_model_field_selector_popover { width: 265px; background: white; --o-input-background-color: white; @@ -11,19 +11,19 @@ outline: none; } - .o_field_selector_popover_header { + .o_model_field_selector_popover_header { color: white; background: map-get($theme-colors, 'primary'); font-weight: bold; padding: 5px 0 5px 0.4em; - .o_field_selector_title { + .o_model_field_selector_popover_title { width: 100%; @include o-text-overflow; padding: 0px 35px; text-align: center; } - .o_field_selector_search { + .o_model_field_selector_popover_search { padding-right: 0.4rem; > .o_input { font-size: 13px; @@ -32,14 +32,14 @@ line-height: normal; } } - .o_field_selector_popover_option { + .o_model_field_selector_popover_option { @include o-position-absolute($top: 0); padding: 8px; - &.o_field_selector_prev_page { + &.o_model_field_selector_popover_prev_page { left: 0; } - &.o_field_selector_close { + &.o_model_field_selector_popover_close { right: 0; } &:hover { @@ -47,18 +47,16 @@ } } } - .o_field_selector_popover_body { - .o_field_selector_page { + .o_model_field_selector_popover_body { + .o_model_field_selector_popover_page { position: relative; height: 320px; overflow: auto; margin: 0; padding: 0; - > .o_field_selector_item { + > .o_model_field_selector_popover_item { list-style: none; - position: relative; - padding: 5px 0 5px 0.4em; cursor: pointer; font-family: Arial; font-size: 13px; @@ -67,19 +65,16 @@ &.active { background: #f5f5f5; } - .o_field_selector_item_title { + .o_model_field_selector_popover_item_title { font-size: 12px; } - .o_field_selector_relation_icon { - @include o-position-absolute($top: 0, $right: 0, $bottom: 0); - display: flex; - align-items: center; - padding: 10px; + .o_model_field_selector_popover_item_relation { + border-left: 1px solid #eee; } } } } - .o_field_selector_popover_footer { + .o_model_field_selector_popover_footer { background: map-get($theme-colors, 'primary'); padding: 5px 0.4em; diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.xml b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.xml index b5bb9027671..d52327d0e70 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.xml +++ b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.xml @@ -2,89 +2,56 @@ -
-
- - +
+ + - -
Default value
-
- -
- - + +
+ - - -
-
    - - - -
  • - -
    ()
    - - - -
  • -
    +
    +
      + + +
    • + + + + +
    -
      -
    • - -
      Default text is used when no values are set
      -
      - -
      As a default text when no value are set
      -
      -
    • -
    - diff --git a/addons/web/static/src/views/fields/dynamic_placeholder_hook.js b/addons/web/static/src/views/fields/dynamic_placeholder_hook.js index 7a4032baca8..fbdccfcf35e 100644 --- a/addons/web/static/src/views/fields/dynamic_placeholder_hook.js +++ b/addons/web/static/src/views/fields/dynamic_placeholder_hook.js @@ -1,10 +1,9 @@ /** @odoo-module **/ import { usePopover } from "@web/core/popover/popover_hook"; -import { useModelField } from "@web/core/model_field_selector/model_field_hook"; import { useService } from "@web/core/utils/hooks"; -import { ModelFieldSelectorPopover } from "@web/core/model_field_selector/model_field_selector_popover"; import { useComponent } from "@odoo/owl"; +import { DynamicPlaceholderPopover } from "./dynamic_placeholder_popover"; export function useDynamicPlaceholder(elementRef) { const TRIGGER_KEY = "#"; @@ -12,25 +11,23 @@ export function useDynamicPlaceholder(elementRef) { const triggerKeyReplaceRegex = new RegExp(`${TRIGGER_KEY}$`); let closeCallback; let positionCallback; - const popover = usePopover(ModelFieldSelectorPopover, { + const popover = usePopover(DynamicPlaceholderPopover, { onclose: () => closeCallback?.(), onPositioned: () => positionCallback?.(), }); - const modelField = useModelField(); const notification = useService("notification"); let model = null; - let dynamicPlaceholderChain = []; - const onDynamicPlaceholderValidate = function (chain, defaultValue) { + const onDynamicPlaceholderValidate = function (path, defaultValue) { const element = elementRef?.el; if (!element) { return; } let rangeIndex = parseInt(element.getAttribute("data-oe-dynamic-placeholder-range-index")); - // When the user cancel/close the popover, the chain is empty. - if (chain) { - let dynamicPlaceholder = "{{object." + chain.join("."); + // When the user cancel/close the popover, the path is empty. + if (path) { + let dynamicPlaceholder = "{{object." + path; dynamicPlaceholder += defaultValue && defaultValue !== "" ? ` or '''${defaultValue}'''}}` : "}}"; @@ -73,19 +70,11 @@ export function useDynamicPlaceholder(elementRef) { { type: "danger" } ); } - - dynamicPlaceholderChain = await modelField.loadChain(model, ""); closeCallback = opts.closeCallback; positionCallback = opts.positionCallback; popover.open(elementRef?.el, { - chain: dynamicPlaceholderChain, - update: (chain) => (dynamicPlaceholderChain = chain), + resModel: model, validate: opts.validateCallback, - showSearchInput: true, - isDebugMode: true, - needDefaultValue: true, - loadChain: modelField.loadChain, - filter: (model) => !["one2many", "boolean", "many2many"].includes(model.type), }); } async function onKeydown(ev) { diff --git a/addons/web/static/src/views/fields/dynamic_placeholder_popover.js b/addons/web/static/src/views/fields/dynamic_placeholder_popover.js new file mode 100644 index 00000000000..d35c16631c6 --- /dev/null +++ b/addons/web/static/src/views/fields/dynamic_placeholder_popover.js @@ -0,0 +1,53 @@ +/** @odoo-module **/ + +import { useAutofocus } from "@web/core/utils/hooks"; +import { ModelFieldSelectorPopover } from "@web/core/model_field_selector/model_field_selector_popover"; +import { Component, useState } from "@odoo/owl"; + +export class DynamicPlaceholderPopover extends Component { + static template = "web.DynamicPlaceholderPopover"; + static components = { + ModelFieldSelectorPopover, + }; + static props = ["resModel", "validate", "close"]; + + setup() { + useAutofocus(); + this.state = useState({ + path: "", + isPathSelected: false, + defaultValue: "", + }); + } + + filter(fieldDef) { + return !["one2many", "boolean", "many2many"].includes(fieldDef.type) && fieldDef.searchable; + } + closeFieldSelector() { + this.state.isPathSelected = true; + } + setPath(path) { + this.state.path = path; + } + setDefaultValue(value) { + this.state.defaultValue = value; + } + validate() { + this.props.close(); + this.props.validate(this.state.path, this.state.defaultValue); + } + + // @TODO should rework this to use hotkeys + async onInputKeydown(ev) { + switch (ev.key) { + case "Enter": { + this.validate(); + break; + } + case "Escape": { + this.props.close(); + break; + } + } + } +} diff --git a/addons/web/static/src/views/fields/dynamic_placeholder_popover.xml b/addons/web/static/src/views/fields/dynamic_placeholder_popover.xml new file mode 100644 index 00000000000..28c83a978b0 --- /dev/null +++ b/addons/web/static/src/views/fields/dynamic_placeholder_popover.xml @@ -0,0 +1,55 @@ + + + + + +
    +
    +
    Default value
    + +
    + +
    +
    +
    +
      +
    • + +
      Default text is used when no values are set
      +
      + +
      As a default text when no value are set
      +
      +
    • +
    +
    + +
    +
    + + + +
    + +
    diff --git a/addons/web/static/src/views/fields/properties/properties_field.js b/addons/web/static/src/views/fields/properties/properties_field.js index 5f94f3e9d02..06de2751aed 100644 --- a/addons/web/static/src/views/fields/properties/properties_field.js +++ b/addons/web/static/src/views/fields/properties/properties_field.js @@ -143,7 +143,7 @@ export class PropertiesField extends Component { return false; } - if (target.closest(".o_field_selector_popover")) { + if (target.closest(".o_model_field_selector_popover")) { // domain selector return false; } diff --git a/addons/web/static/tests/core/domain_selector_tests.js b/addons/web/static/tests/core/domain_selector_tests.js index 949401f2a59..03f028bff4c 100644 --- a/addons/web/static/tests/core/domain_selector_tests.js +++ b/addons/web/static/tests/core/domain_selector_tests.js @@ -14,6 +14,7 @@ import { registerCleanup } from "../helpers/cleanup"; import { makeTestEnv } from "../helpers/mock_env"; import { makeFakeLocalizationService } from "../helpers/mock_services"; import { click, editInput, editSelect, getFixture, mount, triggerEvent } from "../helpers/utils"; +import { openModelFieldSelectorPopover } from "./model_field_selector_tests"; let serverData; let target; @@ -108,13 +109,13 @@ QUnit.module("Components", (hooks) => { // Clicking on the button should add a visible field selector in the // widget so that the user can change the field chain await click(target, ".o_domain_add_first_node_button"); - assert.containsOnce(target, ".o_field_selector", "there should be a field selector"); + assert.containsOnce(target, ".o_model_field_selector"); // Focusing the field selector input should open a field selector popover - await click(target, ".o_field_selector"); + await click(target, ".o_model_field_selector"); assert.containsOnce( document.body, - ".o_field_selector_popover", + ".o_model_field_selector_popover", "field selector popover should be visible" ); @@ -122,14 +123,20 @@ QUnit.module("Components", (hooks) => { // fields. "Bar" should be among them. "Bar" result li will display the // name of the field and some debug info. assert.strictEqual( - document.body.querySelector(".o_field_selector_popover li").textContent, + document.body.querySelector( + ".o_model_field_selector_popover .o_model_field_selector_popover_item_name" + ).textContent, "Barbar (boolean)", "field selector popover should contain the 'Bar' field" ); // Clicking the "Bar" field should change the internal domain and this // should be displayed in the debug textarea - await click(document.body.querySelector(".o_field_selector_popover li")); + await click( + document.body.querySelector( + ".o_model_field_selector_popover .o_model_field_selector_popover_item_name" + ) + ); assert.containsOnce(target, "textarea.o_domain_debug_input"); assert.strictEqual( target.querySelector(".o_domain_debug_input").value, @@ -429,7 +436,10 @@ QUnit.module("Components", (hooks) => { "should still have a single domain node" ); - assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "1"); + assert.strictEqual( + target.querySelector(".o_model_field_selector_chain_part").innerText, + "1" + ); assert.strictEqual( target.querySelector(".o_domain_leaf_operator_select").value, "equal" @@ -451,7 +461,10 @@ QUnit.module("Components", (hooks) => { "should still have a single domain node" ); - assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "0"); + assert.strictEqual( + target.querySelector(".o_model_field_selector_chain_part").innerText, + "0" + ); assert.strictEqual( target.querySelector(".o_domain_leaf_operator_select").value, "equal" @@ -545,7 +558,10 @@ QUnit.module("Components", (hooks) => { // Create the domain selector and its mock environment await mountComponent(Parent); - assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "State"); + assert.strictEqual( + target.querySelector(".o_model_field_selector_chain_part").innerText, + "State" + ); assert.strictEqual( target.querySelector(".o_domain_leaf_operator_select").value, "not_equal" @@ -553,7 +569,10 @@ QUnit.module("Components", (hooks) => { await editSelect(target, ".o_domain_leaf_operator_select", "equal"); - assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "State"); + assert.strictEqual( + target.querySelector(".o_model_field_selector_chain_part").innerText, + "State" + ); assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "equal"); // option "=" assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, `"abc"`); }); @@ -637,7 +656,7 @@ QUnit.module("Components", (hooks) => { isDebugMode: false, }, }); - assert.containsNone(target, ".o_domain_add_first_node_button"); + assert.containsNone(target, ".o_reset_domain_button"); assert.containsNone(target, ".o_domain_debug_input"); }); @@ -650,7 +669,7 @@ QUnit.module("Components", (hooks) => { isDebugMode: true, }, }); - assert.containsNone(target, ".o_domain_add_first_node_button"); + assert.containsNone(target, ".o_reset_domain_button"); assert.containsOnce(target, ".o_domain_debug_input"); assert.ok(target.querySelector(".o_domain_debug_input").hasAttribute("readonly")); }); @@ -664,7 +683,7 @@ QUnit.module("Components", (hooks) => { isDebugMode: false, }, }); - assert.containsOnce(target, ".o_domain_add_first_node_button"); + assert.containsOnce(target, ".o_reset_domain_button"); assert.containsNone(target, ".o_domain_debug_input"); }); @@ -677,8 +696,79 @@ QUnit.module("Components", (hooks) => { isDebugMode: true, }, }); - assert.containsOnce(target, ".o_domain_add_first_node_button"); + assert.containsOnce(target, ".o_reset_domain_button"); assert.containsOnce(target, ".o_domain_debug_input"); assert.notOk(target.querySelector(".o_domain_debug_input").hasAttribute("readonly")); }); + + QUnit.test("reset domain", async (assert) => { + class Parent extends Component { + setup() { + this.value = `[`; + } + onUpdate(domain) { + assert.step(domain); + this.value = domain; + this.render(); + } + } + Parent.components = { DomainSelector }; + Parent.template = xml` + + `; + await mountComponent(Parent); + assert.strictEqual( + target.querySelector(".o_domain_selector").innerText.toLowerCase(), + "this domain is not supported. reset domain" + ); + assert.containsOnce(target, ".o_reset_domain_button"); + assert.containsNone(target, ".o_domain_add_first_node_button"); + + await click(target, ".o_reset_domain_button"); + assert.strictEqual( + target.querySelector(".o_domain_selector").innerText.toLowerCase(), + "match all records add filter" + ); + assert.containsNone(target, ".o_reset_domain_button"); + assert.containsOnce(target, ".o_domain_add_first_node_button"); + assert.verifySteps(["[]"]); + }); + + QUnit.test("incorrect path in debug input in model field selector popover", async (assert) => { + class Parent extends Component { + setup() { + this.value = `[("id", "=", 1)]`; + } + onUpdate(domain) { + assert.step(domain); + this.value = domain; + this.render(); + } + } + Parent.components = { DomainSelector }; + Parent.template = xml` + + `; + await mountComponent(Parent); + await openModelFieldSelectorPopover(target); + await editInput(target, ".o_model_field_selector_debug", "a"); + await click(target, ".o_model_field_selector_popover_close"); + assert.verifySteps([`[("", "=", 1)]`]); + assert.strictEqual( + target.querySelector(".o_model_field_selector_chain_part").innerText, + "-" + ); + assert.containsOnce(target, ".o_model_field_selector_warning"); + }); }); diff --git a/addons/web/static/tests/core/model_field_selector_tests.js b/addons/web/static/tests/core/model_field_selector_tests.js index 2c0739848d2..2a0d1b3cd8c 100644 --- a/addons/web/static/tests/core/model_field_selector_tests.js +++ b/addons/web/static/tests/core/model_field_selector_tests.js @@ -1,17 +1,26 @@ /** @odoo-module **/ -import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector"; +import { browser } from "@web/core/browser/browser"; +import { + click, + editInput, + getFixture, + getNodesTextContent, + mount, + nextTick, + patchWithCleanup, + triggerEvent, +} from "../helpers/utils"; +import { Component, useState, xml } from "@odoo/owl"; +import { fieldService } from "@web/core/field_service"; import { MainComponentsContainer } from "@web/core/main_components_container"; +import { makeFakeLocalizationService } from "../helpers/mock_services"; +import { makeTestEnv } from "../helpers/mock_env"; +import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector"; import { ormService } from "@web/core/orm_service"; import { popoverService } from "@web/core/popover/popover_service"; import { registry } from "@web/core/registry"; import { uiService } from "@web/core/ui/ui_service"; -import { makeTestEnv } from "../helpers/mock_env"; -import { click, getFixture, triggerEvent, mount, editInput } from "../helpers/utils"; -import { makeFakeLocalizationService } from "../helpers/mock_services"; - -import { Component, xml } from "@odoo/owl"; -import { fieldService } from "@web/core/field_service"; let target; let serverData; @@ -22,6 +31,38 @@ async function mountComponent(Component, params = {}) { return mount(Component, target, { env, props: params.props || {} }); } +export async function openModelFieldSelectorPopover(target) { + await click(target, ".o_model_field_selector"); +} + +function getDisplayedFieldNames(target) { + return getNodesTextContent( + target.querySelectorAll(".o_model_field_selector_popover_item_name") + ); +} + +function getModelFieldSelectorValues(target) { + return getNodesTextContent(target.querySelectorAll("span.o_model_field_selector_chain_part")); +} + +function getTitle(target) { + return target.querySelector( + ".o_model_field_selector_popover .o_model_field_selector_popover_title" + ).innerText; +} + +async function clickPrev(target) { + await click(target, ".o_model_field_selector_popover_prev_page"); +} + +async function followRelation(target, index = 0) { + await click(target.querySelectorAll(".o_model_field_selector_popover_item_relation")[index]); +} + +function getFocusedFieldName(target) { + return target.querySelector(".o_model_field_selector_popover_item.active").innerText; +} + QUnit.module("Components", (hooks) => { hooks.beforeEach(async () => { serverData = { @@ -69,18 +110,17 @@ QUnit.module("Components", (hooks) => { QUnit.test("creating a field chain from scratch", async (assert) => { function getValueFromDOM(el) { - return [...el.querySelectorAll(".o_field_selector_chain_part")] + return [...el.querySelectorAll(".o_model_field_selector_chain_part")] .map((part) => part.textContent.trim()) .join(" -> "); } - class Parent extends Component { setup() { - this.fieldName = ""; + this.path = ""; } - onUpdate(value) { - assert.step(`update: ${value}`); - this.fieldName = value; + onUpdate(path) { + assert.step(`update: ${path}`); + this.path = path; this.render(); } } @@ -89,309 +129,195 @@ QUnit.module("Components", (hooks) => { `; - // Create the field selector and its mock environment const fieldSelector = await mountComponent(Parent); - // Focusing the field selector input should open a field selector popover - await click(target, ".o_field_selector"); + await openModelFieldSelectorPopover(target); assert.strictEqual( target.querySelector("input.o_input[placeholder='Search...']"), - document.activeElement, - "the field selector input should be focused" - ); - assert.containsOnce( - target, - ".o_field_selector_popover", - "field selector popover should be visible" + document.activeElement ); + assert.containsOnce(target, ".o_model_field_selector_popover"); // The field selector popover should contain the list of "partner" // fields. "Bar" should be among them. assert.strictEqual( - target.querySelector(".o_field_selector_popover .o_field_selector_item").textContent, - "Bar", - "field selector popover should contain the 'Bar' field" + target.querySelector(".o_model_field_selector_popover_item_name").textContent, + "Bar" ); // Clicking the "Bar" field should close the popover and set the field // chain to "bar" as it is a basic field - await click(target.querySelector(".o_field_selector_popover .o_field_selector_item")); - assert.containsNone( - target, - ".o_field_selector_popover", - "field selector popover should be closed now" - ); - assert.strictEqual( - getValueFromDOM(target), - "Bar", - "field selector value should be displayed with a 'Bar' tag" - ); - assert.strictEqual( - fieldSelector.fieldName, - "bar", - "the selected field should be correctly set" - ); + await click(target.querySelector(".o_model_field_selector_popover_item_name")); + assert.containsNone(target, ".o_model_field_selector_popover"); + assert.strictEqual(getValueFromDOM(target), "Bar"); + assert.strictEqual(fieldSelector.path, "bar"); assert.verifySteps(["update: bar"]); - // Focusing the input again should open the same popover - await click(target, ".o_field_selector"); - assert.containsOnce( - target, - ".o_field_selector_popover", - "field selector popover should be visible" - ); - + await openModelFieldSelectorPopover(target); + assert.containsOnce(target, ".o_model_field_selector_popover"); // The field selector popover should contain the list of "partner" // fields. "Product" should be among them. assert.containsOnce( target, - ".o_field_selector_popover .o_field_selector_relation_icon", + ".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon", "field selector popover should contain the 'Product' field" ); // Clicking on the "Product" field should update the popover to show // the product fields (so only "Product Name" should be there) await click( - target.querySelector(".o_field_selector_popover .o_field_selector_relation_icon") - ); - assert.containsOnce( - target, - ".o_field_selector_popover .o_field_selector_item", - "there should be only one field proposition for 'product' model" + target.querySelector( + ".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon" + ) ); + assert.containsOnce(target, ".o_model_field_selector_popover_item_name"); assert.strictEqual( - target.querySelector(".o_field_selector_popover .o_field_selector_item").textContent, + target.querySelector(".o_model_field_selector_popover_item_name").textContent, "Product Name", "the name of the only suggestion should be 'Product Name'" ); - // Clicking on "Product Name" should close the popover and set the chain - // to "product_id.name" - await click(target.querySelector(".o_field_selector_popover .o_field_selector_item")); - assert.containsNone( - target, - ".o_field_selector_popover", - "field selector popover should be closed now" - ); - assert.strictEqual( - getValueFromDOM(target), - "Product -> Product Name", - "field selector value should be displayed with two tags: 'Product' and 'Product Name'" - ); + await click(target.querySelector(".o_model_field_selector_popover_item_name")); + assert.containsNone(target, ".o_model_field_selector_popover"); + assert.strictEqual(getValueFromDOM(target), "Product -> Product Name"); assert.verifySteps(["update: product_id.name"]); // Remove the current selection and recreate it again - await click(target, ".o_field_selector"); - await click(target, ".o_field_selector_prev_page"); - await click(target, ".o_field_selector_prev_page"); - await click(target, ".o_field_selector_close"); - assert.verifySteps(["update: "]); + await openModelFieldSelectorPopover(target); + await click(target, ".o_model_field_selector_popover_prev_page"); + await click(target, ".o_model_field_selector_popover_close"); + assert.verifySteps(["update: product_id"]); - await click(target, ".o_field_selector"); + await openModelFieldSelectorPopover(target); assert.containsOnce( target, - ".o_field_selector_popover .o_field_selector_relation_icon", - "field selector popover should contain the 'Product' field" + ".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon" ); await click( - target.querySelector(".o_field_selector_popover .o_field_selector_relation_icon") - ); - await click(target.querySelector(".o_field_selector_popover .o_field_selector_item")); - assert.containsNone( - target, - ".o_field_selector_popover", - "field selector popover should be closed now" - ); - assert.strictEqual( - getValueFromDOM(target), - "Product -> Product Name", - "field selector value should be displayed with two tags: 'Product' and 'Product Name'" + target.querySelector( + ".o_model_field_selector_popover .o_model_field_selector_popover_relation_icon" + ) ); + await click(target.querySelector(".o_model_field_selector_popover_item_name")); + assert.containsNone(target, ".o_model_field_selector_popover"); + assert.strictEqual(getValueFromDOM(target), "Product -> Product Name"); assert.verifySteps(["update: product_id.name"]); }); QUnit.test("default field chain should set the page data correctly", async (assert) => { - assert.expect(3); - - // Create the field selector and its mock environment - // passing 'product_id' as a prefilled field-chain await mountComponent(ModelFieldSelector, { props: { readonly: false, - fieldName: "product_id", + path: "product_id", resModel: "partner", isDebugMode: false, }, }); - - // Focusing the field selector input should open a field selector popover - await click(target, ".o_field_selector"); - assert.containsOnce( - target, - ".o_field_selector_popover", - "field selector popover should be visible" - ); - - // The field selector popover should contain the list of "product" - // fields. "Product Name" should be among them. - assert.containsOnce( - target, - ".o_field_selector_popover .o_field_selector_item", - "there should be only one field proposition for 'product' model" - ); - assert.strictEqual( - target.querySelector(".o_field_selector_popover .o_field_selector_item").textContent, - "Product Name", - "the name of the only suggestion should be 'Product Name'" + await openModelFieldSelectorPopover(target); + assert.containsOnce(target, ".o_model_field_selector_popover"); + assert.deepEqual(getDisplayedFieldNames(target), ["Bar", "Foo", "Product"]); + assert.hasClass( + target.querySelectorAll(".o_model_field_selector_popover_item:nth-child(3)"), + "active" ); }); QUnit.test("use the filter option", async (assert) => { - assert.expect(2); - - // Create the field selector and its mock environment await mountComponent(ModelFieldSelector, { props: { readonly: false, - fieldName: "", + path: "", resModel: "partner", - filter: (field) => field.type === "many2one", + filter: (field) => field.type === "many2one" && field.searchable, }, }); - - await click(target, ".o_field_selector"); - assert.containsOnce( - target, - ".o_field_selector_popover .o_field_selector_item", - "there should only be one element" - ); - assert.strictEqual( - target.querySelector(".o_field_selector_popover .o_field_selector_page").textContent, - "Product", - "the available field should be the many2one" - ); + await openModelFieldSelectorPopover(target); + assert.deepEqual(getDisplayedFieldNames(target), ["Product"]); }); QUnit.test("default `showSearchInput` option", async (assert) => { - assert.expect(6); + patchWithCleanup(browser, { setTimeout: (fn) => fn() }); - // Create the field selector and its mock environment await mountComponent(ModelFieldSelector, { props: { readonly: false, - fieldName: "", + path: "", resModel: "partner", }, }); - - await click(target, ".o_field_selector"); + await openModelFieldSelectorPopover(target); assert.containsOnce( target, - ".o_field_selector_popover .o_field_selector_search", - "there should be a search input" + ".o_model_field_selector_popover .o_model_field_selector_popover_search" ); + assert.deepEqual(getDisplayedFieldNames(target), ["Bar", "Foo", "Product"]); - // without search - assert.containsN( + // search 'xx' + await editInput( target, - ".o_field_selector_popover .o_field_selector_item", - 3, - "there should be three available fields" - ); - assert.strictEqual( - target.querySelector(".o_field_selector_popover .o_field_selector_page").textContent, - "BarFooProduct", - "the available field should be correct" + ".o_model_field_selector_popover .o_model_field_selector_popover_search input", + "xx" ); + assert.deepEqual(getDisplayedFieldNames(target), []); - const input = target.querySelector( - ".o_field_selector_popover .o_field_selector_search input" - ); - input.value = "xx"; - await triggerEvent(input, null, "input"); - assert.containsNone( + // search 'Pro' + await editInput( target, - ".o_field_selector_popover .o_field_selector_item", - "there shouldn't be any element" - ); - - input.value = "Pro"; - await triggerEvent(input, null, "input"); - assert.containsOnce( - target, - ".o_field_selector_popover .o_field_selector_item", - "there should only be one element" - ); - assert.strictEqual( - target.querySelector(".o_field_selector_popover .o_field_selector_page").textContent, - "Product", - "the available field should be the Product" + ".o_model_field_selector_popover .o_model_field_selector_popover_search input", + "Pro" ); + assert.deepEqual(getDisplayedFieldNames(target), ["Product"]); }); QUnit.test("false `showSearchInput` option", async (assert) => { - assert.expect(1); - - // Create the field selector and its mock environment await mountComponent(ModelFieldSelector, { props: { readonly: false, showSearchInput: false, - fieldName: "", + path: "", resModel: "partner", }, }); - - await click(target, ".o_field_selector"); + await openModelFieldSelectorPopover(target); assert.containsNone( target, - ".o_field_selector_popover .o_field_selector_search", - "there should be no search input" + ".o_model_field_selector_popover .o_model_field_selector_popover_search" ); }); QUnit.test("create a field chain with value 1 i.e. TRUE_LEAF", async (assert) => { - assert.expect(1); - - //create the field selector with domain value ["1"] await mountComponent(ModelFieldSelector, { props: { readonly: false, showSearchInput: false, - fieldName: "1", + path: 1, resModel: "partner", }, }); - assert.strictEqual( - target.querySelector(".o_field_selector_chain_part").textContent.trim(), - "1", - "field name value should be 1." + target.querySelector(".o_model_field_selector_chain_part").textContent.trim(), + "1" ); }); QUnit.test("create a field chain with value 0 i.e. FALSE_LEAF", async (assert) => { - assert.expect(1); - - //create the field selector with domain value ["0"] await mountComponent(ModelFieldSelector, { props: { readonly: false, showSearchInput: false, - fieldName: "0", + path: 0, resModel: "partner", }, }); - assert.strictEqual( - target.querySelector(".o_field_selector_chain_part").textContent.trim(), + target.querySelector(".o_model_field_selector_chain_part").textContent.trim(), "0", "field name value should be 0." ); @@ -406,18 +332,17 @@ QUnit.module("Components", (hooks) => { }; await mountComponent(ModelFieldSelector, { - mockRPC(route, { method }) { + mockRPC(_, { method }) { if (method === "fields_get") { assert.step("fields_get"); } }, props: { readonly: false, - fieldName: "partner_id.partner_id.partner_id.foo", + path: "partner_id.partner_id.partner_id.foo", resModel: "partner", }, }); - assert.verifySteps(["fields_get"]); }); @@ -431,10 +356,10 @@ QUnit.module("Components", (hooks) => { class Parent extends Component { setup() { - this.fieldName = "partner_id.foo"; + this.path = "partner_id.foo"; } - onUpdate(value) { - this.fieldName = value; + onUpdate(path) { + this.path = path; this.render(); } } @@ -443,65 +368,83 @@ QUnit.module("Components", (hooks) => { `; await mountComponent(Parent); + assert.deepEqual(getModelFieldSelectorValues(target), ["Partner", "Foo"]); + assert.containsNone(target, ".o_model_field_selector i.o_model_field_selector_warning"); - assert.deepEqual( - [...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText), - ["Partner", "Foo"] + await openModelFieldSelectorPopover(target); + await click(target, ".o_model_field_selector_popover_prev_page"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Partner"]); + assert.containsNone(target, ".o_model_field_selector i.o_model_field_selector_warning"); + + await click( + target, + ".o_model_field_selector_popover_item:nth-child(1) .o_model_field_selector_popover_item_name" ); - assert.containsNone(target, ".o_field_selector i.o_field_selector_warning"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Bar"]); + assert.containsNone(target, ".o_model_field_selector_popover"); + }); - await click(target, ".o_field_selector"); - await click(target, ".o_field_selector_prev_page"); - - assert.deepEqual( - [...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText), - ["Partner"] - ); - assert.containsNone(target, ".o_field_selector i.o_field_selector_warning"); - - await click(target, ".o_field_selector_prev_page"); - - assert.deepEqual( - [...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText), - [""] - ); - assert.containsOnce(target, ".o_field_selector i.o_field_selector_warning"); - - await click(target, ".o_field_selector_popover .o_field_selector_item:nth-child(1)"); - - assert.deepEqual( - [...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText), - ["Bar"] + QUnit.test("select a relational field does not follow relation", async (assert) => { + await mountComponent(ModelFieldSelector, { + props: { + readonly: false, + path: "", + resModel: "partner", + update(path) { + assert.step(path); + }, + }, + }); + await openModelFieldSelectorPopover(target); + assert.containsOnce( + target, + ".o_model_field_selector_popover_item:last-child .o_model_field_selector_popover_relation_icon" ); - assert.containsNone(target, ".o_field_selector_popover"); + await click( + target, + ".o_model_field_selector_popover_item:last-child .o_model_field_selector_popover_item_name" + ); + assert.verifySteps(["product_id"]); + assert.containsNone(target, ".o_popover"); + + await openModelFieldSelectorPopover(target); + assert.deepEqual(getDisplayedFieldNames(target), ["Bar", "Foo", "Product"]); + assert.containsOnce(target, ".o_model_field_selector_popover_relation_icon"); + + await click(target, ".o_model_field_selector_popover_relation_icon"); + assert.deepEqual(getDisplayedFieldNames(target), ["Product Name"]); + assert.containsOnce(target, ".o_popover"); + + await click(target, ".o_model_field_selector_popover_item_name"); + assert.verifySteps(["product_id.name"]); + assert.containsNone(target, ".o_popover"); }); QUnit.test("can follow relations", async (assert) => { await mountComponent(ModelFieldSelector, { props: { readonly: false, - fieldName: "", + path: "", resModel: "partner", followRelations: true, // default - update(value) { - assert.strictEqual(value, "product_id"); + update(path) { + assert.strictEqual(path, "product_id"); }, }, }); + await openModelFieldSelectorPopover(target); + assert.deepEqual(getDisplayedFieldNames(target), ["Bar", "Foo", "Product"]); + assert.containsOnce(target, ".o_model_field_selector_popover_relation_icon"); - await click(target, ".o_field_selector"); - assert.containsOnce( - target, - ".o_field_selector_item:last-child .o_field_selector_relation_icon" - ); - await click(target, ".o_field_selector_item:last-child .o_field_selector_relation_icon"); + await click(target, ".o_model_field_selector_popover_relation_icon"); + assert.deepEqual(getDisplayedFieldNames(target), ["Product Name"]); assert.containsOnce(target, ".o_popover"); }); @@ -509,19 +452,24 @@ QUnit.module("Components", (hooks) => { await mountComponent(ModelFieldSelector, { props: { readonly: false, - fieldName: "", + path: "", resModel: "partner", followRelations: false, - update(value) { - assert.strictEqual(value, "product_id"); + update(path) { + assert.strictEqual(path, "product_id"); }, }, }); + await openModelFieldSelectorPopover(target); + assert.deepEqual(getDisplayedFieldNames(target), ["Bar", "Foo", "Product"]); + assert.containsNone(target, ".o_model_field_selector_popover_relation_icon"); - await click(target, ".o_field_selector"); - assert.containsNone(target, ".o_field_selector_relation_icon"); - await click(target, ".o_field_selector_item:last-child"); + await click( + target, + ".o_model_field_selector_popover_item:nth-child(3) .o_model_field_selector_popover_item_name" + ); assert.containsNone(target, ".o_popover"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Product"]); }); QUnit.test("Edit path in popover debug input", async (assert) => { @@ -534,10 +482,10 @@ QUnit.module("Components", (hooks) => { class Parent extends Component { setup() { - this.fieldName = "foo"; + this.path = "foo"; } - onUpdate(value) { - this.fieldName = value; + onUpdate(path) { + this.path = path; this.render(); } } @@ -546,30 +494,230 @@ QUnit.module("Components", (hooks) => { `; await mountComponent(Parent); + assert.deepEqual(getModelFieldSelectorValues(target), ["Foo"]); - assert.deepEqual( - [...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText), - ["Foo"] - ); - - await click(target, ".o_field_selector"); - + await openModelFieldSelectorPopover(target); await editInput( target, - ".o_field_selector_popover .o_field_selector_debug", + ".o_model_field_selector_popover .o_model_field_selector_debug", "partner_id.bar" ); + assert.deepEqual(getModelFieldSelectorValues(target), ["Partner", "Bar"]); + }); - assert.deepEqual( - [...target.querySelectorAll(".o_field_selector_value span")].map((el) => el.innerText), - ["Partner", "Bar"] + QUnit.test("title on first four pages", async (assert) => { + serverData.models.turtle = { + fields: { + mother_id: { + string: "Mother", + type: "many2one", + relation: "turtle", + searchable: true, + }, + }, + }; + + await mountComponent(ModelFieldSelector, { + props: { + readonly: false, + path: "mother_id", + resModel: "turtle", + }, + }); + await openModelFieldSelectorPopover(target); + assert.strictEqual(getTitle(target), ""); + + await followRelation(target); + assert.strictEqual(getTitle(target), "Mother"); + + await followRelation(target); + assert.strictEqual(getTitle(target), "... > Mother"); + + await followRelation(target); + assert.strictEqual(getTitle(target), "... > Mother"); + }); + + QUnit.test("start on complex path and click prev", async (assert) => { + serverData.models.turtle = { + fields: { + mother_id: { + string: "Mother", + type: "many2one", + relation: "turtle", + searchable: true, + }, + father_id: { + string: "Father", + type: "many2one", + relation: "turtle", + searchable: true, + }, + }, + }; + + await mountComponent(ModelFieldSelector, { + props: { + readonly: false, + path: "mother_id.father_id.mother_id", + resModel: "turtle", + }, + }); + + await openModelFieldSelectorPopover(target); + // viewing third page + // mother is selected on that page + assert.strictEqual(getTitle(target), "... > Father"); + assert.strictEqual(getFocusedFieldName(target), "Mother"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Mother", "Father", "Mother"]); + + // select Father on third page and go to next page + // no selection on fourth page --> first item is focused + await followRelation(target); + assert.strictEqual(getTitle(target), "... > Father"); + assert.strictEqual(getFocusedFieldName(target), "Father"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Mother", "Father", "Father"]); + + // go back to third page. Nothing has changed + await clickPrev(target); + assert.strictEqual(getTitle(target), "... > Father"); + assert.strictEqual(getFocusedFieldName(target), "Father"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Mother", "Father", "Father"]); + + // go back to second page. Nothing has changed. + await clickPrev(target); + assert.strictEqual(getTitle(target), "Mother"); + assert.strictEqual(getFocusedFieldName(target), "Father"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Mother", "Father"]); + + // go back to first page. Nothing has changed. + await clickPrev(target); + assert.strictEqual(getTitle(target), ""); + assert.strictEqual(getFocusedFieldName(target), "Mother"); + assert.deepEqual(getModelFieldSelectorValues(target), ["Mother"]); + assert.containsNone(target, ".o_model_field_selector_popover_prev_page"); + }); + + QUnit.test("support of invalid paths", async (assert) => { + class Parent extends Component { + setup() { + this.state = useState({ path: `` }); + } + } + Parent.components = { ModelFieldSelector }; + Parent.template = xml``; + + const parent = await mountComponent(Parent); + assert.deepEqual(getModelFieldSelectorValues(target), ["-"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + parent.state.path = undefined; + await nextTick(); + assert.deepEqual(getModelFieldSelectorValues(target), ["-"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + parent.state.path = {}; + await nextTick(); + assert.deepEqual(getModelFieldSelectorValues(target), ["-"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + parent.state.path = `a`; + await nextTick(); + assert.deepEqual(getModelFieldSelectorValues(target), ["a"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + parent.state.path = `foo.a`; + await nextTick(); + assert.deepEqual(getModelFieldSelectorValues(target), ["Foo", "a"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + parent.state.path = `a.foo`; + await nextTick(); + assert.deepEqual(getModelFieldSelectorValues(target), ["a", "foo"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + }); + + QUnit.test("debug input", async (assert) => { + assert.expect(10); + let num = 1; + class Parent extends Component { + setup() { + this.state = useState({ path: `` }); + } + update(path, fieldInfo) { + if (num === 1) { + assert.strictEqual(path, "a"); + assert.deepEqual(fieldInfo, { + fieldDef: null, + resModel: "partner", + }); + num++; + } else { + assert.strictEqual(path, "foo"); + assert.deepEqual(fieldInfo, { + fieldDef: { + name: "foo", + searchable: true, + string: "Foo", + type: "char", + }, + resModel: "partner", + }); + } + } + } + Parent.components = { ModelFieldSelector }; + Parent.template = xml``; + + await mountComponent(Parent); + assert.deepEqual(getModelFieldSelectorValues(target), ["-"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + await openModelFieldSelectorPopover(target); + await editInput(target, ".o_model_field_selector_debug", "a"); + assert.deepEqual(getModelFieldSelectorValues(target), ["a"]); + assert.containsOnce(target, ".o_model_field_selector_warning"); + + await click(target, ".o_model_field_selector_popover_close"); + + await openModelFieldSelectorPopover(target); + const debugInput = target.querySelector(".o_model_field_selector_debug"); + debugInput.focus(); + debugInput.value = "foo"; + await triggerEvent(debugInput, null, "keydown", { key: "Enter" }); + assert.deepEqual(getModelFieldSelectorValues(target), ["Foo"]); + assert.containsNone(target, ".o_model_field_selector_warning"); + + await click(target, ".o_model_field_selector_popover_close"); + }); + + QUnit.test("focus on search input", async (assert) => { + class Parent extends Component { + setup() { + this.state = useState({ path: `foo` }); + } + update() {} + } + Parent.components = { ModelFieldSelector }; + Parent.template = xml``; + + await mountComponent(Parent); + await openModelFieldSelectorPopover(target); + assert.strictEqual( + document.activeElement, + target.querySelector(".o_model_field_selector_popover_search .o_input") + ); + + await followRelation(target); + assert.strictEqual( + document.activeElement, + target.querySelector(".o_model_field_selector_popover_search .o_input") ); }); }); diff --git a/addons/web/static/tests/search/advanced_search_dialog_tests.js b/addons/web/static/tests/search/advanced_search_dialog_tests.js index 709426c116e..1530549daba 100644 --- a/addons/web/static/tests/search/advanced_search_dialog_tests.js +++ b/addons/web/static/tests/search/advanced_search_dialog_tests.js @@ -235,9 +235,9 @@ QUnit.module("Search", (hooks) => { await openAdvancedSearchDialog(target); assert.containsOnce(target, ".modal"); assert.containsOnce(target, ".modal .modal-body .o_domain_selector"); - assert.containsOnce(target, ".o_field_selector"); + assert.containsOnce(target, ".o_model_field_selector"); assert.strictEqual( - target.querySelector(".o_field_selector").innerText, + target.querySelector(".o_model_field_selector").innerText, "Floaty McFloatface" ); assert.containsOnce(target, ".o_domain_leaf_operator_select"); @@ -273,7 +273,7 @@ QUnit.module("Search", (hooks) => { await toggleFilterMenu(target); await openAdvancedSearchDialog(target); - assert.strictEqual(target.querySelector(".o_field_selector").innerText, "User"); + assert.strictEqual(target.querySelector(".o_model_field_selector").innerText, "User"); assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "equal"); assert.strictEqual(target.querySelector(".o_ds_value_cell input").value, "7"); }); @@ -604,7 +604,7 @@ QUnit.module("Search", (hooks) => { await openAdvancedSearchDialog(target); assert.deepEqual( - getNodesTextContent(target.querySelectorAll(".o_field_selector_chain_part")), + getNodesTextContent(target.querySelectorAll(".o_model_field_selector_chain_part")), ["DateTime", "DateTime", "A date", "A date"] ); // 0 not found! diff --git a/addons/web/static/tests/views/fields/domain_field_tests.js b/addons/web/static/tests/views/fields/domain_field_tests.js index 5849be9ee08..083f4e74c95 100644 --- a/addons/web/static/tests/views/fields/domain_field_tests.js +++ b/addons/web/static/tests/views/fields/domain_field_tests.js @@ -197,33 +197,33 @@ QUnit.module("Fields", (hooks) => { // Clicking on the button should add the [["id", "=", "1"]] domain, so // there should be a field selector in the DOM await click(target, ".o_domain_add_first_node_button"); - assert.containsOnce(target, ".o_field_selector", "there should be a field selector"); + assert.containsOnce(target, ".o_model_field_selector", "there should be a field selector"); // Focusing the field selector input should open the field selector // popover - await click(target, ".o_field_selector"); + await click(target, ".o_model_field_selector"); assert.containsOnce( document.body, - ".o_field_selector_popover", + ".o_model_field_selector_popover", "field selector popover should be visible" ); assert.containsOnce( document.body, - ".o_field_selector_search input", + ".o_model_field_selector_popover_search input", "field selector popover should contain a search input" ); // The popover should contain the list of partner_type fields and so // there should be the "Color index" field assert.strictEqual( - document.body.querySelector(".o_field_selector_item").textContent, + document.body.querySelector(".o_model_field_selector_popover_item").textContent, "Color index", "field selector popover should contain 'Color index' field" ); // Clicking on this field should close the popover, then changing the // associated value should reveal one matched record - await click(document.body.querySelector(".o_field_selector_item")); + await click(document.body.querySelector(".o_model_field_selector_popover_item_name")); const input = target.querySelector(".o_domain_leaf_value_input"); input.value = 2; @@ -264,8 +264,12 @@ QUnit.module("Fields", (hooks) => { }); await click(target, ".o_domain_add_first_node_button"); - await click(target, ".o_field_selector"); - await click(document.body.querySelector(".o_field_selector_item[data-name='image']")); + await click(target, ".o_model_field_selector"); + await click( + document.body.querySelector( + ".o_model_field_selector_popover_item[data-name='image'] button" + ) + ); }); QUnit.test("domain field is correctly reset on every view change", async function (assert) { @@ -293,15 +297,15 @@ QUnit.module("Fields", (hooks) => { // selector to change this assert.containsOnce( target, - ".o_field_domain .o_field_selector", + ".o_field_domain .o_model_field_selector", "there should be a field selector" ); // Focusing its input should open the field selector popover - await click(target.querySelector(".o_field_selector")); + await click(target.querySelector(".o_model_field_selector")); assert.containsOnce( document.body, - ".o_field_selector_popover", + ".o_model_field_selector_popover", "field selector popover should be visible" ); @@ -309,11 +313,11 @@ QUnit.module("Fields", (hooks) => { // popover should contain the list of "product" fields assert.containsOnce( document.body, - ".o_field_selector_item", + ".o_model_field_selector_popover_item", "field selector popover should contain only one field" ); assert.strictEqual( - document.body.querySelector(".o_field_selector_item").textContent, + document.body.querySelector(".o_model_field_selector_popover_item").textContent, "Product Name", "field selector popover should contain 'Product Name' field" ); @@ -322,22 +326,22 @@ QUnit.module("Fields", (hooks) => { await editInput(target, ".o_field_widget[name='bar'] input", "partner_type"); // Refocusing the field selector input should open the popover again - await click(target.querySelector(".o_field_selector")); + await click(target.querySelector(".o_model_field_selector")); assert.containsOnce( document.body, - ".o_field_selector_popover", + ".o_model_field_selector_popover", "field selector popover should be visible" ); // Now the list of fields should be the ones of the "partner_type" model assert.containsN( document.body, - ".o_field_selector_item", + ".o_model_field_selector_popover_item", 2, "field selector popover should contain two fields" ); assert.strictEqual( - document.body.querySelector(".o_field_selector_item").textContent, + document.body.querySelector(".o_model_field_selector_popover_item").textContent, "Color index", "field selector popover should contain 'Color index' field" ); diff --git a/addons/web/static/tests/views/fields/text_field_tests.js b/addons/web/static/tests/views/fields/text_field_tests.js index 72611887882..0fee824b325 100644 --- a/addons/web/static/tests/views/fields/text_field_tests.js +++ b/addons/web/static/tests/views/fields/text_field_tests.js @@ -627,9 +627,9 @@ QUnit.module("Fields", (hooks) => { await click(target, "[name=txt] textarea"); assert.strictEqual(document.activeElement, target.querySelector("[name=txt] textarea")); - assert.containsNone(document.body, ".o_popover .o_field_selector_popover"); + assert.containsNone(document.body, ".o_popover .o_model_field_selector_popover"); triggerHotkey("#"); await nextTick(); - assert.containsOnce(document.body, ".o_popover .o_field_selector_popover"); + assert.containsOnce(document.body, ".o_popover .o_model_field_selector_popover"); }); });