From ff2ee462a5402d4bdaaf1e7f46252d9ca97bb70a Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Tue, 30 May 2023 13:57:06 +0000 Subject: [PATCH] [IMP] web: search bar: facet descriptions We improve the facet descriptions of the domain created via the domain selector. We do this in several ways like it was done for the readonly mode of the domain selector: - instead of displaying ids for relational fields, we display the associated names. - instead of displaying values for selection fields, we display the associated labels. - quotes around strings are not displayed by default. They are if there is some ambiguity: the presence of a value of another type like 0, false or an expression makes necessary to put them. Task ID: 3291990 Part-of: odoo/odoo#121688 --- addons/web/static/src/search/search_model.js | 77 ++++++++++--------- .../src/search/with_search/with_search.js | 1 + .../tests/search/search_bar_menu_tests.js | 46 ++++++++++- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js index 38beeb34ed0..e2f4a5a32d1 100644 --- a/addons/web/static/src/search/search_model.js +++ b/addons/web/static/src/search/search_model.js @@ -20,7 +20,13 @@ import { FACET_ICONS, FACET_COLORS } from "./utils/misc"; import { EventBus, toRaw } from "@odoo/owl"; import { toDomain, toTree, formatValue, normalizeValue } from "@web/core/domain_tree"; -import { createVirtualOperators, extractPathsFromDomain } from "@web/core/domain_selector/utils"; +import { + createVirtualOperators, + extractIdsFromDomain, + extractPathsFromDomain, + leafToString, + useLoadDisplayNames, +} from "@web/core/domain_selector/utils"; import { useLoadFieldInfo, useLoadPathDescription } from "@web/core/model_field_selector/utils"; import { _t } from "@web/core/l10n/translation"; import { getOperatorInfo } from "@web/core/domain_selector/domain_selector_operators"; @@ -214,11 +220,17 @@ function simplifyTree(tree, isRoot = true) { * @param {boolean} [isSubExpression=true] * @returns {string} */ -export function getDomainTreeDescription(tree, getDescription, isSubExpression = true) { +function getDomainTreeDescription( + tree, + getFieldDef, + getDescription, + displayNames, + isSubExpression = true +) { if (tree.type === "connector") { // we assume that the domain tree is normalized (--> there is at least two children) const childDescriptions = tree.children.map((c) => - getDomainTreeDescription(c, getDescription) + getDomainTreeDescription(c, getFieldDef, getDescription, displayNames) ); const separator = tree.value === "&" ? _t("and") : _t("or"); let description = childDescriptions.join(` ${separator} `); @@ -230,43 +242,28 @@ export function getDomainTreeDescription(tree, getDescription, isSubExpression = } return description; } - - const { path, operator, value } = tree; - const operatorInfo = getOperatorInfo(operator); - - let description = `${getDescription(path)} ${operatorInfo.label} `; - - if (["set", "not_set"].includes(operatorInfo.key)) { - description = description.trim(); - } else if (["is", "is_not"].includes(operatorInfo.key)) { - description += value ? _t("set") : _t("not set"); - } else { - const values = Array.isArray(value) ? value : [value]; - let join; - let addParenthesis; - switch (operator) { - case "between": - join = _t("and"); - addParenthesis = false; - break; - case "in": - case "not in": - join = ","; - addParenthesis = true; - break; - default: - join = _t("or"); - addParenthesis = values.length > 1; - } - const jointedValues = values.map((val) => String(val)).join(` ${join} `); + const { negate, operator, path, value } = tree; + const fieldDef = getFieldDef(path); + const operatorInfo = getOperatorInfo(operator, negate); + const { operatorDescription, valueDescription } = leafToString( + fieldDef, + operatorInfo, + value, + displayNames[fieldDef?.relation] + ); + let description = `${getDescription(path)} ${operatorDescription} `; + if (valueDescription) { + const { values, join, addParenthesis } = valueDescription; + const jointedValues = values.join(` ${join} `); description += addParenthesis ? `( ${jointedValues} )` : jointedValues; } return description; } -export function useGetDomainTreeDescription(fieldService) { +function useGetDomainTreeDescription(fieldService, nameService) { const loadFieldInfo = useLoadFieldInfo(fieldService); const loadPathDescription = useLoadPathDescription(fieldService); + const loadDisplayNames = useLoadDisplayNames(nameService); return async (resModel, tree) => { tree = simplifyTree(tree); const domain = toDomain(tree); @@ -287,8 +284,16 @@ export function useGetDomainTreeDescription(fieldService) { await Promise.all(promises); const getFieldDef = (path) => pathFieldDefs[formatValue(path)]; const getDescription = (path) => pathDescriptions[formatValue(path)]; + const idsByModel = extractIdsFromDomain(domain, getFieldDef); const treeWithVirtualOperators = createVirtualOperators(tree, getFieldDef); - return getDomainTreeDescription(treeWithVirtualOperators, getDescription, false); + const displayNames = await loadDisplayNames(idsByModel); + return getDomainTreeDescription( + treeWithVirtualOperators, + getFieldDef, + getDescription, + displayNames, + false + ); }; } @@ -310,12 +315,12 @@ export class SearchModel extends EventBus { */ setup(services) { // services - const { field: fieldService, orm, user, view } = services; + const { field: fieldService, name: nameService, orm, user, view } = services; this.orm = orm; this.userService = user; this.viewService = view; - this.getDomainTreeDescription = useGetDomainTreeDescription(fieldService); + this.getDomainTreeDescription = useGetDomainTreeDescription(fieldService, nameService); // used to manage search items related to date/datetime fields this.referenceMoment = DateTime.local(); diff --git a/addons/web/static/src/search/with_search/with_search.js b/addons/web/static/src/search/with_search/with_search.js index aa82c0d59ff..83398bfc607 100644 --- a/addons/web/static/src/search/with_search/with_search.js +++ b/addons/web/static/src/search/with_search/with_search.js @@ -24,6 +24,7 @@ export class WithSearch extends Component { orm: useService("orm"), view: useService("view"), field: useService("field"), + name: useService("name"), }, this.props.searchModelArgs ); diff --git a/addons/web/static/tests/search/search_bar_menu_tests.js b/addons/web/static/tests/search/search_bar_menu_tests.js index bdf3a3d68d6..0edf5483f36 100644 --- a/addons/web/static/tests/search/search_bar_menu_tests.js +++ b/addons/web/static/tests/search/search_bar_menu_tests.js @@ -1677,7 +1677,7 @@ QUnit.module("Search", (hooks) => { await editInput(target, ".o_domain_debug_input", `[("foo", "in", [uid, 1, "a"])]`); await click(target.querySelector(".modal footer button")); - assert.deepEqual(getFacetTexts(target), [`Foo is in ( uid , 1 , a )`]); + assert.deepEqual(getFacetTexts(target), [`Foo is in ( uid , 1 , "a" )`]); assert.deepEqual(getDomain(controlPanel), [ ["foo", "in", [7, 1, "a"]], // uid = 7 ]); @@ -1794,5 +1794,49 @@ QUnit.module("Search", (hooks) => { assert.deepEqual(getFacetTexts(target), ["Boolean is not set"]); assert.deepEqual(getDomain(controlPanel), [["boolean", "!=", true]]); }); + + QUnit.test("display names in facets", async function (assert) { + patchWithCleanup(odoo, { debug: true }); + serverData.models.partner = { + fields: {}, + records: [ + { id: 1, display_name: "John" }, + { id: 2, display_name: "David" }, + ], + }; + + const controlPanel = await makeWithSearch({ + serverData, + resModel: "foo", + Component: SearchBar, + searchMenuTypes: ["filter"], + searchViewId: false, + searchViewArch: ``, + }); + await toggleSearchBarMenu(target); + await openAddCustomFilterDialog(target); + await editInput( + target, + ".o_domain_debug_input", + `[("bar", "=", 1 ), ("bar", "in", [2, 5555]), ("bar", "!=", false), ("id", "=", 2)]` + ); + await click(target.querySelector(".modal footer button")); + + assert.deepEqual(getFacetTexts(target), [ + "Bar = John", + "Bar is in ( David , Inaccessible/missing record ID: 5555 )", + "Bar is set", + "ID = 2", + ]); + assert.deepEqual(getDomain(controlPanel), [ + "&", + ["bar", "=", 1], + "&", + ["bar", "in", [2, 5555]], + "&", + ["bar", "!=", false], + ["id", "=", 2], + ]); + }); }); });