[IMP] web: introduce MultiRecordSelector and RecordSelector

This commit introduces two new components, MultiRecordSelector and
RecordSelector, which are used to select multiple records and a single
record respectively.

The MultiRecordSelector component is used to display the selected
records with a tag-like style. It also allows to add new records by
typing/selecting a valut in the input field.

The RecordSelector component is used to display the selected record and
allows to select a new record by typing/selecting a value in the input
field.

These components are created from the DomainSelectorAutocomplete and
DomainSelectorSingleAutocomplete components, which are now an extension
of the MultiRecordSelector and RecordSelector components respectively.

This commit also add tests for these two components.

Part of task-id 3433922

closes odoo/odoo#137650

Related: odoo/enterprise#48423
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Pierre Rousseau
2023-10-09 07:00:06 +00:00
parent 176d3be297
commit bd0f2c6d79
11 changed files with 652 additions and 258 deletions
@@ -1,150 +1,14 @@
/** @odoo-module **/
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { MultiRecordSelector } from "../record_selectors/multi_record_selector";
import { _t } from "@web/core/l10n/translation";
import { Domain } from "@web/core/domain";
import { formatAST, toPyValue } from "@web/core/py_js/py_utils";
import { registry } from "@web/core/registry";
import { TagsList } from "@web/core/tags_list/tags_list";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { Expression } from "@web/core/domain_tree";
import { RecordSelector } from "../record_selectors/record_selector";
const SEARCH_LIMIT = 7;
const SEARCH_MORE_LIMIT = 320;
export const isId = (val) => Number.isInteger(val) && val >= 1;
const isId = (val) => Number.isInteger(val) && val >= 1;
class AutoCompleteWithSources extends Component {
static props = {
resModel: String,
update: Function,
multiSelect: Boolean,
getIds: Function,
value: String,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
className: { type: String, optional: true },
fieldString: { type: String, optional: true },
};
static components = { AutoComplete };
static template = "web.DomainSelector.AutoCompleteWithSources";
setup() {
this.orm = useService("orm");
this.nameService = useService("name");
this.addDialog = useOwnedDialogs();
this.sources = [
{
placeholder: _t("Loading..."),
options: this.loadOptionsSource.bind(this),
},
];
}
addNames(nameGets) {
const displayNames = {};
for (const [id, label] of nameGets) {
displayNames[id] = label.split("\n")[0];
}
this.nameService.addDisplayNames(this.props.resModel, displayNames);
}
getIds() {
return this.props.getIds();
}
async loadOptionsSource(name) {
if (this.lastProm) {
this.lastProm.abort(false);
}
this.lastProm = this.search(name, SEARCH_LIMIT + 1);
const nameGets = await this.lastProm;
this.addNames(nameGets);
const options = nameGets.map(([value, label]) => ({ value, label: label.split("\n")[0] }));
if (SEARCH_LIMIT < nameGets.length) {
options.push({
label: _t("Search More..."),
action: this.onSearchMore.bind(this, name),
classList: "o_m2o_dropdown_option",
});
}
if (options.length === 0) {
options.push({ label: _t("(no result)"), unselectable: true });
}
return options;
}
async onSearchMore(name) {
const { fieldString, multiSelect, resModel } = this.props;
let operator;
const ids = [];
if (name) {
const nameGets = await this.search(name, SEARCH_MORE_LIMIT);
this.addNames(nameGets);
operator = "in";
ids.push(...nameGets.map((nameGet) => nameGet[0]));
} else {
operator = "not in";
ids.push(...this.getIds());
}
const dynamicFilters = ids.length
? [
{
description: _t("Quick search: %s", name),
domain: [["id", operator, ids]],
},
]
: undefined;
// fine for now but we don't like this kind of dependence of core to views
const SelectCreateDialog = registry.category("dialogs").get("select_create");
this.addDialog(SelectCreateDialog, {
title: _t("Search: %s", fieldString),
dynamicFilters,
resModel,
noCreate: true,
multiSelect,
context: this.props.context || {},
onSelected: (resId) => {
const resIds = Array.isArray(resId) ? resId : [resId];
this.props.update([...resIds]);
},
});
}
getDomain() {
const domainIds = Domain.not([["id", "in", this.getIds()]]);
if (this.props.domain) {
return Domain.and([this.props.domain, domainIds]).toList();
}
return domainIds.toList();
}
onSelect({ value: resId, action }, params) {
if (action) {
return action(params);
}
this.props.update([resId]);
}
search(name, limit) {
const domain = this.getDomain();
return this.orm.call(this.props.resModel, "name_search", [], {
name,
args: domain,
limit,
context: this.props.context || {},
});
}
onChange({ inputValue }) {
if (!inputValue.length) {
this.props.update([]);
}
}
}
const getFormat = (val, displayNames) => {
export const getFormat = (val, displayNames) => {
let text;
let colorIndex;
if (isId(val)) {
@@ -163,105 +27,52 @@ const getFormat = (val, displayNames) => {
return { text, colorIndex };
};
export class DomainSelectorAutocomplete extends Component {
export class DomainSelectorAutocomplete extends MultiRecordSelector {
static props = {
resModel: String,
update: Function,
value: true,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
fieldString: { type: String, optional: true },
...MultiRecordSelector.props,
resIds: true, //resIds could be an array of ids or an array of expressions
};
static components = { AutoCompleteWithSources, TagsList };
static template = "web.DomainSelector.DomainSelectorAutocomplete";
setup() {
this.nameService = useService("name");
onWillStart(() => this.computeDerivedParams());
onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));
}
async computeDerivedParams(props = this.props) {
const displayNames = await this.getDisplayNames(props);
this.tags = this.getTags(props, displayNames);
}
async getDisplayNames(props) {
const ids = this.getIds(props);
return this.nameService.loadDisplayNames(props.resModel, ids);
}
getIds(props = this.props) {
return props.value.filter((val) => isId(val));
return props.resIds.filter((val) => isId(val));
}
getTags(props, displayNames) {
return props.value.map((val, index) => {
return props.resIds.map((val, index) => {
const { text, colorIndex } = getFormat(val, displayNames);
return {
text,
colorIndex,
onDelete: () => {
this.props.update([
...this.props.value.slice(0, index),
...this.props.value.slice(index + 1),
...this.props.resIds.slice(0, index),
...this.props.resIds.slice(index + 1),
]);
},
};
});
}
update(resIds) {
this.props.update([...this.props.value, ...resIds]);
}
}
export class DomainSelectorSingleAutocomplete extends Component {
export class DomainSelectorSingleAutocomplete extends RecordSelector {
static props = {
resModel: String,
update: Function,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
value: true,
fieldString: { type: String, optional: true },
...RecordSelector.props,
resId: true,
};
static components = { AutoCompleteWithSources };
static template = "web.DomainSelector.DomainSelectorSingleAutocomplete";
setup() {
this.nameService = useService("name");
onWillStart(() => this.computeDerivedParams());
onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));
}
async computeDerivedParams(props = this.props) {
const displayNames = await this.getDisplayNames(props);
this.displayName = this.getDisplayName(props, displayNames);
}
async getDisplayNames(props) {
const ids = this.getIds(props);
return this.nameService.loadDisplayNames(props.resModel, ids);
}
getDisplayName(props = this.props, displayNames) {
const { value } = props;
if (value === false) {
const { resId } = props;
if (resId === false) {
return "";
}
const { text } = getFormat(value, displayNames);
const { text } = getFormat(resId, displayNames);
return text;
}
getIds(props = this.props) {
if (isId(props.value)) {
return [props.value];
if (isId(props.resId)) {
return [props.resId];
}
return [];
}
update(resIds) {
this.props.update(resIds[0] || false);
this.render(true);
}
}
@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.DomainSelector.AutoCompleteWithSources" >
<AutoComplete
value="props.value"
autoSelect="true"
resetOnSelect="props.multiSelect"
class="props.className"
sources="sources"
onSelect.bind="onSelect"
onChange.bind="onChange"
/>
</t>
<t t-name="web.DomainSelector.DomainSelectorAutocomplete" >
<div class="o_input d-flex flex-wrap h-100 gap-1">
<TagsList tags="tags"/>
<AutoCompleteWithSources
resModel="props.resModel"
value="''"
domain="props.domain"
context="props.context"
className="'o_input o_domain_leaf_value_input flex-grow-1'"
fieldString="props.fieldString"
multiSelect="true"
getIds.bind="getIds"
update.bind="update"
/>
</div>
</t>
<t t-name="web.DomainSelector.DomainSelectorSingleAutocomplete" >
<AutoCompleteWithSources
resModel="props.resModel"
value="displayName"
domain="props.domain"
context="props.context"
className="'o_input o_domain_leaf_value_input h-100'"
fieldString="props.fieldString"
multiSelect="false"
getIds="() => []"
update.bind="update"
/>
</t>
</templates>
@@ -319,7 +319,7 @@ const RELATIONAL_EDITOR_EQUALITY = makeEditor(DomainSelectorSingleAutocomplete,
resModel: fieldDef.relation,
fieldString: fieldDef.string,
update,
value,
resId: value,
};
},
isSupported: () => true,
@@ -331,8 +331,8 @@ const RELATIONAL_EDITOR_IN = makeEditor(DomainSelectorAutocomplete, {
return {
resModel: fieldDef.relation,
fieldString: fieldDef.string,
update: (value) => update([...new Set(value)]),
value: Array.isArray(value) ? [...new Set(value)] : value,
update: (resIds) => update([...new Set(resIds)]),
resIds: Array.isArray(value) ? [...new Set(value)] : value,
};
},
isSupported: (value) => Array.isArray(value),
@@ -0,0 +1,62 @@
/** @odoo-module **/
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { TagsList } from "@web/core/tags_list/tags_list";
import { useService } from "@web/core/utils/hooks";
import { RecordAutocomplete } from "./record_autocomplete";
import { _t } from "@web/core/l10n/translation";
export class MultiRecordSelector extends Component {
static props = {
resIds: { type: Array, element: Number },
resModel: String,
update: Function,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
fieldString: { type: String, optional: true },
};
static components = { RecordAutocomplete, TagsList };
static template = "web.MultiRecordSelector";
setup() {
this.nameService = useService("name");
onWillStart(() => this.computeDerivedParams());
onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));
}
async computeDerivedParams(props = this.props) {
const displayNames = await this.getDisplayNames(props);
this.tags = this.getTags(props, displayNames);
}
async getDisplayNames(props) {
const ids = this.getIds(props);
return this.nameService.loadDisplayNames(props.resModel, ids);
}
getIds(props = this.props) {
return props.resIds;
}
getTags(props, displayNames) {
return props.resIds.map((id, index) => {
const text =
typeof displayNames[id] === "string"
? displayNames[id]
: _t("Inaccessible/missing record ID: %s", id);
return {
text,
onDelete: () => {
this.props.update([
...this.props.resIds.slice(0, index),
...this.props.resIds.slice(index + 1),
]);
},
};
});
}
update(resIds) {
this.props.update([...this.props.resIds, ...resIds]);
}
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.MultiRecordSelector" >
<div class="o_input d-flex flex-wrap h-100 gap-1 o_multi_record_selector">
<TagsList tags="tags"/>
<RecordAutocomplete
resModel="props.resModel"
value="''"
domain="props.domain"
context="props.context"
className="'o_input o_domain_leaf_value_input flex-grow-1'"
fieldString="props.fieldString"
multiSelect="true"
getIds.bind="getIds"
update.bind="update"
/>
</div>
</t>
</templates>
@@ -0,0 +1,140 @@
/** @odoo-module **/
import { Component } from "@odoo/owl";
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
import { _t } from "@web/core/l10n/translation";
import { Domain } from "@web/core/domain";
import { registry } from "@web/core/registry";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
const SEARCH_LIMIT = 7;
const SEARCH_MORE_LIMIT = 320;
export class RecordAutocomplete extends Component {
static props = {
resModel: String,
update: Function,
multiSelect: Boolean,
getIds: Function,
value: String,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
className: { type: String, optional: true },
fieldString: { type: String, optional: true },
};
static components = { AutoComplete };
static template = "web.RecordAutocomplete";
setup() {
this.orm = useService("orm");
this.nameService = useService("name");
this.addDialog = useOwnedDialogs();
this.sources = [
{
placeholder: _t("Loading..."),
options: this.loadOptionsSource.bind(this),
},
];
}
addNames(nameGets) {
const displayNames = {};
for (const [id, label] of nameGets) {
displayNames[id] = label.split("\n")[0];
}
this.nameService.addDisplayNames(this.props.resModel, displayNames);
}
getIds() {
return this.props.getIds();
}
async loadOptionsSource(name) {
if (this.lastProm) {
this.lastProm.abort(false);
}
this.lastProm = this.search(name, SEARCH_LIMIT + 1);
const nameGets = await this.lastProm;
this.addNames(nameGets);
const options = nameGets.map(([value, label]) => ({ value, label: label.split("\n")[0] }));
if (SEARCH_LIMIT < nameGets.length) {
options.push({
label: _t("Search More..."),
action: this.onSearchMore.bind(this, name),
classList: "o_m2o_dropdown_option",
});
}
if (options.length === 0) {
options.push({ label: _t("(no result)"), unselectable: true });
}
return options;
}
async onSearchMore(name) {
const { fieldString, multiSelect, resModel } = this.props;
let operator;
const ids = [];
if (name) {
const nameGets = await this.search(name, SEARCH_MORE_LIMIT);
this.addNames(nameGets);
operator = "in";
ids.push(...nameGets.map((nameGet) => nameGet[0]));
} else {
operator = "not in";
ids.push(...this.getIds());
}
const dynamicFilters = ids.length
? [
{
description: _t("Quick search: %s", name),
domain: [["id", operator, ids]],
},
]
: undefined;
// fine for now but we don't like this kind of dependence of core to views
const SelectCreateDialog = registry.category("dialogs").get("select_create");
this.addDialog(SelectCreateDialog, {
title: _t("Search: %s", fieldString),
dynamicFilters,
resModel,
noCreate: true,
multiSelect,
context: this.props.context || {},
onSelected: (resId) => {
const resIds = Array.isArray(resId) ? resId : [resId];
this.props.update([...resIds]);
},
});
}
getDomain() {
const domainIds = Domain.not([["id", "in", this.getIds()]]);
if (this.props.domain) {
return Domain.and([this.props.domain, domainIds]).toList();
}
return domainIds.toList();
}
onSelect({ value: resId, action }, params) {
if (action) {
return action(params);
}
this.props.update([resId]);
}
search(name, limit) {
const domain = this.getDomain();
return this.orm.call(this.props.resModel, "name_search", [], {
name,
args: domain,
limit,
context: this.props.context || {},
});
}
onChange({ inputValue }) {
if (!inputValue.length) {
this.props.update([]);
}
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.RecordAutocomplete" >
<AutoComplete
value="props.value"
autoSelect="true"
resetOnSelect="props.multiSelect"
class="props.className"
sources="sources"
onSelect.bind="onSelect"
onChange.bind="onChange"
/>
</t>
</templates>
@@ -0,0 +1,57 @@
/** @odoo-module **/
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { RecordAutocomplete } from "./record_autocomplete";
import { _t } from "@web/core/l10n/translation";
export class RecordSelector extends Component {
static props = {
resId: [Number, { value: false }],
resModel: String,
update: Function,
domain: { type: Array, optional: true },
context: { type: Object, optional: true },
fieldString: { type: String, optional: true },
};
static components = { RecordAutocomplete };
static template = "web.RecordSelector";
setup() {
this.nameService = useService("name");
onWillStart(() => this.computeDerivedParams());
onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));
}
async computeDerivedParams(props = this.props) {
const displayNames = await this.getDisplayNames(props);
this.displayName = this.getDisplayName(props, displayNames);
}
async getDisplayNames(props) {
const ids = this.getIds(props);
return this.nameService.loadDisplayNames(props.resModel, ids);
}
getDisplayName(props = this.props, displayNames) {
const { resId } = props;
if (resId === false) {
return "";
}
return typeof displayNames[resId] === "string"
? displayNames[resId]
: _t("Inaccessible/missing record ID: %s", resId);
}
getIds(props = this.props) {
if (props.resId) {
return [props.resId];
}
return [];
}
update(resIds) {
this.props.update(resIds[0] || false);
this.render(true);
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.RecordSelector" >
<RecordAutocomplete
resModel="props.resModel"
value="displayName"
domain="props.domain"
context="props.context"
className="'o_input o_domain_leaf_value_input h-100 o_record_selector'"
fieldString="props.fieldString"
multiSelect="false"
getIds="() => []"
update.bind="update"
/>
</t>
</templates>
@@ -0,0 +1,166 @@
/** @odoo-module **/
import { MultiRecordSelector } from "@web/core/record_selectors/multi_record_selector";
import { makeTestEnv } from "../../helpers/mock_env";
import { getFixture, mount, click } from "../../helpers/utils";
import { registry } from "@web/core/registry";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { Component, useState, xml } from "@odoo/owl";
import { nameService } from "@web/core/name_service";
import { dialogService } from "@web/core/dialog/dialog_service";
QUnit.module("Web Components", (hooks) => {
QUnit.module("MultiRecordSelector");
let target;
const serverData = {
models: {
partner: {
fields: {
display_name: { string: "Display name", type: "char" },
},
records: [
{ id: 1, display_name: "Alice" },
{ id: 2, display_name: "Bob" },
{ id: 3, display_name: "Charlie" },
],
},
},
};
async function makeMultiRecordSelector(props, { mockRPC } = {}) {
class Parent extends Component {
setup() {
this.state = useState({ resIds: props.resIds });
}
get recordProps() {
return {
...props,
resIds: this.state.resIds,
update: (resIds) => this._update(resIds),
};
}
_update(resIds) {
this.state.resIds = resIds;
}
}
Parent.components = { MultiRecordSelector };
Parent.template = xml`
<MultiRecordSelector t-props="recordProps" />`;
const env = await makeTestEnv({ serverData, mockRPC });
await mount(Parent, target, { env });
}
hooks.beforeEach(async () => {
target = getFixture();
registry.category("services").add("hotkey", hotkeyService);
registry.category("services").add("dialog", dialogService);
registry.category("services").add("name", nameService);
});
QUnit.test("Can be renderer with no values", async (assert) => {
await makeMultiRecordSelector({
resModel: "partner",
resIds: [],
});
const input = target.querySelector(".o_multi_record_selector input");
assert.strictEqual(input.value, "", "The input should be empty");
assert.hasClass(input, "o_input");
});
QUnit.test("Can be renderer with a value", async (assert) => {
await makeMultiRecordSelector({
resModel: "partner",
resIds: [1],
});
const input = target.querySelector(".o_multi_record_selector input");
assert.strictEqual(input.value, "");
assert.strictEqual(target.querySelectorAll(".o_tag").length, 1);
assert.strictEqual(target.querySelector(".o_tag").textContent, "Alice");
});
QUnit.test("Can be renderer with multiple values", async (assert) => {
await makeMultiRecordSelector({
resModel: "partner",
resIds: [1, 2],
});
const input = target.querySelector(".o_multi_record_selector input");
assert.strictEqual(input.value, "");
assert.strictEqual(target.querySelectorAll(".o_tag").length, 2);
assert.deepEqual(
[...target.querySelectorAll(".o_tag")].map((el) => el.textContent),
["Alice", "Bob"]
);
});
QUnit.test("Can be updated from autocomplete", async (assert) => {
await makeMultiRecordSelector({
resModel: "partner",
resIds: [],
});
const input = target.querySelector(".o_multi_record_selector input");
assert.containsNone(target, ".o_tag");
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
await click(input);
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
const secondItem = target.querySelectorAll("li.o-autocomplete--dropdown-item")[1];
await click(secondItem);
assert.containsOnce(target, ".o_tag");
assert.strictEqual(target.querySelector(".o_tag").textContent, "Bob");
});
QUnit.test("Display name is correctly fetched", async (assert) => {
await makeMultiRecordSelector(
{
resModel: "partner",
resIds: [1],
},
{
mockRPC: (route, args) => {
if (args.method === "web_search_read") {
assert.step("web_search_read");
assert.strictEqual(args.model, "partner");
assert.deepEqual(args.kwargs.domain, [["id", "in", [1]]]);
}
},
}
);
assert.strictEqual(target.querySelectorAll(".o_tag").length, 1);
assert.strictEqual(target.querySelector(".o_tag").textContent, "Alice");
assert.verifySteps(["web_search_read"]);
});
QUnit.test("Can give domain and context props for the name search", async (assert) => {
await makeMultiRecordSelector(
{
resModel: "partner",
resIds: [1],
domain: [["display_name", "=", "Bob"]],
context: { blip: "blop " },
},
{
mockRPC: (route, args) => {
if (args.method === "name_search") {
assert.step("name_search");
assert.strictEqual(args.model, "partner");
assert.deepEqual(args.kwargs.args, [
"&",
["display_name", "=", "Bob"],
"!",
["id", "in", [1]],
]);
assert.strictEqual(args.kwargs.context.blip, "blop ");
}
},
}
);
const input = target.querySelector(".o_multi_record_selector input");
assert.verifySteps([]);
await click(input);
assert.verifySteps(["name_search"]);
});
});
@@ -0,0 +1,150 @@
/** @odoo-module **/
import { RecordSelector } from "@web/core/record_selectors/record_selector";
import { makeTestEnv } from "../../helpers/mock_env";
import { getFixture, mount, click } from "../../helpers/utils";
import { registry } from "@web/core/registry";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { Component, useState, xml } from "@odoo/owl";
import { nameService } from "@web/core/name_service";
import { dialogService } from "@web/core/dialog/dialog_service";
QUnit.module("Web Components", (hooks) => {
QUnit.module("RecordSelector");
let target;
const serverData = {
models: {
partner: {
fields: {
display_name: { string: "Display name", type: "char" },
},
records: [
{ id: 1, display_name: "Alice" },
{ id: 2, display_name: "Bob" },
{ id: 3, display_name: "Charlie" },
],
},
},
};
async function makeRecordSelector(props, { mockRPC } = {}) {
class Parent extends Component {
setup() {
this.state = useState({ resId: props.resId });
}
get recordProps() {
return {
...props,
resId: this.state.resId,
update: (resId) => this._update(resId),
};
}
_update(resId) {
this.state.resId = resId;
}
}
Parent.components = { RecordSelector };
Parent.template = xml`
<RecordSelector t-props="recordProps" />`;
const env = await makeTestEnv({ serverData, mockRPC });
await mount(Parent, target, { env });
}
hooks.beforeEach(async () => {
target = getFixture();
registry.category("services").add("hotkey", hotkeyService);
registry.category("services").add("dialog", dialogService);
registry.category("services").add("name", nameService);
});
QUnit.test("Can be renderer with no values", async (assert) => {
await makeRecordSelector({
resModel: "partner",
resId: false,
});
const input = target.querySelector(".o_record_selector input");
assert.strictEqual(input.value, "", "The input should be empty");
assert.hasClass(input, "o_input");
});
QUnit.test("Can be renderer with a value", async (assert) => {
await makeRecordSelector({
resModel: "partner",
resId: 1,
});
const input = target.querySelector(".o_record_selector input");
assert.strictEqual(input.value, "Alice");
});
QUnit.test("Can be updated from autocomplete", async (assert) => {
await makeRecordSelector({
resModel: "partner",
resId: 1,
});
const input = target.querySelector(".o_record_selector input");
assert.strictEqual(input.value, "Alice");
assert.containsNone(target, ".o-autocomplete--dropdown-menu");
await click(input);
assert.containsOnce(target, ".o-autocomplete--dropdown-menu");
const secondItem = target.querySelectorAll("li.o-autocomplete--dropdown-item")[1];
await click(secondItem);
assert.strictEqual(input.value, "Bob");
});
QUnit.test("Display name is correctly fetched", async (assert) => {
await makeRecordSelector(
{
resModel: "partner",
resId: 1,
},
{
mockRPC: (route, args) => {
if (args.method === "web_search_read") {
assert.step("web_search_read");
assert.strictEqual(args.model, "partner");
assert.deepEqual(args.kwargs.domain, [["id", "in", [1]]]);
}
},
}
);
const input = target.querySelector(".o_record_selector input");
assert.strictEqual(input.value, "Alice");
assert.verifySteps(["web_search_read"]);
});
QUnit.test("Can give domain and context props for the name search", async (assert) => {
await makeRecordSelector(
{
resModel: "partner",
resId: 1,
domain: [["display_name", "=", "Bob"]],
context: { blip: "blop " },
},
{
mockRPC: (route, args) => {
if (args.method === "name_search") {
assert.step("name_search");
assert.strictEqual(args.model, "partner");
assert.deepEqual(args.kwargs.args, [
"&",
["display_name", "=", "Bob"],
"!",
["id", "in", []],
]);
assert.strictEqual(args.kwargs.context.blip, "blop ");
}
},
}
);
const input = target.querySelector(".o_record_selector input");
assert.strictEqual(input.value, "Alice");
assert.verifySteps([]);
await click(input);
assert.verifySteps(["name_search"]);
});
});