diff --git a/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.js b/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.js index 3db747068b4..1f44ad75f1f 100644 --- a/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.js +++ b/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.js @@ -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); - } } diff --git a/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.xml b/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.xml deleted file mode 100644 index b6e7453c79c..00000000000 --- a/addons/web/static/src/core/domain_selector/domain_selector_autocomplete.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - -
- - -
-
- - - - - -
diff --git a/addons/web/static/src/core/domain_selector/domain_selector_fields.js b/addons/web/static/src/core/domain_selector/domain_selector_fields.js index 57fd640935c..24e5658315f 100644 --- a/addons/web/static/src/core/domain_selector/domain_selector_fields.js +++ b/addons/web/static/src/core/domain_selector/domain_selector_fields.js @@ -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), diff --git a/addons/web/static/src/core/record_selectors/multi_record_selector.js b/addons/web/static/src/core/record_selectors/multi_record_selector.js new file mode 100644 index 00000000000..290c8ad4587 --- /dev/null +++ b/addons/web/static/src/core/record_selectors/multi_record_selector.js @@ -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]); + } +} diff --git a/addons/web/static/src/core/record_selectors/multi_record_selector.xml b/addons/web/static/src/core/record_selectors/multi_record_selector.xml new file mode 100644 index 00000000000..6bef7e439fb --- /dev/null +++ b/addons/web/static/src/core/record_selectors/multi_record_selector.xml @@ -0,0 +1,21 @@ + + + + +
+ + +
+
+ +
diff --git a/addons/web/static/src/core/record_selectors/record_autocomplete.js b/addons/web/static/src/core/record_selectors/record_autocomplete.js new file mode 100644 index 00000000000..6510645580f --- /dev/null +++ b/addons/web/static/src/core/record_selectors/record_autocomplete.js @@ -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([]); + } + } +} diff --git a/addons/web/static/src/core/record_selectors/record_autocomplete.xml b/addons/web/static/src/core/record_selectors/record_autocomplete.xml new file mode 100644 index 00000000000..4830fea8777 --- /dev/null +++ b/addons/web/static/src/core/record_selectors/record_autocomplete.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/addons/web/static/src/core/record_selectors/record_selector.js b/addons/web/static/src/core/record_selectors/record_selector.js new file mode 100644 index 00000000000..9803c1517b0 --- /dev/null +++ b/addons/web/static/src/core/record_selectors/record_selector.js @@ -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); + } +} diff --git a/addons/web/static/src/core/record_selectors/record_selector.xml b/addons/web/static/src/core/record_selectors/record_selector.xml new file mode 100644 index 00000000000..1261eae8727 --- /dev/null +++ b/addons/web/static/src/core/record_selectors/record_selector.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/addons/web/static/tests/core/record_selectors/multi_record_selector_tests.js b/addons/web/static/tests/core/record_selectors/multi_record_selector_tests.js new file mode 100644 index 00000000000..a2718e154e2 --- /dev/null +++ b/addons/web/static/tests/core/record_selectors/multi_record_selector_tests.js @@ -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` + `; + + 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"]); + }); +}); diff --git a/addons/web/static/tests/core/record_selectors/record_selector_tests.js b/addons/web/static/tests/core/record_selectors/record_selector_tests.js new file mode 100644 index 00000000000..c7f0d9cbd3c --- /dev/null +++ b/addons/web/static/tests/core/record_selectors/record_selector_tests.js @@ -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` + `; + + 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"]); + }); +});